├── source ├── cdk │ └── ecs-and-lambda │ │ ├── .npmignore │ │ ├── jest.config.js │ │ ├── .gitignore │ │ ├── servers │ │ ├── sample-ecs-weather-streamablehttp-stateless-nodejs-express │ │ │ ├── tsconfig.json │ │ │ ├── package.json │ │ │ ├── Dockerfile │ │ │ ├── src │ │ │ │ ├── oauth-cognito.ts │ │ │ │ └── index.ts │ │ │ └── package-lock.json │ │ └── sample-lambda-weather-streamablehttp-stateless-nodejs-express │ │ │ ├── tsconfig.json │ │ │ ├── Dockerfile │ │ │ ├── package.json │ │ │ └── src │ │ │ ├── oauth-cognito.ts │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ ├── package.json │ │ ├── lib │ │ ├── constructs │ │ │ ├── mcp-lambda-serverless-construct.ts │ │ │ └── mcp-fargate-server-construct.ts │ │ ├── constants │ │ │ └── geo-restrictions.ts │ │ └── stacks │ │ │ ├── vpc-stack.ts │ │ │ ├── security-stack.ts │ │ │ ├── cloudfront-waf-stack.ts │ │ │ └── mcp-server-stack.ts │ │ ├── bin │ │ └── app.ts │ │ └── cdk.json └── sample-clients │ └── simple-auth-client-python │ ├── mcp_simple_auth_client │ ├── __init__.py │ └── main.py │ ├── pyproject.toml │ └── README.md ├── assets ├── architecture-diagram.png ├── aws-cognito-mcp-integration.md └── cost-estimate-report.md ├── CODE_OF_CONDUCT.md ├── .gitignore ├── LICENSE ├── CONTRIBUTING.md └── README.md /source/cdk/ecs-and-lambda/.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /source/sample-clients/simple-auth-client-python/mcp_simple_auth_client/__init__.py: -------------------------------------------------------------------------------- 1 | """Simple OAuth client for MCP simple-auth server.""" 2 | -------------------------------------------------------------------------------- /assets/architecture-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions-library-samples/guidance-for-deploying-model-context-protocol-servers-on-aws/HEAD/assets/architecture-diagram.png -------------------------------------------------------------------------------- /source/cdk/ecs-and-lambda/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | roots: ['/test'], 4 | testMatch: ['**/*.test.ts'], 5 | transform: { 6 | '^.+\\.tsx?$': 'ts-jest' 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /source/cdk/ecs-and-lambda/.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !jest.config.js 3 | *.d.ts 4 | node_modules 5 | 6 | # CDK asset staging directory 7 | .cdk.staging 8 | cdk.out 9 | 10 | __pycache__ 11 | *.pyc 12 | *.env 13 | 14 | !lib/lambdas/cognito-redirect-updater/index.js -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | 3 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 4 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 5 | opensource-codeofconduct@amazon.com with any additional questions or comments. 6 | -------------------------------------------------------------------------------- /source/cdk/ecs-and-lambda/servers/sample-ecs-weather-streamablehttp-stateless-nodejs-express/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "Node16", 5 | "moduleResolution": "Node16", 6 | "outDir": "./build", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true 12 | }, 13 | "include": ["src/**/*"], 14 | "exclude": ["node_modules"] 15 | } 16 | -------------------------------------------------------------------------------- /source/cdk/ecs-and-lambda/servers/sample-lambda-weather-streamablehttp-stateless-nodejs-express/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "Node16", 5 | "moduleResolution": "Node16", 6 | "outDir": "./build", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true 12 | }, 13 | "include": ["src/**/*"], 14 | "exclude": ["node_modules"] 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### vsCode ### 2 | .vscode 3 | 4 | ### macOS ### 5 | # General 6 | .DS_Store 7 | .AppleDouble 8 | .LSOverride 9 | # Files that might appear in the root of a volume 10 | .DocumentRevisions-V100 11 | .fseventsd 12 | .Spotlight-V100 13 | .TemporaryItems 14 | .Trashes 15 | .VolumeIcon.icns 16 | .com.apple.timemachine.donotpresent 17 | 18 | **/node_modules 19 | **/dist 20 | **/build 21 | **/.DS_Store 22 | **/.angular 23 | **/.idea 24 | cdk.out 25 | **/data/**.csv 26 | 27 | .env 28 | **/.venv 29 | **/agentcore-gateway-and-runtime 30 | **/__pycache__ 31 | 32 | -------------------------------------------------------------------------------- /source/cdk/ecs-and-lambda/servers/sample-lambda-weather-streamablehttp-stateless-nodejs-express/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/docker/library/node:22-slim 2 | COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:0.9.1 /lambda-adapter /opt/extensions/lambda-adapter 3 | WORKDIR "/var/task" 4 | 5 | # Set environment variables 6 | ENV PORT=8080 7 | 8 | # Expose port for container 9 | EXPOSE 8080 10 | 11 | # Copy package files 12 | COPY package.json . 13 | COPY package-lock.json . 14 | COPY tsconfig.json . 15 | 16 | # Install dependencies 17 | RUN npm ci 18 | 19 | # Copy source files 20 | COPY src/ ./src/ 21 | 22 | # Build TypeScript code 23 | RUN npm run build 24 | 25 | CMD ["node", "build/index.js"] -------------------------------------------------------------------------------- /source/cdk/ecs-and-lambda/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es2020", 7 | "dom" 8 | ], 9 | "declaration": true, 10 | "strict": true, 11 | "noImplicitAny": true, 12 | "strictNullChecks": true, 13 | "noImplicitThis": true, 14 | "alwaysStrict": true, 15 | "noUnusedLocals": false, 16 | "noUnusedParameters": false, 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": false, 19 | "inlineSourceMap": true, 20 | "inlineSources": true, 21 | "experimentalDecorators": true, 22 | "strictPropertyInitialization": false, 23 | "typeRoots": [ 24 | "./node_modules/@types" 25 | ] 26 | }, 27 | "exclude": [ 28 | "node_modules", 29 | "cdk.out" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT No Attribution 2 | 3 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /source/cdk/ecs-and-lambda/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "guidance-for-remote-mcp-servers-on-aws", 3 | "version": "0.1.0", 4 | "bin": { 5 | "guidance-for-remote-mcp-servers-on-aws": "bin/app.js" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "test": "jest", 11 | "cdk": "cdk" 12 | }, 13 | "devDependencies": { 14 | "@types/jest": "^29.5.14", 15 | "@types/node": "22.7.9", 16 | "aws-cdk": "^2.1014.0", 17 | "esbuild": "^0.25.4", 18 | "jest": "^29.7.0", 19 | "ts-jest": "^29.2.5", 20 | "ts-node": "^10.9.2", 21 | "typescript": "~5.6.3" 22 | }, 23 | "dependencies": { 24 | "@modelcontextprotocol/sdk": "^1.11.0", 25 | "aws-cdk-lib": "^2.195.0", 26 | "cdk-nag": "^2.35.69", 27 | "constructs": "^10.4.2", 28 | "fetch-to-node": "^2.1.0", 29 | "hono": "^4.7.8", 30 | "jose": "^6.0.11", 31 | "node-fetch": "^3.3.2", 32 | "source-map-support": "^0.5.21", 33 | "zod": "^3.24.4" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /source/cdk/ecs-and-lambda/servers/sample-ecs-weather-streamablehttp-stateless-nodejs-express/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sample-weather-nodejs", 3 | "version": "1.0.0", 4 | "bin": { 5 | "weather": "./build/index.js" 6 | }, 7 | "type": "module", 8 | "files": [ 9 | "build" 10 | ], 11 | "main": "index.js", 12 | "scripts": { 13 | "build": "tsc && chmod 755 build/index.js", 14 | "test": "echo \"Error: no test specified\" && exit 1" 15 | }, 16 | "keywords": [], 17 | "author": "", 18 | "license": "ISC", 19 | "description": "", 20 | "dependencies": { 21 | "@modelcontextprotocol/sdk": "^1.17.2", 22 | "@types/jsonwebtoken": "^9.0.9", 23 | "@types/uuid": "^10.0.0", 24 | "cors": "^2.8.5", 25 | "express": "^5.1.0", 26 | "jose": "^6.0.11", 27 | "jsonwebtoken": "^9.0.2", 28 | "node-fetch": "^3.3.2", 29 | "uuid": "^11.1.0", 30 | "zod": "^3.24.2" 31 | }, 32 | "devDependencies": { 33 | "@types/express": "^5.0.1", 34 | "@types/node": "^22.14.1", 35 | "typescript": "^5.8.3" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /source/cdk/ecs-and-lambda/servers/sample-lambda-weather-streamablehttp-stateless-nodejs-express/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sample-weather-nodejs", 3 | "version": "1.0.0", 4 | "bin": { 5 | "weather": "./build/index.js" 6 | }, 7 | "type": "module", 8 | "files": [ 9 | "build" 10 | ], 11 | "main": "index.js", 12 | "scripts": { 13 | "build": "tsc && chmod 755 build/index.js", 14 | "test": "echo \"Error: no test specified\" && exit 1" 15 | }, 16 | "keywords": [], 17 | "author": "", 18 | "license": "ISC", 19 | "description": "", 20 | "dependencies": { 21 | "@modelcontextprotocol/sdk": "^1.17.2", 22 | "@types/jsonwebtoken": "^9.0.9", 23 | "@types/uuid": "^10.0.0", 24 | "cors": "^2.8.5", 25 | "express": "^5.1.0", 26 | "jose": "^6.0.11", 27 | "jsonwebtoken": "^9.0.2", 28 | "node-fetch": "^3.3.2", 29 | "uuid": "^11.1.0", 30 | "zod": "^3.24.2" 31 | }, 32 | "devDependencies": { 33 | "@types/express": "^5.0.1", 34 | "@types/node": "^22.14.1", 35 | "typescript": "^5.8.3" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /source/cdk/ecs-and-lambda/servers/sample-ecs-weather-streamablehttp-stateless-nodejs-express/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use Node.js 22 as the base image (LTS version) 2 | FROM node:22-slim 3 | 4 | # Set working directory 5 | WORKDIR /app 6 | 7 | # Install required packages 8 | RUN apt-get update && apt-get install -y curl && \ 9 | apt-get clean && \ 10 | rm -rf /var/lib/apt/lists/* 11 | 12 | # Copy package files 13 | COPY package.json . 14 | COPY package-lock.json . 15 | COPY tsconfig.json . 16 | 17 | # Install dependencies 18 | RUN npm ci 19 | 20 | # Copy source files 21 | COPY src/ ./src/ 22 | 23 | # Build TypeScript code 24 | RUN npm run build 25 | 26 | # Create a non-root user and group 27 | RUN groupadd --system --gid 1001 appgroup && \ 28 | useradd --system --uid 1001 --gid appgroup appuser 29 | 30 | # Set appropriate ownership 31 | RUN chown -R appuser:appgroup /app 32 | 33 | # Set environment variables 34 | ENV PORT=8080 35 | ENV BASE_PATH="" 36 | 37 | # Expose port for container 38 | EXPOSE 8080 39 | 40 | # Switch to non-root user 41 | USER appuser 42 | 43 | # Add simple healthcheck 44 | HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \ 45 | CMD curl -f http://localhost:8080${BASE_PATH}/ || exit 1 46 | 47 | # Run the weather application 48 | CMD ["node", "build/index.js"] 49 | -------------------------------------------------------------------------------- /source/sample-clients/simple-auth-client-python/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "mcp-simple-auth-client" 3 | version = "0.1.0" 4 | description = "A simple OAuth client for the MCP simple-auth server" 5 | readme = "README.md" 6 | requires-python = ">=3.10" 7 | authors = [{ name = "Anthropic" }] 8 | keywords = ["mcp", "oauth", "client", "auth"] 9 | license = { text = "MIT" } 10 | classifiers = [ 11 | "Development Status :: 4 - Beta", 12 | "Intended Audience :: Developers", 13 | "License :: OSI Approved :: MIT License", 14 | "Programming Language :: Python :: 3", 15 | "Programming Language :: Python :: 3.10", 16 | ] 17 | dependencies = [ 18 | "click>=8.2.0", 19 | "mcp>=1.0.0", 20 | ] 21 | 22 | [project.scripts] 23 | mcp-simple-auth-client = "mcp_simple_auth_client.main:cli" 24 | 25 | [build-system] 26 | requires = ["hatchling"] 27 | build-backend = "hatchling.build" 28 | 29 | [tool.hatch.build.targets.wheel] 30 | packages = ["mcp_simple_auth_client"] 31 | 32 | [tool.pyright] 33 | include = ["mcp_simple_auth_client"] 34 | venvPath = "." 35 | venv = ".venv" 36 | 37 | [tool.ruff.lint] 38 | select = ["E", "F", "I"] 39 | ignore = [] 40 | 41 | [tool.ruff] 42 | line-length = 120 43 | target-version = "py310" 44 | 45 | [tool.uv] 46 | dev-dependencies = ["pyright>=1.1.379", "pytest>=8.3.3", "ruff>=0.6.9"] 47 | 48 | [[tool.uv.index]] 49 | url = "https://pypi.org/simple" 50 | -------------------------------------------------------------------------------- /source/cdk/ecs-and-lambda/lib/constructs/mcp-lambda-serverless-construct.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from "aws-cdk-lib"; 2 | import { Construct } from "constructs"; 3 | import * as lambda from "aws-cdk-lib/aws-lambda"; 4 | import * as elbv2 from "aws-cdk-lib/aws-elasticloadbalancingv2"; 5 | import * as targets from "aws-cdk-lib/aws-elasticloadbalancingv2-targets"; 6 | import * as ec2 from "aws-cdk-lib/aws-ec2"; 7 | 8 | export interface McpLambdaServerlessConstructProps { 9 | /** 10 | * VPC where the Lambda is deployed 11 | */ 12 | vpc: ec2.IVpc; 13 | 14 | /** 15 | * The Lambda function to use 16 | */ 17 | function: lambda.IFunction; 18 | 19 | /** 20 | * Path for ALB health checks 21 | * @default /weather-nodejs/mcp 22 | */ 23 | healthCheckPath?: string; 24 | } 25 | 26 | export class McpLambdaServerlessConstruct extends Construct { 27 | public readonly targetGroup: elbv2.ApplicationTargetGroup; 28 | 29 | constructor( 30 | scope: Construct, 31 | id: string, 32 | props: McpLambdaServerlessConstructProps 33 | ) { 34 | super(scope, id); 35 | 36 | // Create target group 37 | this.targetGroup = new elbv2.ApplicationTargetGroup(this, "TargetGroup", { 38 | vpc: props.vpc, 39 | targetType: elbv2.TargetType.LAMBDA, 40 | targets: [new targets.LambdaTarget(props.function)], 41 | }); 42 | 43 | // Grant invoke permissions from ALB 44 | props.function.addPermission("AllowALBInvoke", { 45 | principal: new cdk.aws_iam.ServicePrincipal( 46 | "elasticloadbalancing.amazonaws.com" 47 | ), 48 | sourceArn: this.targetGroup.targetGroupArn, 49 | }); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /source/cdk/ecs-and-lambda/lib/constants/geo-restrictions.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Geographic restrictions for CloudFront distributions 3 | * Includes countries with: 4 | * - Major tech hubs 5 | * - AWS regions 6 | * - Significant cloud computing adoption 7 | * - Active developer communities 8 | */ 9 | export const ALLOWED_COUNTRIES = { 10 | // North America 11 | NORTH_AMERICA: [ 12 | "US", // United States 13 | "CA", // Canada 14 | "MX", // Mexico 15 | ], 16 | 17 | // Europe 18 | EUROPE: [ 19 | "GB", // United Kingdom 20 | "DE", // Germany 21 | "FR", // France 22 | "IE", // Ireland (AWS Region) 23 | "NL", // Netherlands 24 | "SE", // Sweden (AWS Region) 25 | "NO", // Norway 26 | "FI", // Finland 27 | "DK", // Denmark 28 | "ES", // Spain 29 | "IT", // Italy 30 | "CH", // Switzerland 31 | "AT", // Austria 32 | "BE", // Belgium 33 | "PL", // Poland 34 | ], 35 | 36 | // Asia Pacific 37 | ASIA_PACIFIC: [ 38 | "JP", // Japan 39 | "KR", // South Korea 40 | "SG", // Singapore 41 | "AU", // Australia 42 | "NZ", // New Zealand 43 | "HK", // Hong Kong 44 | "TW", // Taiwan 45 | "IN", // India 46 | "ID", // Indonesia 47 | "MY", // Malaysia 48 | "TH", // Thailand 49 | "VN", // Vietnam 50 | ], 51 | 52 | // South America 53 | SOUTH_AMERICA: [ 54 | "BR", // Brazil 55 | "AR", // Argentina 56 | "CL", // Chile 57 | "CO", // Colombia 58 | "PE", // Peru 59 | ], 60 | 61 | // Middle East 62 | MIDDLE_EAST: [ 63 | "AE", // United Arab Emirates 64 | "BH", // Bahrain (AWS Region) 65 | "IL", // Israel 66 | "SA", // Saudi Arabia 67 | ], 68 | }; 69 | 70 | /** 71 | * Get all allowed countries as a single array 72 | */ 73 | export const getAllowedCountries = (): string[] => { 74 | return Object.values(ALLOWED_COUNTRIES).flat(); 75 | }; 76 | -------------------------------------------------------------------------------- /source/sample-clients/simple-auth-client-python/README.md: -------------------------------------------------------------------------------- 1 | # Simple Auth Client Example 2 | 3 | A demonstration of how to use the MCP Python SDK with OAuth authentication over streamable HTTP or SSE transport. 4 | 5 | ## Features 6 | 7 | - OAuth 2.0 authentication with PKCE 8 | - Support for both StreamableHTTP and SSE transports 9 | - Interactive command-line interface 10 | 11 | ## Installation 12 | 13 | ```bash 14 | cd examples/clients/simple-auth-client 15 | uv sync --reinstall 16 | ``` 17 | 18 | ## Usage 19 | 20 | ### 1. Start an MCP server with OAuth support 21 | 22 | ```bash 23 | # Example with mcp-simple-auth 24 | cd path/to/mcp-simple-auth 25 | uv run mcp-simple-auth --transport streamable-http --port 3001 26 | ``` 27 | 28 | ### 2. Run the client 29 | 30 | ```bash 31 | uv run mcp-simple-auth-client 32 | 33 | # Or with custom server URL 34 | MCP_SERVER_PORT=3001 uv run mcp-simple-auth-client 35 | 36 | # Use SSE transport 37 | MCP_TRANSPORT_TYPE=sse uv run mcp-simple-auth-client 38 | ``` 39 | 40 | ### 3. Complete OAuth flow 41 | 42 | The client will open your browser for authentication. After completing OAuth, you can use commands: 43 | 44 | - `list` - List available tools 45 | - `call [args]` - Call a tool with optional JSON arguments 46 | - `quit` - Exit 47 | 48 | ## Example 49 | 50 | ```markdown 51 | 🔐 Simple MCP Auth Client 52 | Connecting to: http://localhost:3001 53 | 54 | Please visit the following URL to authorize the application: 55 | http://localhost:3001/authorize?response_type=code&client_id=... 56 | 57 | ✅ Connected to MCP server at http://localhost:3001 58 | 59 | mcp> list 60 | 📋 Available tools: 61 | 62 | 1. echo - Echo back the input text 63 | 64 | mcp> call echo {"text": "Hello, world!"} 65 | 🔧 Tool 'echo' result: 66 | Hello, world! 67 | 68 | mcp> quit 69 | 👋 Goodbye! 70 | ``` 71 | 72 | ## Configuration 73 | 74 | - `OAUTH_CLIENT_ID` - Cognito User Pool App Client ID 75 | - `OAUTH_CLIENT_SECRET` - Cognito User Pool App Client Secret 76 | - `MCP_SERVER_URL` - the server url you are attempting to connect to (ends with `/mcp`) 77 | -------------------------------------------------------------------------------- /source/cdk/ecs-and-lambda/lib/stacks/vpc-stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from "aws-cdk-lib"; 2 | import { Construct } from "constructs"; 3 | import * as ec2 from "aws-cdk-lib/aws-ec2"; 4 | 5 | export interface VpcStackProps extends cdk.StackProps { 6 | /** 7 | * Optional existing VPC ID to use instead of creating a new VPC 8 | */ 9 | existingVpcId?: string; 10 | 11 | /** 12 | * Optional existing public subnet IDs when using an existing VPC 13 | */ 14 | publicSubnetIds?: string[]; 15 | 16 | /** 17 | * Optional existing private subnet IDs when using an existing VPC 18 | */ 19 | privateSubnetIds?: string[]; 20 | } 21 | 22 | export class VpcStack extends cdk.Stack { 23 | public readonly vpc: ec2.IVpc; 24 | 25 | constructor(scope: Construct, id: string, props?: VpcStackProps) { 26 | super(scope, id, props); 27 | 28 | if (props?.existingVpcId) { 29 | // Use existing VPC 30 | this.vpc = ec2.Vpc.fromVpcAttributes(this, "ImportedVpc", { 31 | vpcId: props.existingVpcId, 32 | publicSubnetIds: props.publicSubnetIds || [], 33 | privateSubnetIds: props.privateSubnetIds || [], 34 | availabilityZones: cdk.Stack.of(this).availabilityZones, 35 | }); 36 | 37 | // Output information about the imported VPC 38 | new cdk.CfnOutput(this, "VpcId", { 39 | value: this.vpc.vpcId, 40 | description: "The ID of the imported VPC", 41 | }); 42 | } else { 43 | // Create new VPC 44 | this.vpc = new ec2.Vpc(this, "MCP-VPC", { 45 | maxAzs: 2, 46 | natGateways: 1, 47 | subnetConfiguration: [ 48 | { 49 | cidrMask: 24, 50 | name: "public", 51 | subnetType: ec2.SubnetType.PUBLIC, 52 | mapPublicIpOnLaunch: false, // Disable auto-assignment of public IPs 53 | }, 54 | { 55 | cidrMask: 24, 56 | name: "private", 57 | subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS, 58 | }, 59 | ], 60 | }); 61 | 62 | // Output information about the created VPC 63 | new cdk.CfnOutput(this, "VpcId", { 64 | value: this.vpc.vpcId, 65 | description: "The ID of the created VPC", 66 | }); 67 | } 68 | 69 | this.vpc.addFlowLog("VpcFlowLog", { 70 | trafficType: ec2.FlowLogTrafficType.ALL, 71 | destination: ec2.FlowLogDestination.toCloudWatchLogs(), 72 | }); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /source/cdk/ecs-and-lambda/servers/sample-ecs-weather-streamablehttp-stateless-nodejs-express/src/oauth-cognito.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * OAuth handling functionality for MCP server authentication. 3 | * Provides OAuth 2.0 authorization code flow implementation. 4 | */ 5 | 6 | import * as jose from "jose"; 7 | import fetch from "node-fetch"; 8 | 9 | /** 10 | * Validate a Cognito access token. 11 | */ 12 | export async function validateCognitoToken( 13 | token: string 14 | ): Promise<{ isValid: boolean; claims: any }> { 15 | const region = process.env.AWS_REGION || "us-west-2"; 16 | const user_pool_id = process.env.COGNITO_USER_POOL_ID; 17 | const client_id = process.env.COGNITO_CLIENT_ID; 18 | 19 | // Get the JWKs from Cognito 20 | const jwks_url = `https://cognito-idp.${region}.amazonaws.com/${user_pool_id}/.well-known/jwks.json`; 21 | 22 | try { 23 | // Fetch the JWKS 24 | const jwks_response = await fetch(jwks_url); 25 | const jwks = (await jwks_response.json()) as { keys: any[] }; 26 | 27 | // Get the key ID from the token header 28 | const { kid } = await jose.decodeProtectedHeader(token); 29 | if (!kid) { 30 | return { isValid: false, claims: {} }; 31 | } 32 | 33 | // Find the correct key 34 | const key = jwks.keys.find((k: any) => k.kid === kid); 35 | if (!key) { 36 | return { isValid: false, claims: {} }; 37 | } 38 | 39 | // Create JWKS 40 | const JWKS = jose.createLocalJWKSet({ keys: [key] }); 41 | 42 | // Define expected issuer 43 | const issuer = `https://cognito-idp.${region}.amazonaws.com/${user_pool_id}`; 44 | 45 | // Verify the token with RS256 algorithm 46 | const { payload } = await jose.jwtVerify(token, JWKS, { 47 | issuer, 48 | algorithms: ["RS256"], 49 | }); 50 | 51 | // Additional validations for Cognito access tokens 52 | if (payload.token_use !== "access") { 53 | console.log(`Invalid token_use: ${payload.token_use}, expected: access`); 54 | return { isValid: false, claims: {} }; 55 | } 56 | 57 | // For access tokens, client_id is in the 'client_id' claim, not 'aud' 58 | if (payload.client_id !== client_id) { 59 | console.log( 60 | `Invalid client_id: ${payload.client_id}, expected: ${client_id}` 61 | ); 62 | return { isValid: false, claims: {} }; 63 | } 64 | 65 | return { isValid: true, claims: payload }; 66 | } catch (error) { 67 | console.error("Token validation error:", error); 68 | return { isValid: false, claims: {} }; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /source/cdk/ecs-and-lambda/servers/sample-lambda-weather-streamablehttp-stateless-nodejs-express/src/oauth-cognito.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * OAuth handling functionality for MCP server authentication. 3 | * Provides OAuth 2.0 authorization code flow implementation. 4 | */ 5 | 6 | import * as jose from "jose"; 7 | import fetch from "node-fetch"; 8 | 9 | /** 10 | * Validate a Cognito access token. 11 | */ 12 | export async function validateCognitoToken( 13 | token: string 14 | ): Promise<{ isValid: boolean; claims: any }> { 15 | const region = process.env.AWS_REGION || "us-west-2"; 16 | const user_pool_id = process.env.COGNITO_USER_POOL_ID; 17 | const client_id = process.env.COGNITO_CLIENT_ID; 18 | 19 | // Get the JWKs from Cognito 20 | const jwks_url = `https://cognito-idp.${region}.amazonaws.com/${user_pool_id}/.well-known/jwks.json`; 21 | 22 | try { 23 | // Fetch the JWKS 24 | const jwks_response = await fetch(jwks_url); 25 | const jwks = (await jwks_response.json()) as { keys: any[] }; 26 | 27 | // Get the key ID from the token header 28 | const { kid } = await jose.decodeProtectedHeader(token); 29 | if (!kid) { 30 | return { isValid: false, claims: {} }; 31 | } 32 | 33 | // Find the correct key 34 | const key = jwks.keys.find((k: any) => k.kid === kid); 35 | if (!key) { 36 | return { isValid: false, claims: {} }; 37 | } 38 | 39 | // Create JWKS 40 | const JWKS = jose.createLocalJWKSet({ keys: [key] }); 41 | 42 | // Define expected issuer 43 | const issuer = `https://cognito-idp.${region}.amazonaws.com/${user_pool_id}`; 44 | 45 | // Verify the token with RS256 algorithm 46 | const { payload } = await jose.jwtVerify(token, JWKS, { 47 | issuer, 48 | algorithms: ["RS256"], 49 | }); 50 | 51 | // Additional validations for Cognito access tokens 52 | if (payload.token_use !== "access") { 53 | console.log(`Invalid token_use: ${payload.token_use}, expected: access`); 54 | return { isValid: false, claims: {} }; 55 | } 56 | 57 | // For access tokens, client_id is in the 'client_id' claim, not 'aud' 58 | if (payload.client_id !== client_id) { 59 | console.log( 60 | `Invalid client_id: ${payload.client_id}, expected: ${client_id}` 61 | ); 62 | return { isValid: false, claims: {} }; 63 | } 64 | 65 | return { isValid: true, claims: payload }; 66 | } catch (error) { 67 | console.error("Token validation error:", error); 68 | return { isValid: false, claims: {} }; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /source/cdk/ecs-and-lambda/bin/app.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import * as cdk from "aws-cdk-lib"; 3 | import { VpcStack } from "../lib/stacks/vpc-stack"; 4 | import { SecurityStack } from "../lib/stacks/security-stack"; 5 | import { MCPServerStack } from "../lib/stacks/mcp-server-stack"; 6 | import { CloudFrontWafStack } from "../lib/stacks/cloudfront-waf-stack"; 7 | import { Aspects } from "aws-cdk-lib"; 8 | import { AwsSolutionsChecks } from "cdk-nag"; 9 | 10 | const app = new cdk.App(); 11 | 12 | const resourceSuffix = app.node.addr 13 | .substring(0, 8) 14 | .toLowerCase() 15 | .replace(/[^a-z0-9-]/g, ""); 16 | 17 | const chars = "abcdefghijklmnopqrstuvwxyz0123456789"; 18 | let domainSuffix = ""; 19 | for (let i = 0; i < 8; i++) { 20 | domainSuffix += chars.charAt(Math.floor(Math.random() * chars.length)); 21 | } 22 | 23 | // Get context values for existing VPC if provided 24 | const existingVpcId = app.node.tryGetContext("existingVpcId"); 25 | const publicSubnetIds = app.node.tryGetContext("publicSubnetIds")?.split(","); 26 | const privateSubnetIds = app.node.tryGetContext("privateSubnetIds")?.split(","); 27 | 28 | // Create VPC stack (or use existing VPC) 29 | const vpcStack = new VpcStack(app, "MCP-VPC", { 30 | env: { 31 | account: process.env.CDK_DEFAULT_ACCOUNT, 32 | region: process.env.CDK_DEFAULT_REGION, 33 | }, 34 | existingVpcId, 35 | publicSubnetIds, 36 | privateSubnetIds, 37 | }); 38 | 39 | // Create security stack (Cognito + WAF) 40 | const securityStack = new SecurityStack(app, "MCP-Security", { 41 | env: { 42 | account: process.env.CDK_DEFAULT_ACCOUNT, 43 | region: process.env.CDK_DEFAULT_REGION, 44 | }, 45 | vpc: vpcStack.vpc, 46 | resourceSuffix, 47 | domainSuffix: domainSuffix, 48 | }); 49 | 50 | // Get the target region (where the MCP server stack will be deployed) 51 | const targetRegion = process.env.CDK_DEFAULT_REGION || "us-west-2"; 52 | 53 | // Create CloudFront WAF stack in us-east-1 (required for CloudFront WAF) 54 | const cloudFrontWafStack = new CloudFrontWafStack(app, "MCP-CloudFront-WAF", { 55 | env: { 56 | account: process.env.CDK_DEFAULT_ACCOUNT, 57 | region: "us-east-1", // CloudFront WAF must be in us-east-1 58 | }, 59 | resourceSuffix, 60 | targetRegion, // Pass the target region to the CloudFront WAF stack 61 | }); 62 | 63 | // Create MCPServerStack which includes both platform and servers 64 | const serverStack = new MCPServerStack(app, "MCP-Server", { 65 | env: { 66 | account: process.env.CDK_DEFAULT_ACCOUNT, 67 | region: process.env.CDK_DEFAULT_REGION, 68 | }, 69 | description: "Guidance for Deploying Model Context Servers on AWS (SO9018)", 70 | resourceSuffix, 71 | vpc: vpcStack.vpc, 72 | }); 73 | serverStack.addDependency(cloudFrontWafStack); 74 | 75 | // Tag all resources 76 | cdk.Tags.of(app).add("Project", "MCP-Servers-On-AWS"); 77 | 78 | Aspects.of(app).add(new AwsSolutionsChecks({ verbose: true })); 79 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | ## Reporting Bugs/Feature Requests 10 | 11 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 12 | 13 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 14 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 15 | 16 | - A reproducible test case or series of steps 17 | - The version of our code being used 18 | - Any modifications you've made relevant to the bug 19 | - Anything unusual about your environment or deployment 20 | 21 | ## Contributing via Pull Requests 22 | 23 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 24 | 25 | 1. You are working against the latest source on the _main_ branch. 26 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 27 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 28 | 29 | To send us a pull request, please: 30 | 31 | 1. Fork the repository. 32 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 33 | 3. Ensure local tests pass. 34 | 4. Commit to your fork using clear commit messages. 35 | 5. Send us a pull request, answering any default questions in the pull request interface. 36 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 37 | 38 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 39 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 40 | 41 | ## Finding contributions to work on 42 | 43 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 44 | 45 | ## Code of Conduct 46 | 47 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 48 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 49 | opensource-codeofconduct@amazon.com with any additional questions or comments. 50 | 51 | ## Security issue notifications 52 | 53 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 54 | 55 | ## Licensing 56 | 57 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 58 | -------------------------------------------------------------------------------- /source/cdk/ecs-and-lambda/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/app.ts", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "**/*.d.ts", 11 | "**/*.js", 12 | "tsconfig.json", 13 | "package*.json", 14 | "yarn.lock", 15 | "node_modules", 16 | "test" 17 | ] 18 | }, 19 | "context": { 20 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 21 | "@aws-cdk/core:checkSecretUsage": true, 22 | "@aws-cdk/core:target-partitions": [ 23 | "aws", 24 | "aws-cn" 25 | ], 26 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 27 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 28 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 29 | "@aws-cdk/aws-iam:minimizePolicies": true, 30 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 31 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 32 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 33 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 34 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 35 | "@aws-cdk/core:enablePartitionLiterals": true, 36 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, 37 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, 38 | "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, 39 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, 40 | "@aws-cdk/aws-route53-patters:useCertificate": true, 41 | "@aws-cdk/customresources:installLatestAwsSdkDefault": false, 42 | "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, 43 | "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, 44 | "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, 45 | "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, 46 | "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, 47 | "@aws-cdk/aws-redshift:columnId": true, 48 | "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, 49 | "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, 50 | "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, 51 | "@aws-cdk/aws-kms:aliasNameRef": true, 52 | "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, 53 | "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, 54 | "@aws-cdk/aws-efs:denyAnonymousAccess": true, 55 | "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, 56 | "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, 57 | "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, 58 | "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, 59 | "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, 60 | "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, 61 | "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, 62 | "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true, 63 | "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true, 64 | "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true, 65 | "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true, 66 | "@aws-cdk/aws-eks:nodegroupNameAttribute": true, 67 | "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true, 68 | "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true, 69 | "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false, 70 | "@aws-cdk/aws-s3:keepNotificationInImportedBucket": false, 71 | "@aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions": true, 72 | "@aws-cdk/aws-dynamodb:resourcePolicyPerReplica": true, 73 | "@aws-cdk/aws-ec2:ec2SumTImeoutEnabled": true, 74 | "@aws-cdk/aws-appsync:appSyncGraphQLAPIScopeLambdaPermission": true, 75 | "@aws-cdk/aws-rds:setCorrectValueForDatabaseInstanceReadReplicaInstanceResourceId": true, 76 | "@aws-cdk/core:cfnIncludeRejectComplexResourceUpdateCreatePolicyIntrinsics": true, 77 | "@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages": true, 78 | "@aws-cdk/aws-stepfunctions-tasks:fixRunEcsTaskPolicy": true 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /source/cdk/ecs-and-lambda/lib/stacks/security-stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from "aws-cdk-lib"; 2 | import { Construct } from "constructs"; 3 | import * as ec2 from "aws-cdk-lib/aws-ec2"; 4 | import * as cognito from "aws-cdk-lib/aws-cognito"; 5 | import * as wafv2 from "aws-cdk-lib/aws-wafv2"; 6 | import * as ssm from "aws-cdk-lib/aws-ssm"; 7 | import { NagSuppressions } from "cdk-nag"; 8 | import { StandardThreatProtectionMode } from "aws-cdk-lib/aws-cognito"; 9 | 10 | export interface SecurityStackProps extends cdk.StackProps { 11 | /** 12 | * The VPC where resources will be deployed 13 | */ 14 | vpc: ec2.IVpc; 15 | 16 | /** 17 | * Resource suffix for unique naming 18 | */ 19 | resourceSuffix: string; 20 | domainSuffix: string; 21 | } 22 | 23 | export class SecurityStack extends cdk.Stack { 24 | public readonly userPool: cognito.UserPool; 25 | public readonly appClientClientCredentials: cognito.UserPoolClient; 26 | public readonly appClientUser: cognito.UserPoolClient; 27 | public readonly webAcl: wafv2.CfnWebACL; 28 | 29 | constructor(scope: Construct, id: string, props: SecurityStackProps) { 30 | super(scope, id, props); 31 | 32 | // Create Cognito User Pool 33 | this.userPool = new cognito.UserPool(this, "MCPServerUserPool", { 34 | userPoolName: "mcp-server-user-pool", 35 | selfSignUpEnabled: true, // Allow users to sign up 36 | featurePlan: cognito.FeaturePlan.PLUS, 37 | standardThreatProtectionMode: StandardThreatProtectionMode.FULL_FUNCTION, 38 | signInAliases: { 39 | username: true, 40 | email: true, 41 | }, 42 | autoVerify: { 43 | email: true, 44 | }, 45 | passwordPolicy: { 46 | minLength: 12, 47 | requireLowercase: true, 48 | requireUppercase: true, 49 | requireDigits: true, 50 | requireSymbols: true, 51 | }, 52 | removalPolicy: cdk.RemovalPolicy.DESTROY, // For demo purposes only 53 | 54 | // Enable MFA configuration with TOTP only 55 | mfa: cognito.Mfa.REQUIRED, // Allow users to enable MFA 56 | mfaSecondFactor: { 57 | sms: false, 58 | otp: true, // Enable TOTP only 59 | }, 60 | }); 61 | 62 | // Add domain for hosted UI 63 | const domainPrefix = `mcp-server-${props.domainSuffix}`; 64 | 65 | this.userPool.addDomain("CognitoDomain", { 66 | cognitoDomain: { 67 | domainPrefix, 68 | }, 69 | }); 70 | 71 | // Create resource server for OAuth scopes 72 | const resourceServer = this.userPool.addResourceServer( 73 | "MCPResourceServer", 74 | { 75 | identifier: "mcp-server", 76 | scopes: [ 77 | { 78 | scopeName: "read", 79 | scopeDescription: "Read access to MCP Server", 80 | }, 81 | { 82 | scopeName: "write", 83 | scopeDescription: "Write access to MCP Server", 84 | }, 85 | ], 86 | } 87 | ); 88 | 89 | // Create app client for interactive user authentication 90 | this.appClientUser = this.userPool.addClient("mcp-user-client", { 91 | userPoolClientName: "mcp-user-client", 92 | generateSecret: true, 93 | authFlows: { 94 | userPassword: true, 95 | adminUserPassword: true, 96 | userSrp: true, 97 | }, 98 | accessTokenValidity: cdk.Duration.minutes(60), // default is 60 minutes 99 | idTokenValidity: cdk.Duration.minutes(60), // default is 60 minutes 100 | refreshTokenValidity: cdk.Duration.days(30), // default is 30 days 101 | oAuth: { 102 | flows: { 103 | authorizationCodeGrant: true, 104 | implicitCodeGrant: true, 105 | }, 106 | scopes: [ 107 | cognito.OAuthScope.EMAIL, 108 | cognito.OAuthScope.OPENID, 109 | cognito.OAuthScope.PROFILE, 110 | cognito.OAuthScope.resourceServer(resourceServer, { 111 | scopeName: "read", 112 | scopeDescription: "Read access to MCP Server", 113 | }), 114 | cognito.OAuthScope.resourceServer(resourceServer, { 115 | scopeName: "write", 116 | scopeDescription: "Write access to MCP Server", 117 | }), 118 | ], 119 | callbackUrls: [ 120 | "http://localhost:2299/callback", // for local development/testing with sample-auth-python server 121 | ], 122 | }, 123 | preventUserExistenceErrors: true, 124 | }); 125 | 126 | // Create WAF Web ACL 127 | this.webAcl = new wafv2.CfnWebACL(this, "MCPServerWAF", { 128 | name: "mcp-server-waf", 129 | defaultAction: { allow: {} }, 130 | scope: "REGIONAL", 131 | visibilityConfig: { 132 | cloudWatchMetricsEnabled: true, 133 | metricName: "MCPServerWAF", 134 | sampledRequestsEnabled: true, 135 | }, 136 | rules: [ 137 | // AWS Managed Rules - Core rule set 138 | { 139 | name: "AWS-AWSManagedRulesCommonRuleSet", 140 | priority: 10, 141 | statement: { 142 | managedRuleGroupStatement: { 143 | name: "AWSManagedRulesCommonRuleSet", 144 | vendorName: "AWS", 145 | }, 146 | }, 147 | overrideAction: { none: {} }, 148 | visibilityConfig: { 149 | cloudWatchMetricsEnabled: true, 150 | metricName: "AWS-AWSManagedRulesCommonRuleSet", 151 | sampledRequestsEnabled: true, 152 | }, 153 | }, 154 | // Rate-based rule to prevent DDoS 155 | { 156 | name: "RateLimitRule", 157 | priority: 20, 158 | statement: { 159 | rateBasedStatement: { 160 | limit: 1000, 161 | aggregateKeyType: "IP", 162 | }, 163 | }, 164 | action: { block: {} }, 165 | visibilityConfig: { 166 | cloudWatchMetricsEnabled: true, 167 | metricName: "RateLimitRule", 168 | sampledRequestsEnabled: true, 169 | }, 170 | }, 171 | ], 172 | }); 173 | 174 | // Output Client ID and endpoint 175 | new cdk.CfnOutput(this, "UserPoolId", { 176 | value: this.userPool.userPoolId, 177 | description: "The ID of the Cognito User Pool", 178 | }); 179 | 180 | new cdk.CfnOutput(this, "UserPoolClientId", { 181 | value: this.appClientUser.userPoolClientId, 182 | description: "The Client ID for the Cognito User Pool Client", 183 | }); 184 | 185 | // Store the user pool ID in SSM for later use in MCP server stack 186 | new ssm.StringParameter(this, "UserPoolIdParameter", { 187 | parameterName: `/mcp/cognito/user-pool-id-${props.resourceSuffix}`, 188 | description: "The user pool ID for user authentication", 189 | stringValue: this.userPool.userPoolId, 190 | }); 191 | 192 | // Store the app client ID in SSM for later use in MCP server stack 193 | new ssm.StringParameter(this, "UserPoolClientIdParameter", { 194 | parameterName: `/mcp/cognito/user-pool-client-id-${props.resourceSuffix}`, 195 | description: "The user pool client ID for user authentication", 196 | stringValue: this.appClientUser.userPoolClientId, 197 | }); 198 | 199 | // Add suppressions for CDK nag rules 200 | NagSuppressions.addStackSuppressions(this, [ 201 | { 202 | id: "AwsSolutions-IAM4", 203 | reason: 204 | "SSM Parameter custom resource Lambda function requires basic execution role for CloudWatch logs", 205 | appliesTo: [ 206 | "Policy::arn::iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", 207 | ], 208 | }, 209 | { 210 | id: "AwsSolutions-L1", 211 | reason: 212 | "SSM Parameter custom resource Lambda function uses runtime defined by L2 construct", 213 | }, 214 | ]); 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /assets/aws-cognito-mcp-integration.md: -------------------------------------------------------------------------------- 1 | # AWS Cognito Integration with MCP Server 2 | 3 | This document illustrates the technical architecture of AWS Cognito integration with MCP servers using OAuth 2.0 Protected Resource Metadata (RFC9728) and StreamableHTTP transport, implementing the 2025-06-18 MCP specification. 4 | 5 | ## OAuth 2.0 Protected Resource Flow 6 | 7 | The complete Authorization flow proceeds as follows: 8 | 9 | ```mermaid 10 | sequenceDiagram 11 | participant User as User-Agent (Browser) 12 | participant Client as Client 13 | participant MCP as MCP Server (Resource Server) 14 | participant AS as Authorization Server 15 | 16 | Note over Client,AS: Initial MCP request triggers OAuth flow 17 | Client->>MCP: MCP request without token 18 | MCP-->>Client: HTTP 401 Unauthorized with WWW-Authenticate header 19 | 20 | Note over Client: Extract resource_metadata URL from WWW-Authenticate 21 | Client->>MCP: Request Protected Resource Metadata 22 | MCP-->>Client: Return metadata 23 | 24 | Note over Client: Parse metadata and extract authorization server(s)
Client determines AS to use 25 | Client->>AS: GET /.well-known/openid-configuration 26 | AS-->>Client: Authorization server metadata response 27 | 28 | Note over Client: Client uses pre-configured credentials
(No dynamic client registration) 29 | 30 | Note over Client: Generate PKCE parameters
Include resource parameter 31 | Client->>User: Open browser with authorization URL + code_challenge + resource 32 | User->>AS: Authorization request with resource parameter 33 | 34 | Note over AS: User authorizes 35 | AS->>User: Redirect to callback with authorization code 36 | User->>Client: Authorization code callback 37 | 38 | Client->>AS: Token request + code_verifier + resource 39 | AS-->>Client: Access token (+ refresh token) 40 | 41 | Client->>MCP: MCP request with access token 42 | MCP-->>Client: MCP response 43 | 44 | Note over Client,MCP: MCP communication continues with valid token 45 | ``` 46 | 47 | ## Architecture Components 48 | 49 | ```mermaid 50 | flowchart TD 51 | %% Client and User Agent 52 | User["User-Agent (Browser)"] 53 | Client["MCP Client"] 54 | 55 | %% MCP Server Components 56 | subgraph "MCP Server on AWS" 57 | CloudFront["AWS CloudFront
HTTPS Distribution"] 58 | ALB["Application Load Balancer"] 59 | ECS["ECS Fargate Cluster / Lambda"] 60 | 61 | subgraph "MCP Server Container" 62 | MCPEndpoint["MCP StreamableHTTP Endpoint
/mcp"] 63 | WellKnown["OAuth Protected Resource Metadata
/.well-known/oauth-protected-resource"] 64 | AuthMiddleware["Authentication Middleware
(Token Validation)"] 65 | MCPTools["MCP Tools & Resources
(Weather API)"] 66 | end 67 | 68 | %% Container connections 69 | MCPEndpoint --- AuthMiddleware 70 | AuthMiddleware --- MCPTools 71 | WellKnown -.-> MCPEndpoint 72 | end 73 | 74 | %% AWS Cognito Components 75 | subgraph "AWS Cognito (Authorization Server)" 76 | UserPool["Cognito User Pool"] 77 | AppClient["App Client
with OIDC scopes"] 78 | Domain["Cognito Domain
{prefix}.auth.{region}.amazoncognito.com"] 79 | WellKnownAS["/.well-known/openid-configuration"] 80 | 81 | %% Cognito internal connections 82 | UserPool --- AppClient 83 | UserPool --- Domain 84 | UserPool --- WellKnownAS 85 | end 86 | 87 | %% AWS Supporting Services 88 | subgraph "Supporting AWS Services" 89 | SSM["SSM Parameter Store
- HTTPS URLs
- User Pool IDs
- Client Credentials"] 90 | WAF["AWS WAF"] 91 | end 92 | 93 | %% External connections 94 | User <--> Client 95 | User <--> CloudFront 96 | User <--> Domain 97 | 98 | %% AWS Architecture flow 99 | Client <--> CloudFront 100 | CloudFront <--> ALB 101 | ALB <--> ECS 102 | WAF -.-> CloudFront 103 | 104 | %% Authentication flow 105 | AuthMiddleware <--> UserPool 106 | Client <--> WellKnown 107 | Client <--> WellKnownAS 108 | AuthMiddleware -..-> SSM 109 | ECS -..-> SSM 110 | 111 | %% CDK Deployment components 112 | subgraph "CDK Deployment" 113 | VPCStack["VPC Stack
- VPC
- Subnets
- Security Groups"] 114 | SecurityStack["Security Stack
- User Pool
- App Client"] 115 | CloudFrontWAFStack["CloudFront WAF Stack
- WAF Web ACL
- (us-east-1 only)"] 116 | MCPServerStack["MCP Server Stack
- Fargate/Lambda Service
- CloudFront
- ALB"] 117 | CrossRegionSync["Cross-Region
Parameter Sync"] 118 | 119 | VPCStack --> MCPServerStack 120 | SecurityStack --> MCPServerStack 121 | CloudFrontWAFStack --> MCPServerStack 122 | CloudFrontWAFStack --> CrossRegionSync 123 | end 124 | 125 | %% Environment Variables Configuration 126 | subgraph "Container Environment" 127 | EnvVars["Environment Variables
- COGNITO_USER_POOL_ID
- COGNITO_CLIENT_ID
- COGNITO_CLIENT_SECRET
- BASE_URL"] 128 | end 129 | 130 | MCPServerStack --> EnvVars 131 | EnvVars --> AuthMiddleware 132 | 133 | %% Legend 134 | classDef aws fill:#FF9900,stroke:#232F3E,color:white 135 | classDef mcp fill:#1D3557,stroke:#457B9D,color:white 136 | class UserPool,AppClient,Domain,WellKnownAS,CloudFront,ALB,ECS,SSM,WAF,CrossRegionSync aws 137 | class MCPEndpoint,WellKnown,AuthMiddleware,MCPTools,Client aws 138 | 139 | %% Style for CDK stacks 140 | classDef cdkStack fill:#232F3E,stroke:#FF9900,color:white 141 | class VPCStack,SecurityStack,CloudFrontWAFStack,MCPServerStack cdkStack 142 | ``` 143 | 144 | ## Key Implementation Details 145 | 146 | ### OAuth 2.0 Protected Resource Metadata (RFC9728) 147 | 148 | The MCP server implements OAuth 2.0 Protected Resource Metadata specification: 149 | 150 | - **Endpoint**: `/.well-known/oauth-protected-resource` 151 | - **Purpose**: Advertises OAuth configuration and authorization servers 152 | - **Content**: JSON metadata including resource identifier and supported authorization servers 153 | 154 | ### WWW-Authenticate Header 155 | 156 | When unauthorized requests are made to the MCP endpoint: 157 | 158 | ```http 159 | HTTP/1.1 401 Unauthorized 160 | WWW-Authenticate: Bearer realm="mcp-server", resource_metadata="https://example.com/.well-known/oauth-protected-resource" 161 | ``` 162 | 163 | ### StreamableHTTP Transport 164 | 165 | - **Protocol**: HTTP/HTTPS with JSON-RPC 2.0 over POST requests 166 | - **Stateless**: Each request creates a new server instance for concurrent client support 167 | - **Authentication**: Bearer token validation on each request 168 | - **Endpoints**: 169 | - `POST /mcp` - Main MCP communication endpoint 170 | - `GET /.well-known/oauth-protected-resource` - OAuth metadata endpoint 171 | 172 | ### Token Validation Flow 173 | 174 | 1. Client makes MCP request without authentication 175 | 2. Server responds with 401 and WWW-Authenticate header 176 | 3. Client discovers OAuth metadata from protected resource endpoint 177 | 4. Client performs OAuth authorization code flow with PKCE 178 | 5. Client includes resource parameter in OAuth requests 179 | 6. Client makes authenticated MCP requests with bearer token 180 | 7. Server validates token against AWS Cognito User Pool 181 | 182 | ### Environment Configuration 183 | 184 | The MCP server requires these environment variables: 185 | 186 | - `COGNITO_USER_POOL_ID` - AWS Cognito User Pool identifier 187 | - `COGNITO_CLIENT_ID` - OAuth client identifier (optional for public clients) 188 | - `COGNITO_CLIENT_SECRET` - OAuth client secret (for confidential clients) 189 | - `BASE_URL` - Base URL for generating OAuth metadata 190 | - `AWS_REGION` - AWS region for Cognito integration 191 | 192 | ### Deployment Architecture 193 | 194 | - **VPC Stack**: Creates VPC, subnets, and security groups for network infrastructure 195 | - **Security Stack**: Creates Cognito User Pool and App Client for authentication 196 | - **CloudFront WAF Stack**: Creates WAF Web ACL for CloudFront (deployed in us-east-1 only) 197 | - **MCP Server Stack**: Deploys Fargate service or Lambda function, CloudFront distribution, and Application Load Balancer 198 | - **Cross-Region Sync**: Synchronizes CloudFront WAF parameters across AWS regions 199 | 200 | ### Client Implementation 201 | 202 | The Python client demonstrates: 203 | 204 | - **Pre-configured Client Credentials**: Uses static client ID/secret (no dynamic client registration) 205 | - **PKCE Flow**: Proof Key for Code Exchange for enhanced security 206 | - **Resource Parameters**: Including resource identifier in OAuth requests 207 | - **Token Management**: Automatic token refresh and storage 208 | - **Interactive Interface**: Command-line interface for testing MCP tools 209 | 210 | ### Important Limitations 211 | 212 | - **No Dynamic Client Registration (DCR)**: This implementation does not support dynamic client registration. Client credentials must be pre-configured in AWS Cognito and provided via environment variables. 213 | 214 | This implementation follows the 2025-06-18 MCP specification with full OAuth 2.0 Protected Resource support, enabling secure and standards-compliant authentication for MCP servers deployed on AWS. 215 | -------------------------------------------------------------------------------- /source/cdk/ecs-and-lambda/lib/constructs/mcp-fargate-server-construct.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from "aws-cdk-lib"; 2 | import { Construct } from "constructs"; 3 | import * as path from "path"; 4 | import * as ecs from "aws-cdk-lib/aws-ecs"; 5 | import * as ec2 from "aws-cdk-lib/aws-ec2"; 6 | import * as elbv2 from "aws-cdk-lib/aws-elasticloadbalancingv2"; 7 | import * as ecr_assets from "aws-cdk-lib/aws-ecr-assets"; 8 | import * as logs from "aws-cdk-lib/aws-logs"; 9 | import { NagSuppressions } from "cdk-nag"; 10 | 11 | export interface McpFargateServerConstructProps { 12 | /** 13 | * The shared platform components 14 | */ 15 | platform: { 16 | vpc: ec2.IVpc; 17 | cluster: ecs.Cluster; 18 | }; 19 | 20 | /** 21 | * The name of the MCP server 22 | */ 23 | serverName: string; 24 | 25 | /** 26 | * The path to the server implementation 27 | */ 28 | serverPath: string; 29 | 30 | /** 31 | * The port that the container listens on 32 | * @default 8080 33 | */ 34 | containerPort?: number; 35 | 36 | /** 37 | * The health check path 38 | * @default /health 39 | */ 40 | healthCheckPath?: string; 41 | 42 | /** 43 | * Environment variables for the container 44 | */ 45 | environment?: Record; 46 | 47 | /** 48 | * Secret environment variables for the container 49 | */ 50 | secrets?: Record; 51 | 52 | /** 53 | * Memory limit for the Fargate task 54 | * @default 512 55 | */ 56 | memoryLimitMiB?: number; 57 | 58 | /** 59 | * CPU units for the Fargate task 60 | * @default 256 61 | */ 62 | cpuUnits?: number; 63 | 64 | /** 65 | * Container count for the Fargate service 66 | * @default 2 67 | */ 68 | desiredCount?: number; 69 | 70 | /** 71 | * Auto-scaling config for minimum capacity 72 | * @default 1 73 | */ 74 | minCapacity?: number; 75 | 76 | /** 77 | * Auto-scaling config for maximum capacity 78 | * @default 5 79 | */ 80 | maxCapacity?: number; 81 | 82 | /** 83 | * EC2 security group for application load balancer 84 | */ 85 | albSecurityGroup: ec2.SecurityGroup; 86 | 87 | urlParameterName: string; 88 | } 89 | 90 | export class McpFargateServerConstruct extends Construct { 91 | public readonly fargateService: ecs.FargateService; 92 | public readonly targetGroup: elbv2.ApplicationTargetGroup; 93 | 94 | constructor( 95 | scope: Construct, 96 | id: string, 97 | props: McpFargateServerConstructProps 98 | ) { 99 | super(scope, id); 100 | 101 | // Set defaults 102 | const serverName = props.serverName; 103 | const containerPort = props.containerPort || 8080; 104 | const healthCheckPath = props.healthCheckPath || "/health"; 105 | const memoryLimitMiB = props.memoryLimitMiB || 512; 106 | const cpuUnits = props.cpuUnits || 256; 107 | const desiredCount = props.desiredCount || 2; 108 | const minCapacity = props.minCapacity || 1; 109 | const maxCapacity = props.maxCapacity || 5; 110 | const albSecurityGroup = props.albSecurityGroup; 111 | 112 | // Create Docker image asset with platform specification for cross-architecture compatibility 113 | const dockerImage = new ecr_assets.DockerImageAsset( 114 | this, 115 | `${serverName}Image`, 116 | { 117 | // Sanitize the path to prevent path traversal attacks 118 | directory: path.resolve(props.serverPath.replace(/\.\./g, "")), 119 | platform: ecr_assets.Platform.LINUX_AMD64, // Explicitly build for x86 Linux 120 | buildArgs: { 121 | // Build arguments if needed 122 | }, 123 | } 124 | ); 125 | 126 | // Use the shared cluster from the platform 127 | const cluster = props.platform.cluster; 128 | 129 | // Create task definition 130 | const taskDefinition = new ecs.FargateTaskDefinition( 131 | this, 132 | `${serverName}Task`, 133 | { 134 | memoryLimitMiB, 135 | cpu: cpuUnits, 136 | } 137 | ); 138 | 139 | // Add permissions to the task role to read SSM parameters 140 | taskDefinition.taskRole.addToPrincipalPolicy( 141 | new cdk.aws_iam.PolicyStatement({ 142 | actions: ["ssm:GetParameter", "ssm:GetParameters"], 143 | resources: [ 144 | `arn:aws:ssm:${cdk.Stack.of(this).region}:${ 145 | cdk.Stack.of(this).account 146 | }:parameter/mcp/*`, 147 | ], 148 | effect: cdk.aws_iam.Effect.ALLOW, 149 | }) 150 | ); 151 | 152 | // Create log group for container 153 | const logGroup = new logs.LogGroup(this, `${serverName}Logs`, { 154 | logGroupName: `/ecs/mcp-${serverName.toLowerCase()}-server`, 155 | retention: logs.RetentionDays.ONE_WEEK, 156 | removalPolicy: cdk.RemovalPolicy.DESTROY, 157 | }); 158 | 159 | // Add container to task definition 160 | const container = taskDefinition.addContainer(`${serverName}Container`, { 161 | image: ecs.ContainerImage.fromDockerImageAsset(dockerImage), 162 | logging: ecs.LogDrivers.awsLogs({ 163 | streamPrefix: `mcp-${serverName.toLowerCase()}`, 164 | logGroup, 165 | }), 166 | healthCheck: { 167 | command: [ 168 | "CMD-SHELL", 169 | `curl -f http://localhost:${containerPort}${healthCheckPath} || exit 1`, 170 | ], 171 | interval: cdk.Duration.seconds(30), 172 | timeout: cdk.Duration.seconds(5), 173 | retries: 3, 174 | startPeriod: cdk.Duration.seconds(60), 175 | }, 176 | environment: { 177 | ...props.environment, 178 | }, 179 | secrets: props.secrets || {}, 180 | }); 181 | 182 | // Map container port 183 | container.addPortMappings({ 184 | containerPort, 185 | hostPort: containerPort, 186 | protocol: ecs.Protocol.TCP, 187 | }); 188 | 189 | // Create security group for the Fargate service 190 | const serviceSecurityGroup = new ec2.SecurityGroup( 191 | this, 192 | `${serverName}ServiceSecurityGroup`, 193 | { 194 | vpc: props.platform.vpc, 195 | allowAllOutbound: true, 196 | description: `Security group for ${serverName} MCP server service`, 197 | } 198 | ); 199 | 200 | // Allow inbound traffic from ALB 201 | serviceSecurityGroup.addIngressRule( 202 | ec2.Peer.securityGroupId(albSecurityGroup.securityGroupId), 203 | ec2.Port.tcp(containerPort), 204 | `Allow traffic from ${serverName} ALB to container` 205 | ); 206 | 207 | // Create Fargate service 208 | this.fargateService = new ecs.FargateService(this, `${serverName}Service`, { 209 | cluster, 210 | taskDefinition, 211 | desiredCount, 212 | securityGroups: [serviceSecurityGroup], 213 | assignPublicIp: false, 214 | minHealthyPercent: 50, 215 | maxHealthyPercent: 100, 216 | }); 217 | 218 | // Enable auto-scaling 219 | const scaling = this.fargateService.autoScaleTaskCount({ 220 | minCapacity, 221 | maxCapacity, 222 | }); 223 | 224 | scaling.scaleOnCpuUtilization(`${serverName}CpuScaling`, { 225 | targetUtilizationPercent: 70, 226 | scaleInCooldown: cdk.Duration.seconds(60), 227 | scaleOutCooldown: cdk.Duration.seconds(60), 228 | }); 229 | 230 | // Create target group with a unique name 231 | this.targetGroup = new elbv2.ApplicationTargetGroup( 232 | this, 233 | `${serverName}TargetGroup`, 234 | { 235 | vpc: props.platform.vpc, 236 | port: containerPort, 237 | protocol: elbv2.ApplicationProtocol.HTTP, 238 | targetType: elbv2.TargetType.IP, 239 | targetGroupName: `${id}-${serverName}-tg` 240 | .substring(0, 32) 241 | .toLowerCase(), 242 | healthCheck: { 243 | path: healthCheckPath, 244 | port: containerPort.toString(), 245 | interval: cdk.Duration.seconds(30), 246 | timeout: cdk.Duration.seconds(5), 247 | healthyThresholdCount: 3, 248 | unhealthyThresholdCount: 3, 249 | }, 250 | } 251 | ); 252 | 253 | // Register targets 254 | this.targetGroup.addTarget(this.fargateService); 255 | 256 | // Add suppressions for IAM wildcards 257 | NagSuppressions.addResourceSuppressions( 258 | taskDefinition, 259 | [ 260 | { 261 | id: "AwsSolutions-IAM5", 262 | reason: 263 | "Task role needs access to MCP-related SSM parameters using consistent prefix pattern", 264 | appliesTo: [ 265 | `Resource::arn:aws:ssm:${cdk.Stack.of(this).region}:${ 266 | cdk.Stack.of(this).account 267 | }:parameter/mcp/*`, 268 | ], 269 | }, 270 | { 271 | id: "AwsSolutions-IAM5", 272 | reason: 273 | "ECS task execution role requires ECR, CloudWatch Logs, and Secrets Manager access", 274 | appliesTo: ["Resource::*"], 275 | }, 276 | { 277 | id: "AwsSolutions-ECS2", 278 | reason: 279 | "Environment variables contain non-sensitive configuration values only - sensitive values are passed via Secrets Manager", 280 | }, 281 | ], 282 | true // Apply to child constructs including task and execution roles 283 | ); 284 | 285 | // Update the container's environment to include the parameter name 286 | container.addEnvironment( 287 | "MCP_SERVER_BASE_URL_PARAMETER_NAME", 288 | props.urlParameterName 289 | ); 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /source/cdk/ecs-and-lambda/lib/stacks/cloudfront-waf-stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from "aws-cdk-lib"; 2 | import { Construct } from "constructs"; 3 | import * as wafv2 from "aws-cdk-lib/aws-wafv2"; 4 | import * as ssm from "aws-cdk-lib/aws-ssm"; 5 | import { NagSuppressions } from "cdk-nag"; 6 | 7 | export interface CloudFrontWafStackProps extends cdk.StackProps { 8 | /** 9 | * Resource suffix for unique naming 10 | */ 11 | resourceSuffix: string; 12 | 13 | /** 14 | * Target region where the main stack is deployed 15 | */ 16 | targetRegion: string; 17 | } 18 | 19 | /** 20 | * Stack that creates a CloudFront-scoped WAF Web ACL 21 | * This stack must be deployed in us-east-1 region 22 | */ 23 | export class CloudFrontWafStack extends cdk.Stack { 24 | public readonly webAcl: wafv2.CfnWebACL; 25 | public readonly webAclArn: string; 26 | public readonly webAclId: string; 27 | 28 | constructor(scope: Construct, id: string, props: CloudFrontWafStackProps) { 29 | super(scope, id, props); 30 | 31 | // Verify we're in us-east-1 since CloudFront WAF must be in that region 32 | if (this.region !== "us-east-1") { 33 | throw new Error( 34 | "CloudFrontWafStack must be deployed in us-east-1 region only" 35 | ); 36 | } 37 | 38 | // Create WAF Web ACL for CloudFront (global scope) 39 | this.webAcl = new wafv2.CfnWebACL(this, "MCPCloudFrontWAF", { 40 | name: `mcp-cloudfront-waf-${props.resourceSuffix}`, 41 | defaultAction: { allow: {} }, 42 | scope: "CLOUDFRONT", // Must be CLOUDFRONT for CloudFront distributions 43 | visibilityConfig: { 44 | cloudWatchMetricsEnabled: true, 45 | metricName: "MCPCloudFrontWAF", 46 | sampledRequestsEnabled: true, 47 | }, 48 | rules: [ 49 | // Allow rule for OAuth endpoints 50 | { 51 | name: "AllowOAuthEndpoints", 52 | priority: 1, // Highest priority 53 | statement: { 54 | orStatement: { 55 | statements: [ 56 | // /register endpoint 57 | { 58 | byteMatchStatement: { 59 | fieldToMatch: { 60 | uriPath: {}, 61 | }, 62 | positionalConstraint: "EXACTLY", 63 | searchString: "/register", 64 | textTransformations: [ 65 | { 66 | priority: 0, 67 | type: "NONE", 68 | }, 69 | ], 70 | }, 71 | }, 72 | // /authorize endpoint 73 | { 74 | byteMatchStatement: { 75 | fieldToMatch: { 76 | uriPath: {}, 77 | }, 78 | positionalConstraint: "EXACTLY", 79 | searchString: "/authorize", 80 | textTransformations: [ 81 | { 82 | priority: 0, 83 | type: "NONE", 84 | }, 85 | ], 86 | }, 87 | }, 88 | // /token endpoint 89 | { 90 | byteMatchStatement: { 91 | fieldToMatch: { 92 | uriPath: {}, 93 | }, 94 | positionalConstraint: "EXACTLY", 95 | searchString: "/token", 96 | textTransformations: [ 97 | { 98 | priority: 0, 99 | type: "NONE", 100 | }, 101 | ], 102 | }, 103 | }, 104 | ], 105 | }, 106 | }, 107 | action: { allow: {} }, 108 | visibilityConfig: { 109 | cloudWatchMetricsEnabled: true, 110 | metricName: "AllowOAuthEndpoints", 111 | sampledRequestsEnabled: true, 112 | }, 113 | }, 114 | // AWS Managed Rules - Core rule set 115 | { 116 | name: "AWS-AWSManagedRulesCommonRuleSet", 117 | priority: 10, 118 | statement: { 119 | managedRuleGroupStatement: { 120 | name: "AWSManagedRulesCommonRuleSet", 121 | vendorName: "AWS", 122 | excludedRules: [ 123 | // Rules that commonly affect SSE connections 124 | { name: "NoUserAgent_HEADER" }, 125 | { name: "UserAgent_BadBots_HEADER" }, 126 | ], 127 | }, 128 | }, 129 | overrideAction: { none: {} }, 130 | visibilityConfig: { 131 | cloudWatchMetricsEnabled: true, 132 | metricName: "AWS-AWSManagedRulesCommonRuleSet", 133 | sampledRequestsEnabled: true, 134 | }, 135 | }, 136 | // Rate-based rule to prevent DDoS 137 | { 138 | name: "RateLimitRule", 139 | priority: 20, 140 | statement: { 141 | rateBasedStatement: { 142 | limit: 3000, // Increased from 1000 to accommodate SSE connections 143 | aggregateKeyType: "IP", 144 | }, 145 | }, 146 | action: { block: {} }, 147 | visibilityConfig: { 148 | cloudWatchMetricsEnabled: true, 149 | metricName: "RateLimitRule", 150 | sampledRequestsEnabled: true, 151 | }, 152 | }, 153 | ], 154 | }); 155 | 156 | // Store the WebACL ARN and ID 157 | this.webAclArn = this.webAcl.attrArn; 158 | this.webAclId = this.webAcl.ref; 159 | 160 | // Store the CloudFront WAF ARN in local SSM Parameter Store 161 | const paramName = `/mcp/cloudfront-waf-arn-${props.resourceSuffix}`; 162 | new ssm.StringParameter(this, "CloudFrontWafArnParameter", { 163 | parameterName: paramName, 164 | description: "ARN of the CloudFront WAF Web ACL", 165 | stringValue: this.webAclArn, 166 | }); 167 | 168 | // Output the WAF ARN 169 | new cdk.CfnOutput(this, "CloudFrontWafArn", { 170 | value: this.webAclArn, 171 | description: "ARN of the CloudFront WAF Web ACL", 172 | exportName: `CloudFrontWafArn-${props.resourceSuffix}`, 173 | }); 174 | 175 | // Output the param name 176 | new cdk.CfnOutput(this, "CloudFrontWafArnParamName", { 177 | value: paramName, 178 | description: "SSM Parameter name storing the CloudFront WAF ARN", 179 | }); 180 | 181 | // Create a Lambda function to sync the WAF ID to the target region 182 | const crossRegionSyncFunction = new cdk.aws_lambda.Function( 183 | this, 184 | "CrossRegionWafSyncFunction", 185 | { 186 | functionName: `cloudfront-waf-sync-${props.resourceSuffix}`, 187 | runtime: cdk.aws_lambda.Runtime.NODEJS_22_X, 188 | handler: "index.handler", 189 | code: cdk.aws_lambda.Code.fromInline(` 190 | const https = require('https'); 191 | const url = require('url'); 192 | 193 | // AWS SDK v3 is available in the Lambda runtime 194 | const { SSMClient, PutParameterCommand } = require('@aws-sdk/client-ssm'); 195 | 196 | exports.handler = async function(event, context) { 197 | console.log('Event:', JSON.stringify(event, null, 2)); 198 | 199 | try { 200 | // For CREATE and UPDATE events, we need to sync the parameter 201 | if (event.RequestType === 'Create' || event.RequestType === 'Update') { 202 | const props = event.ResourceProperties; 203 | const webAclId = props.WebAclId; 204 | const webAclArn = props.WebAclArn; 205 | const parameterName = props.ParameterName; 206 | const targetRegion = props.TargetRegion; 207 | 208 | console.log('WebACL ID:', webAclId); 209 | console.log('WebACL ARN:', webAclArn); 210 | console.log('Parameter Name:', parameterName); 211 | console.log('Target Region:', targetRegion); 212 | 213 | // Update the SSM parameter in the target region 214 | const ssmClient = new SSMClient({ region: targetRegion }); 215 | const putParameterCommand = new PutParameterCommand({ 216 | Name: parameterName, 217 | Value: webAclArn, // Just store the ARN directly 218 | Type: 'String', 219 | Overwrite: true 220 | }); 221 | 222 | await ssmClient.send(putParameterCommand); 223 | console.log('Parameter updated successfully in region', targetRegion); 224 | } 225 | 226 | // Send success response back to CloudFormation 227 | await sendResponse(event, context, 'SUCCESS', { Message: 'Operation completed successfully' }); 228 | } catch (error) { 229 | console.error('Error:', error); 230 | await sendResponse(event, context, 'FAILED', { Error: error.message }); 231 | } 232 | }; 233 | 234 | // Helper function to send response to CloudFormation 235 | async function sendResponse(event, context, responseStatus, responseData) { 236 | const responseBody = JSON.stringify({ 237 | Status: responseStatus, 238 | Reason: responseStatus === 'FAILED' ? 'See the details in CloudWatch Log Stream: ' + context.logStreamName : 'See the details in CloudWatch Log Stream', 239 | PhysicalResourceId: context.logStreamName, 240 | StackId: event.StackId, 241 | RequestId: event.RequestId, 242 | LogicalResourceId: event.LogicalResourceId, 243 | NoEcho: false, 244 | Data: responseData 245 | }); 246 | 247 | console.log('Response body:', responseBody); 248 | 249 | const parsedUrl = url.parse(event.ResponseURL); 250 | 251 | const options = { 252 | hostname: parsedUrl.hostname, 253 | port: 443, 254 | path: parsedUrl.path, 255 | method: 'PUT', 256 | headers: { 257 | 'Content-Type': '', 258 | 'Content-Length': responseBody.length 259 | } 260 | }; 261 | 262 | return new Promise((resolve, reject) => { 263 | const request = https.request(options, function(response) { 264 | console.log('Status code:', response.statusCode); 265 | resolve(); 266 | }); 267 | 268 | request.on('error', function(error) { 269 | console.log('send response error:', error); 270 | reject(error); 271 | }); 272 | 273 | request.write(responseBody); 274 | request.end(); 275 | }); 276 | } 277 | `), 278 | timeout: cdk.Duration.seconds(30), 279 | } 280 | ); 281 | 282 | // Grant permission to write SSM parameter in the target region 283 | crossRegionSyncFunction.addToRolePolicy( 284 | new cdk.aws_iam.PolicyStatement({ 285 | actions: ["ssm:PutParameter"], 286 | resources: [ 287 | `arn:aws:ssm:${props.targetRegion}:${cdk.Aws.ACCOUNT_ID}:parameter/mcp/cloudfront-waf-*`, 288 | ], 289 | }) 290 | ); 291 | 292 | // Create a custom resource that uses the Lambda function 293 | new cdk.CustomResource(this, "CrossRegionWafSync", { 294 | serviceToken: crossRegionSyncFunction.functionArn, 295 | properties: { 296 | // Adding a timestamp forces the custom resource to run on each deployment 297 | Timestamp: Date.now().toString(), 298 | WebAclId: this.webAclId, 299 | WebAclArn: this.webAclArn, 300 | ParameterName: `/mcp/cloudfront-waf-arn-${props.resourceSuffix}`, 301 | TargetRegion: props.targetRegion, 302 | }, 303 | }); 304 | 305 | NagSuppressions.addResourceSuppressions( 306 | crossRegionSyncFunction, 307 | [ 308 | { 309 | id: "AwsSolutions-IAM4", 310 | reason: 311 | "Lambda function used by CloudFront WAF cross-region sync custom resource requires CloudWatch logs access", 312 | appliesTo: [ 313 | "Policy::arn::iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", 314 | ], 315 | }, 316 | { 317 | id: "AwsSolutions-IAM5", 318 | reason: 319 | "Lambda function used by CloudFront WAF cross-region sync custom resource requires access to SSM parameters using consistent prefix for WAF ARN storage", 320 | appliesTo: [ 321 | `Resource::arn:aws:ssm:${props.targetRegion}::parameter/mcp/cloudfront-waf-*`, 322 | ], 323 | }, 324 | ], 325 | true // Apply to child constructs 326 | ); 327 | } 328 | } 329 | -------------------------------------------------------------------------------- /source/cdk/ecs-and-lambda/servers/sample-ecs-weather-streamablehttp-stateless-nodejs-express/src/index.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; 3 | import { validateCognitoToken } from "./oauth-cognito.js"; 4 | import { z } from "zod"; 5 | import express, { Request, Response, NextFunction } from "express"; 6 | 7 | const NWS_API_BASE = "https://api.weather.gov"; 8 | const USER_AGENT = "weather-app/1.0"; 9 | 10 | const getServer = () => { 11 | // Create server instance 12 | const server = new McpServer({ 13 | name: "weather", 14 | version: "1.0.0", 15 | capabilities: { 16 | resources: {}, 17 | tools: {}, 18 | }, 19 | }); 20 | 21 | // Helper function for making NWS API requests 22 | async function makeNWSRequest(url: string): Promise { 23 | const headers = { 24 | "User-Agent": USER_AGENT, 25 | Accept: "application/geo+json", 26 | }; 27 | 28 | try { 29 | const response = await fetch(url, { headers }); 30 | if (!response.ok) { 31 | throw new Error(`HTTP error! status: ${response.status}`); 32 | } 33 | return (await response.json()) as T; 34 | } catch (error) { 35 | console.error("Error making NWS request:", error); 36 | return null; 37 | } 38 | } 39 | 40 | interface AlertFeature { 41 | properties: { 42 | event?: string; 43 | areaDesc?: string; 44 | severity?: string; 45 | status?: string; 46 | headline?: string; 47 | }; 48 | } 49 | 50 | // Format alert data 51 | function formatAlert(feature: AlertFeature): string { 52 | const props = feature.properties; 53 | return [ 54 | `Event: ${props.event || "Unknown"}`, 55 | `Area: ${props.areaDesc || "Unknown"}`, 56 | `Severity: ${props.severity || "Unknown"}`, 57 | `Status: ${props.status || "Unknown"}`, 58 | `Headline: ${props.headline || "No headline"}`, 59 | "---", 60 | ].join("\n"); 61 | } 62 | 63 | interface ForecastPeriod { 64 | name?: string; 65 | temperature?: number; 66 | temperatureUnit?: string; 67 | windSpeed?: string; 68 | windDirection?: string; 69 | shortForecast?: string; 70 | } 71 | 72 | interface AlertsResponse { 73 | features: AlertFeature[]; 74 | } 75 | 76 | interface PointsResponse { 77 | properties: { 78 | forecast?: string; 79 | }; 80 | } 81 | 82 | interface ForecastResponse { 83 | properties: { 84 | periods: ForecastPeriod[]; 85 | }; 86 | } 87 | 88 | // Register weather tools 89 | server.tool( 90 | "get_alerts", 91 | "Get weather alerts for a state", 92 | { 93 | state: z 94 | .string() 95 | .length(2) 96 | .describe("Two-letter state code (e.g. CA, NY)"), 97 | }, 98 | async ({ state }) => { 99 | const stateCode = state.toUpperCase(); 100 | const alertsUrl = `${NWS_API_BASE}/alerts?area=${stateCode}`; 101 | const alertsData = await makeNWSRequest(alertsUrl); 102 | 103 | if (!alertsData) { 104 | return { 105 | content: [ 106 | { 107 | type: "text", 108 | text: "Failed to retrieve alerts data", 109 | }, 110 | ], 111 | }; 112 | } 113 | 114 | const features = alertsData.features || []; 115 | if (features.length === 0) { 116 | return { 117 | content: [ 118 | { 119 | type: "text", 120 | text: `No active alerts for ${stateCode}`, 121 | }, 122 | ], 123 | }; 124 | } 125 | 126 | const formattedAlerts = features.map(formatAlert); 127 | const alertsText = `Active alerts for ${stateCode}:\n\n${formattedAlerts.join( 128 | "\n" 129 | )}`; 130 | 131 | return { 132 | content: [ 133 | { 134 | type: "text", 135 | text: alertsText, 136 | }, 137 | ], 138 | }; 139 | } 140 | ); 141 | 142 | server.tool( 143 | "get_forecast", 144 | "Get weather forecast for a location", 145 | { 146 | latitude: z 147 | .number() 148 | .min(-90) 149 | .max(90) 150 | .describe("Latitude of the location"), 151 | longitude: z 152 | .number() 153 | .min(-180) 154 | .max(180) 155 | .describe("Longitude of the location"), 156 | }, 157 | async ({ latitude, longitude }) => { 158 | // Get grid point data 159 | const pointsUrl = `${NWS_API_BASE}/points/${latitude.toFixed( 160 | 4 161 | )},${longitude.toFixed(4)}`; 162 | const pointsData = await makeNWSRequest(pointsUrl); 163 | 164 | if (!pointsData) { 165 | return { 166 | content: [ 167 | { 168 | type: "text", 169 | text: `Failed to retrieve grid point data for coordinates: ${latitude}, ${longitude}. This location may not be supported by the NWS API (only US locations are supported).`, 170 | }, 171 | ], 172 | }; 173 | } 174 | 175 | const forecastUrl = pointsData.properties?.forecast; 176 | if (!forecastUrl) { 177 | return { 178 | content: [ 179 | { 180 | type: "text", 181 | text: "Failed to get forecast URL from grid point data", 182 | }, 183 | ], 184 | }; 185 | } 186 | 187 | // Get forecast data 188 | const forecastData = await makeNWSRequest(forecastUrl); 189 | if (!forecastData) { 190 | return { 191 | content: [ 192 | { 193 | type: "text", 194 | text: "Failed to retrieve forecast data", 195 | }, 196 | ], 197 | }; 198 | } 199 | 200 | const periods = forecastData.properties?.periods || []; 201 | if (periods.length === 0) { 202 | return { 203 | content: [ 204 | { 205 | type: "text", 206 | text: "No forecast periods available", 207 | }, 208 | ], 209 | }; 210 | } 211 | 212 | // Format forecast periods 213 | const formattedForecast = periods.map((period: ForecastPeriod) => 214 | [ 215 | `${period.name || "Unknown"}:`, 216 | `Temperature: ${period.temperature || "Unknown"}°${ 217 | period.temperatureUnit || "F" 218 | }`, 219 | `Wind: ${period.windSpeed || "Unknown"} ${ 220 | period.windDirection || "" 221 | }`, 222 | `${period.shortForecast || "No forecast available"}`, 223 | "---", 224 | ].join("\n") 225 | ); 226 | 227 | const forecastText = `Forecast for ${latitude}, ${longitude}:\n\n${formattedForecast.join( 228 | "\n" 229 | )}`; 230 | 231 | return { 232 | content: [ 233 | { 234 | type: "text", 235 | text: forecastText, 236 | }, 237 | ], 238 | }; 239 | } 240 | ); 241 | 242 | return server; 243 | }; 244 | 245 | const app = express(); 246 | app.use(express.json()); 247 | 248 | /** 249 | * Get WWW-Authenticate header for 401 responses. 250 | */ 251 | function getWWWAuthenticateHeader(req: Request): string { 252 | const protocol = req.get("X-Forwarded-Proto") || req.protocol; 253 | const baseUrl = process.env.BASE_URL || `${protocol}://${req.get("host")}`; 254 | const val = `Bearer realm="mcp-server", resource_metadata="${baseUrl}/weather-nodejs/.well-known/oauth-protected-resource"`; 255 | console.log(val); 256 | return val; 257 | } 258 | 259 | /** 260 | * Send a 401 Unauthorized response with the appropriate WWW-Authenticate header. 261 | */ 262 | function sendUnauthorizedResponse(req: Request, res: Response): void { 263 | res.setHeader("WWW-Authenticate", getWWWAuthenticateHeader(req)); 264 | res.status(401).json({ 265 | jsonrpc: "2.0", 266 | error: { 267 | code: -32600, 268 | message: "Unauthorized. Valid authentication credentials required.", 269 | }, 270 | id: null, 271 | }); 272 | } 273 | 274 | /** 275 | * Middleware to authenticate requests using Cognito tokens. 276 | */ 277 | const authMiddleware = async ( 278 | req: Request, 279 | res: Response, 280 | next: NextFunction 281 | ) => { 282 | const authHeader = req.headers.authorization; 283 | 284 | if (!authHeader || !authHeader.startsWith("Bearer ")) { 285 | return sendUnauthorizedResponse(req, res); 286 | } 287 | 288 | const token = authHeader.substring(7); // Remove 'Bearer ' prefix 289 | 290 | // Check if token is actually present after "Bearer " 291 | if (!token || token.trim() === "") { 292 | return sendUnauthorizedResponse(req, res); 293 | } 294 | 295 | try { 296 | const { isValid } = await validateCognitoToken(token); 297 | if (!isValid) { 298 | return sendUnauthorizedResponse(req, res); 299 | } 300 | } catch (error) { 301 | console.error("Token validation error:", error); 302 | return sendUnauthorizedResponse(req, res); 303 | } 304 | 305 | next(); 306 | }; 307 | 308 | /** 309 | * OAuth 2.0 Protected Resource Metadata endpoint. 310 | * Implements RFC9728 specification. 311 | */ 312 | app.get( 313 | "/weather-nodejs/.well-known/oauth-protected-resource", 314 | (req: Request, res: Response) => { 315 | const region = process.env.AWS_REGION || "us-west-2"; 316 | const user_pool_id = process.env.COGNITO_USER_POOL_ID; 317 | const protocol = req.get("X-Forwarded-Proto") || req.protocol; 318 | const baseUrl = process.env.BASE_URL || `${protocol}://${req.get("host")}`; 319 | 320 | res.json({ 321 | resource: `${baseUrl}/weather-nodejs/mcp`, 322 | authorization_servers: [ 323 | `https://cognito-idp.${region}.amazonaws.com/${user_pool_id}`, 324 | ], 325 | bearer_methods_supported: ["header"], 326 | scopes_supported: ["openid", "email", "profile"], // adjust as needed 327 | }); 328 | } 329 | ); 330 | 331 | // Health check 332 | app.get("/weather-nodejs/", async (req: Request, res: Response) => { 333 | res.status(200).json({ 334 | status: "healthy", 335 | service: "weather-nodejs", 336 | }); 337 | }); 338 | 339 | // Apply authentication middleware to MCP endpoints 340 | app.use("/weather-nodejs/mcp", authMiddleware); 341 | 342 | app.post("/weather-nodejs/mcp", async (req: Request, res: Response) => { 343 | // In stateless mode, create a new instance of transport and server for each request 344 | // to ensure complete isolation. A single instance would cause request ID collisions 345 | // when multiple clients connect concurrently. 346 | 347 | try { 348 | const server = getServer(); 349 | const transport: StreamableHTTPServerTransport = 350 | new StreamableHTTPServerTransport({ 351 | sessionIdGenerator: undefined, 352 | }); 353 | res.on("close", () => { 354 | console.log("Request closed"); 355 | transport.close(); 356 | server.close(); 357 | }); 358 | await server.connect(transport); 359 | await transport.handleRequest(req, res, req.body); 360 | } catch (error) { 361 | console.error("Error handling MCP request:", error); 362 | if (!res.headersSent) { 363 | res.status(500).json({ 364 | jsonrpc: "2.0", 365 | error: { 366 | code: -32603, 367 | message: "Internal server error", 368 | }, 369 | id: null, 370 | }); 371 | } 372 | } 373 | }); 374 | 375 | // SSE notifications not supported in stateless mode 376 | app.get("/weather-nodejs/mcp", async (req: Request, res: Response) => { 377 | console.log("Received GET MCP request"); 378 | res.writeHead(405).end( 379 | JSON.stringify({ 380 | jsonrpc: "2.0", 381 | error: { 382 | code: -32000, 383 | message: "Method not allowed.", 384 | }, 385 | id: null, 386 | }) 387 | ); 388 | }); 389 | 390 | // Session termination not needed in stateless mode 391 | app.delete("/weather-nodejs/mcp", async (req: Request, res: Response) => { 392 | console.log("Received DELETE MCP request"); 393 | res.writeHead(405).end( 394 | JSON.stringify({ 395 | jsonrpc: "2.0", 396 | error: { 397 | code: -32000, 398 | message: "Method not allowed.", 399 | }, 400 | id: null, 401 | }) 402 | ); 403 | }); 404 | 405 | // Start the server 406 | const PORT = process.env.PORT || 3001; 407 | app.listen(PORT); 408 | -------------------------------------------------------------------------------- /source/cdk/ecs-and-lambda/servers/sample-lambda-weather-streamablehttp-stateless-nodejs-express/src/index.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; 3 | import { validateCognitoToken } from "./oauth-cognito.js"; 4 | import { z } from "zod"; 5 | import express, { Request, Response, NextFunction } from "express"; 6 | 7 | const NWS_API_BASE = "https://api.weather.gov"; 8 | const USER_AGENT = "weather-app/1.0"; 9 | 10 | const getServer = () => { 11 | // Create server instance 12 | const server = new McpServer({ 13 | name: "weather", 14 | version: "1.0.0", 15 | capabilities: { 16 | resources: {}, 17 | tools: {}, 18 | }, 19 | }); 20 | 21 | // Helper function for making NWS API requests 22 | async function makeNWSRequest(url: string): Promise { 23 | const headers = { 24 | "User-Agent": USER_AGENT, 25 | Accept: "application/geo+json", 26 | }; 27 | 28 | try { 29 | const response = await fetch(url, { headers }); 30 | if (!response.ok) { 31 | throw new Error(`HTTP error! status: ${response.status}`); 32 | } 33 | return (await response.json()) as T; 34 | } catch (error) { 35 | console.error("Error making NWS request:", error); 36 | return null; 37 | } 38 | } 39 | 40 | interface AlertFeature { 41 | properties: { 42 | event?: string; 43 | areaDesc?: string; 44 | severity?: string; 45 | status?: string; 46 | headline?: string; 47 | }; 48 | } 49 | 50 | // Format alert data 51 | function formatAlert(feature: AlertFeature): string { 52 | const props = feature.properties; 53 | return [ 54 | `Event: ${props.event || "Unknown"}`, 55 | `Area: ${props.areaDesc || "Unknown"}`, 56 | `Severity: ${props.severity || "Unknown"}`, 57 | `Status: ${props.status || "Unknown"}`, 58 | `Headline: ${props.headline || "No headline"}`, 59 | "---", 60 | ].join("\n"); 61 | } 62 | 63 | interface ForecastPeriod { 64 | name?: string; 65 | temperature?: number; 66 | temperatureUnit?: string; 67 | windSpeed?: string; 68 | windDirection?: string; 69 | shortForecast?: string; 70 | } 71 | 72 | interface AlertsResponse { 73 | features: AlertFeature[]; 74 | } 75 | 76 | interface PointsResponse { 77 | properties: { 78 | forecast?: string; 79 | }; 80 | } 81 | 82 | interface ForecastResponse { 83 | properties: { 84 | periods: ForecastPeriod[]; 85 | }; 86 | } 87 | 88 | // Register weather tools 89 | server.tool( 90 | "get_alerts", 91 | "Get weather alerts for a state", 92 | { 93 | state: z 94 | .string() 95 | .length(2) 96 | .describe("Two-letter state code (e.g. CA, NY)"), 97 | }, 98 | async ({ state }) => { 99 | const stateCode = state.toUpperCase(); 100 | const alertsUrl = `${NWS_API_BASE}/alerts?area=${stateCode}`; 101 | const alertsData = await makeNWSRequest(alertsUrl); 102 | 103 | if (!alertsData) { 104 | return { 105 | content: [ 106 | { 107 | type: "text", 108 | text: "Failed to retrieve alerts data", 109 | }, 110 | ], 111 | }; 112 | } 113 | 114 | const features = alertsData.features || []; 115 | if (features.length === 0) { 116 | return { 117 | content: [ 118 | { 119 | type: "text", 120 | text: `No active alerts for ${stateCode}`, 121 | }, 122 | ], 123 | }; 124 | } 125 | 126 | const formattedAlerts = features.map(formatAlert); 127 | const alertsText = `Active alerts for ${stateCode}:\n\n${formattedAlerts.join( 128 | "\n" 129 | )}`; 130 | 131 | return { 132 | content: [ 133 | { 134 | type: "text", 135 | text: alertsText, 136 | }, 137 | ], 138 | }; 139 | } 140 | ); 141 | 142 | server.tool( 143 | "get_forecast", 144 | "Get weather forecast for a location", 145 | { 146 | latitude: z 147 | .number() 148 | .min(-90) 149 | .max(90) 150 | .describe("Latitude of the location"), 151 | longitude: z 152 | .number() 153 | .min(-180) 154 | .max(180) 155 | .describe("Longitude of the location"), 156 | }, 157 | async ({ latitude, longitude }) => { 158 | // Get grid point data 159 | const pointsUrl = `${NWS_API_BASE}/points/${latitude.toFixed( 160 | 4 161 | )},${longitude.toFixed(4)}`; 162 | const pointsData = await makeNWSRequest(pointsUrl); 163 | 164 | if (!pointsData) { 165 | return { 166 | content: [ 167 | { 168 | type: "text", 169 | text: `Failed to retrieve grid point data for coordinates: ${latitude}, ${longitude}. This location may not be supported by the NWS API (only US locations are supported).`, 170 | }, 171 | ], 172 | }; 173 | } 174 | 175 | const forecastUrl = pointsData.properties?.forecast; 176 | if (!forecastUrl) { 177 | return { 178 | content: [ 179 | { 180 | type: "text", 181 | text: "Failed to get forecast URL from grid point data", 182 | }, 183 | ], 184 | }; 185 | } 186 | 187 | // Get forecast data 188 | const forecastData = await makeNWSRequest(forecastUrl); 189 | if (!forecastData) { 190 | return { 191 | content: [ 192 | { 193 | type: "text", 194 | text: "Failed to retrieve forecast data", 195 | }, 196 | ], 197 | }; 198 | } 199 | 200 | const periods = forecastData.properties?.periods || []; 201 | if (periods.length === 0) { 202 | return { 203 | content: [ 204 | { 205 | type: "text", 206 | text: "No forecast periods available", 207 | }, 208 | ], 209 | }; 210 | } 211 | 212 | // Format forecast periods 213 | const formattedForecast = periods.map((period: ForecastPeriod) => 214 | [ 215 | `${period.name || "Unknown"}:`, 216 | `Temperature: ${period.temperature || "Unknown"}°${ 217 | period.temperatureUnit || "F" 218 | }`, 219 | `Wind: ${period.windSpeed || "Unknown"} ${ 220 | period.windDirection || "" 221 | }`, 222 | `${period.shortForecast || "No forecast available"}`, 223 | "---", 224 | ].join("\n") 225 | ); 226 | 227 | const forecastText = `Forecast for ${latitude}, ${longitude}:\n\n${formattedForecast.join( 228 | "\n" 229 | )}`; 230 | 231 | return { 232 | content: [ 233 | { 234 | type: "text", 235 | text: forecastText, 236 | }, 237 | ], 238 | }; 239 | } 240 | ); 241 | 242 | return server; 243 | }; 244 | 245 | const app = express(); 246 | app.use(express.json()); 247 | 248 | /** 249 | * Get WWW-Authenticate header for 401 responses. 250 | */ 251 | function getWWWAuthenticateHeader(req: Request): string { 252 | // Check X-Forwarded-Proto from ALB/CloudFront, fallback to req.protocol for local testing 253 | const protocol = req.get("X-Forwarded-Proto") || req.protocol; 254 | const baseUrl = process.env.BASE_URL || `${protocol}://${req.get("host")}`; 255 | const val = `Bearer realm="mcp-server", resource_metadata="${baseUrl}/weather-nodejs-lambda/.well-known/oauth-protected-resource"`; 256 | console.log(val); 257 | return val; 258 | } 259 | 260 | /** 261 | * Send a 401 Unauthorized response with the appropriate WWW-Authenticate header. 262 | */ 263 | function sendUnauthorizedResponse(req: Request, res: Response): void { 264 | res.setHeader("WWW-Authenticate", getWWWAuthenticateHeader(req)); 265 | res.status(401).json({ 266 | jsonrpc: "2.0", 267 | error: { 268 | code: -32600, 269 | message: "Unauthorized. Valid authentication credentials required.", 270 | }, 271 | id: null, 272 | }); 273 | } 274 | 275 | /** 276 | * Middleware to authenticate requests using Cognito tokens. 277 | */ 278 | const authMiddleware = async ( 279 | req: Request, 280 | res: Response, 281 | next: NextFunction 282 | ) => { 283 | const authHeader = req.headers.authorization; 284 | 285 | if (!authHeader || !authHeader.startsWith("Bearer ")) { 286 | return sendUnauthorizedResponse(req, res); 287 | } 288 | 289 | const token = authHeader.substring(7); // Remove 'Bearer ' prefix 290 | 291 | // Check if token is actually present after "Bearer " 292 | if (!token || token.trim() === "") { 293 | return sendUnauthorizedResponse(req, res); 294 | } 295 | 296 | try { 297 | const { isValid } = await validateCognitoToken(token); 298 | if (!isValid) { 299 | return sendUnauthorizedResponse(req, res); 300 | } 301 | } catch (error) { 302 | console.error("Token validation error:", error); 303 | return sendUnauthorizedResponse(req, res); 304 | } 305 | 306 | next(); 307 | }; 308 | 309 | /** 310 | * OAuth 2.0 Protected Resource Metadata endpoint. 311 | * Implements RFC9728 specification. 312 | */ 313 | app.get( 314 | "/weather-nodejs-lambda/.well-known/oauth-protected-resource", 315 | (req: Request, res: Response) => { 316 | const region = process.env.AWS_REGION || "us-west-2"; 317 | const user_pool_id = process.env.COGNITO_USER_POOL_ID; 318 | const baseUrl = process.env.BASE_URL || `https://${req.get("host")}`; 319 | 320 | res.json({ 321 | resource: `${baseUrl}/weather-nodejs-lambda/mcp`, 322 | authorization_servers: [ 323 | `https://cognito-idp.${region}.amazonaws.com/${user_pool_id}`, 324 | ], 325 | bearer_methods_supported: ["header"], 326 | scopes_supported: ["openid", "email", "profile"], // adjust as needed 327 | }); 328 | } 329 | ); 330 | 331 | // Health check 332 | app.get("/weather-nodejs-lambda/", async (req: Request, res: Response) => { 333 | res.status(200).json({ 334 | status: "healthy", 335 | service: "weather-nodejs-lambda", 336 | }); 337 | }); 338 | 339 | // Apply authentication middleware to MCP endpoints 340 | app.use("/weather-nodejs-lambda/mcp", authMiddleware); 341 | 342 | app.post("/weather-nodejs-lambda/mcp", async (req: Request, res: Response) => { 343 | // In stateless mode, create a new instance of transport and server for each request 344 | // to ensure complete isolation. A single instance would cause request ID collisions 345 | // when multiple clients connect concurrently. 346 | 347 | try { 348 | const server = getServer(); 349 | const transport: StreamableHTTPServerTransport = 350 | new StreamableHTTPServerTransport({ 351 | sessionIdGenerator: undefined, 352 | }); 353 | res.on("close", () => { 354 | console.log("Request closed"); 355 | transport.close(); 356 | server.close(); 357 | }); 358 | await server.connect(transport); 359 | await transport.handleRequest(req, res, req.body); 360 | } catch (error) { 361 | console.error("Error handling MCP request:", error); 362 | if (!res.headersSent) { 363 | res.status(500).json({ 364 | jsonrpc: "2.0", 365 | error: { 366 | code: -32603, 367 | message: "Internal server error", 368 | }, 369 | id: null, 370 | }); 371 | } 372 | } 373 | }); 374 | 375 | // SSE notifications not supported in stateless mode 376 | app.get("/weather-nodejs-lambda/mcp", async (req: Request, res: Response) => { 377 | console.log("Received GET MCP request"); 378 | res.writeHead(405).end( 379 | JSON.stringify({ 380 | jsonrpc: "2.0", 381 | error: { 382 | code: -32000, 383 | message: "Method not allowed.", 384 | }, 385 | id: null, 386 | }) 387 | ); 388 | }); 389 | 390 | // Session termination not needed in stateless mode 391 | app.delete( 392 | "/weather-nodejs-lambda/mcp", 393 | async (req: Request, res: Response) => { 394 | console.log("Received DELETE MCP request"); 395 | res.writeHead(405).end( 396 | JSON.stringify({ 397 | jsonrpc: "2.0", 398 | error: { 399 | code: -32000, 400 | message: "Method not allowed.", 401 | }, 402 | id: null, 403 | }) 404 | ); 405 | } 406 | ); 407 | 408 | // Start the server 409 | const PORT = process.env.PORT || 3001; 410 | app.listen(PORT); 411 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Guidance for Deploying Model Context Protocol Servers on AWS 2 | 3 | ## Table of Contents 4 | 5 | 1. [Overview](#overview) 6 | - [Cost](#cost) 7 | 2. [Prerequisites](#prerequisites) 8 | - [Operating System](#operating-system) 9 | 3. [Deployment Steps](#deployment-steps) 10 | 4. [Deployment Validation](#deployment-validation) 11 | 5. [Running the Guidance](#running-the-guidance) 12 | 6. [Next Steps](#next-steps) 13 | 7. [Cleanup](#cleanup) 14 | 8. [FAQ, known issues, additional considerations, and limitations](#faq-known-issues-additional-considerations-and-limitations) 15 | 9. [Revisions](#revisions) 16 | 10. [Notices](#notices) 17 | 18 | ## Overview 19 | 20 | This guidance demonstrates how to deploy Model Context Protocol (MCP) servers on AWS with secure authentication using Amazon Cognito, implementing the **2025-06-18 MCP specification** with OAuth 2.0 Protected Resource Metadata (RFC9728). It enables you to host MCP servers that can be accessed remotely while maintaining security through standards-compliant OAuth 2.0 authentication flows. 21 | 22 | The solution addresses several key challenges: 23 | 24 | - Secure hosting of MCP servers on AWS infrastructure 25 | - Standards-compliant authentication using OAuth 2.0 Protected Resource Metadata (RFC9728) 26 | - Remote access to MCP servers through secure StreamableHTTP transport 27 | - Stateless server architecture for concurrent client support 28 | - Scalable and maintainable deployment using AWS CDK 29 | 30 | ### Architecture 31 | 32 | ![Architecture Diagram](assets/architecture-diagram.png) 33 | 34 | The architecture implements: 35 | 36 | 1. **CloudFront distribution** for global content delivery with WAF protection 37 | 2. **Application Load Balancer** for traffic distribution and SSL termination 38 | 3. **ECS Fargate and Lambda** for containerized and serverless MCP servers 39 | 4. **AWS Cognito** for OAuth 2.0 authorization server functionality 40 | 5. **OAuth 2.0 Protected Resource Metadata** endpoints for standards-compliant authentication 41 | 6. **StreamableHTTP transport** with stateless request handling 42 | 7. **Four-stack CDK deployment**: VPC, Security, CloudFront WAF, and MCP Server stacks 43 | 44 | ### Cost 45 | 46 | You are responsible for the cost of the AWS services used while running this Guidance. As of August 2025, the cost for running this Guidance with the default settings in the US East (N. Virginia) Region is approximately $194.18 per month for processing moderate traffic levels. 47 | 48 | We recommend creating a [Budget](https://docs.aws.amazon.com/cost-management/latest/userguide/budgets-managing-costs.html) through [AWS Cost Explorer](https://aws.amazon.com/aws-cost-management/aws-cost-explorer/) to help manage costs. Prices are subject to change. For full details, refer to the pricing webpage for each AWS service used in this Guidance. 49 | 50 | #### Estimated Cost Table 51 | 52 | The following table provides a sample cost breakdown for deploying this Guidance with the default parameters in the US East (N. Virginia) Region for one month. 53 | 54 | | AWS service | Dimensions | Cost [USD] | 55 | | ---------------------- | -------------------------------------------------- | ----------------- | 56 | | VPC (NAT Gateway) | 1 NAT Gateway × 730 hours + 100 GB data processing | $37.35 | 57 | | Elastic Load Balancing | Application Load Balancer with moderate traffic | $16.83 | 58 | | Amazon Cognito | 10,500 MAUs (within 50,000 free tier) | $0.00 | 59 | | CloudFront | 2 TB data transfer + 15M requests | $87.96 | 60 | | WAF | 2 Web ACLs (CloudFront and Regional) | $10.00 | 61 | | ECS (Fargate) | 1 vCPU, 2GB memory × 730 hours | $36.04 | 62 | | Secrets Manager | 1 secret for Cognito credentials | $0.40 | 63 | | Lambda | Custom resources (minimal usage) | $0.20 | 64 | | **Total** | | **$194.18/month** | 65 | 66 | ## Prerequisites 67 | 68 | ### Operating System 69 | 70 | These deployment instructions are optimized to work on **Amazon Linux 2 AMI**. Deployment in another OS may require additional steps. 71 | 72 | ### Required Tools 73 | 74 | 1. [AWS CLI](https://aws.amazon.com/cli/) installed and configured 75 | 2. [Node.js](https://nodejs.org/) v14 or later 76 | 3. [AWS CDK](https://aws.amazon.com/cdk/) installed: 77 | ```bash 78 | npm install -g aws-cdk 79 | ``` 80 | 81 | ### AWS CDK Bootstrap 82 | 83 | If you're using AWS CDK for the first time, bootstrap your account: 84 | 85 | ```bash 86 | cdk bootstrap 87 | ``` 88 | 89 | ## Deployment Steps 90 | 91 | 1. Clone the repository: 92 | 93 | ```bash 94 | git clone 95 | cd guidance-for-deploying-model-context-protocol-servers-on-aws 96 | cd source/cdk/ecs-and-lambda 97 | ``` 98 | 99 | 2. Install dependencies: 100 | 101 | ```bash 102 | npm install 103 | ``` 104 | 105 | 3. Login to public ECR: 106 | 107 | ```bash 108 | aws ecr-public get-login-password --region us-east-1 | docker login --username AWS --password-stdin public.ecr.aws 109 | ``` 110 | 111 | 4. Deploy the stacks: 112 | 113 | Without domain configuration: 114 | 115 | ```bash 116 | cdk deploy --all 117 | ``` 118 | 119 | Or with domain configuration (single region - us-east-1): 120 | 121 | ```bash 122 | cdk deploy --all --context cdnCertificateArn=arn:aws:acm:us-east-1:123456789012:certificate/abc123 --context albCertificateArn=arn:aws:acm:us-east-1:123456789012:certificate/abc123 --context customDomain=mcp-server.example.com 123 | ``` 124 | 125 | Or with multi-region certificate configuration: 126 | 127 | ```bash 128 | cdk deploy --all --context cdnCertificateArn=arn:aws:acm:us-east-1:123456789012:certificate/abc123 --context albCertificateArn=arn:aws:acm:eu-west-1:123456789012:certificate/def456 --context customDomain=mcp-server.example.com 129 | ``` 130 | 131 | Or with CloudFront HTTPS only (ALB stays HTTP): 132 | 133 | ```bash 134 | cdk deploy --all --context cdnCertificateArn=arn:aws:acm:us-east-1:123456789012:certificate/abc123 --context customDomain=mcp-server.example.com 135 | ``` 136 | 137 | 5. Update MCP servers: 138 | 139 | Without domain configuration: 140 | 141 | ```bash 142 | cdk deploy MCP-Server 143 | ``` 144 | 145 | Or with domain configuration: 146 | 147 | ```bash 148 | cdk deploy MCP-Server --context cdnCertificateArn=arn:aws:acm:us-east-1:123456789012:certificate/abc123 --context albCertificateArn=arn:aws:acm:us-east-1:123456789012:certificate/abc123 --context customDomain=mcp-server.example.com 149 | ``` 150 | 151 | ## Deployment Validation 152 | 153 | 1. Verify CloudFormation stack status: 154 | 155 | - Open AWS CloudFormation console 156 | - Check that all stacks show "CREATE_COMPLETE" 157 | 158 | 2. Validate Cognito setup: 159 | 160 | - Open Amazon Cognito console 161 | - Verify User Pool creation 162 | - Confirm App Client configuration 163 | 164 | 3. Verify infrastructure: 165 | - CloudFront distribution is "Deployed" 166 | - Application Load Balancer is "Active" 167 | - ECS services are running 168 | 169 | ## Running the Guidance 170 | 171 | ### Testing with Cognito Users (Development Only) 172 | 173 | For development and testing environments only, you can quickly create and manage users with AWS CLI: 174 | 175 | ```bash 176 | # Create test user 177 | aws cognito-idp admin-create-user --user-pool-id YOUR_USER_POOL_ID --username test@example.com 178 | 179 | # Set permanent password (bypass temporary) 180 | aws cognito-idp admin-set-user-password --user-pool-id YOUR_USER_POOL_ID --username test@example.com --password "TestPass123!" --permanent 181 | ``` 182 | 183 | ### Testing with the Sample Python MCP Client 184 | 185 | The deployment includes a sample Python MCP client that demonstrates OAuth 2.0 Protected Resource authentication with the deployed servers. This client implements the 2025-06-18 MCP specification with StreamableHTTP transport. 186 | 187 | > **Note:** This client is a modified version of the [simple-auth-client example](https://github.com/modelcontextprotocol/python-sdk/tree/main/examples/clients/simple-auth-client) from the official MCP Python SDK. 188 | 189 | ### Why Use the Python Client? 190 | 191 | The included Python client (`source/sample-clients/simple-auth-client-python/`) demonstrates: 192 | 193 | - **OAuth 2.0 Protected Resource Metadata** (RFC9728) authentication flow 194 | - **StreamableHTTP transport** communication 195 | - **Interactive CLI interface** for testing MCP tools 196 | - **Standards-compliant implementation** of the 2025-06-18 MCP specification 197 | 198 | > **Important:** This implementation does **not support Dynamic Client Registration (DCR)**. Client credentials must be pre-configured in AWS Cognito and provided via environment variables. 199 | 200 | ### Using the Python Client 201 | 202 | 1. Navigate to the client directory: 203 | 204 | ```bash 205 | cd source/sample-clients/simple-auth-client-python 206 | ``` 207 | 208 | 2. Install dependencies with uv: 209 | 210 | ```bash 211 | pip install uv 212 | uv sync --reinstall 213 | ``` 214 | 215 | 3. Set environment variables: 216 | 217 | ```bash 218 | export MCP_SERVER_URL="https:///weather-nodejs/mcp" 219 | export OAUTH_CLIENT_ID="" 220 | export OAUTH_CLIENT_SECRET="" 221 | ``` 222 | 223 | 4. Run the client: 224 | 225 | ```bash 226 | uv run python -m mcp_simple_auth_client.main 227 | ``` 228 | 229 | 5. Test available endpoints: 230 | 231 | - **ECS Fargate Server**: `https:///weather-nodejs/mcp` 232 | - **Lambda Server**: `https:///weather-nodejs-lambda/mcp` 233 | 234 | The client will automatically handle the OAuth flow, open a browser for authentication, and provide an interactive CLI to test the MCP tools. 235 | 236 | ## Next Steps 237 | 238 | 1. Implement additional MCP servers: 239 | 240 | - Add new server containers to ECS 241 | - Configure OAuth flows for new servers 242 | - Update client configurations for new endpoints 243 | 244 | 2. Optimize costs: 245 | 246 | - Monitor usage patterns 247 | - Consider reserved capacity for steady workloads 248 | - Implement caching strategies 249 | 250 | 3. Enhance security: 251 | - Enable MFA in Cognito 252 | - Implement additional WAF rules 253 | - Set up monitoring and alerting 254 | 255 | ## Cleanup 256 | 257 | 1. Remove deployed resources: 258 | 259 | ```bash 260 | cdk destroy --all 261 | ``` 262 | 263 | 2. Manual cleanup steps: 264 | - Empty any created S3 buckets 265 | - Delete Cognito User Pool (if not needed) 266 | - Remove CloudWatch log groups 267 | - Delete any created secrets in Secrets Manager 268 | 269 | ## FAQ, known issues, additional considerations, and limitations 270 | 271 | ### Known Issues 272 | 273 | 1. Token refresh may require re-authentication in some cases 274 | 2. CloudFront cache invalidation may take up to 5 minutes 275 | 3. Initial cold start delay for Fargate containers 276 | 277 | ### Additional Considerations 278 | 279 | - **OAuth 2.0 Compliant**: Implements RFC9728 Protected Resource Metadata specification 280 | - **Stateless Architecture**: Each request creates new server instance for concurrent client support 281 | - **Public endpoints are created** for OAuth Protected Resource Metadata discovery 282 | - **CloudFront distributions** may take 15-20 minutes to deploy 283 | - **Four-stack deployment**: VPC, Security, CloudFront WAF, and MCP Server stacks 284 | 285 | For detailed information, refer to these additional documentation files: 286 | 287 | - [Monthly Cost Estimate Report](assets/cost-estimate-report.md) 288 | - [AWS Architecture & OAuth 2.0 Flow](assets/aws-cognito-mcp-integration.md) 289 | 290 | ### Limitations 291 | 292 | 1. **No Dynamic Client Registration (DCR)**: Client credentials must be pre-configured in AWS Cognito 293 | 2. **Region availability** depends on AWS Cognito support 294 | 3. **Multi-region certificate requirements**: 295 | - CloudFront certificates (`cdnCertificateArn`) must be in us-east-1 296 | - ALB certificates (`albCertificateArn`) must be in the deployment region 297 | - Both certificates must cover the same custom domain 298 | 4. **CloudFront WAF only**: AWS WAF is configured for CloudFront distribution, not ALB directly 299 | 5. **StreamableHTTP transport only**: SSE transport (deprecated) not supported in this implementation 300 | 6. **Some MCP clients** may not support remote connections or OAuth flows 301 | 302 | For any feedback, questions, or suggestions, please use the issues tab under this repo. 303 | 304 | ## Revisions 305 | 306 | ### [2.0.0] - 2025-08-25 307 | 308 | - **BREAKING CHANGE**: Migrate to 2025-06-18 MCP specification 309 | - Implement OAuth 2.0 Protected Resource Metadata (RFC9728) 310 | - Replace SSE transport with StreamableHTTP transport 311 | - Add stateless server architecture for concurrent client support 312 | - Remove Dynamic Client Registration (DCR) - clients must be pre-configured 313 | - Restructure project to `source/cdk/ecs-and-lambda/` for better organization 314 | - Add sample Python MCP client with interactive CLI 315 | - Implement four-stack CDK deployment (VPC, Security, CloudFront WAF, MCP Server) 316 | - Add Lambda-based MCP server deployment option 317 | - Remove DynamoDB token storage - now using stateless authentication 318 | 319 | ### [1.0.0] - 2025-05-06 320 | 321 | - Initial release 322 | - Basic OAuth flow implementation 323 | - Support for weather sample servers 324 | 325 | ## Notices 326 | 327 | Customers are responsible for making their own independent assessment of the information in this Guidance. This Guidance: (a) is for informational purposes only, (b) represents AWS current product offerings and practices, which are subject to change without notice, and (c) does not create any commitments or assurances from AWS and its affiliates, suppliers or licensors. AWS products or services are provided "as is" without warranties, representations, or conditions of any kind, whether express or implied. AWS responsibilities and liabilities to its customers are controlled by AWS agreements, and this Guidance is not part of, nor does it modify, any agreement between AWS and its customers. 328 | -------------------------------------------------------------------------------- /assets/cost-estimate-report.md: -------------------------------------------------------------------------------- 1 | # AWS MCP Server Infrastructure Cost Analysis Estimate Report 2 | 3 | ## Service Overview 4 | 5 | AWS MCP Server Infrastructure is a fully managed solution for deploying Model Context Protocol servers using multiple AWS services. This project uses AWS ECS Fargate, Lambda, Cognito, CloudFront, and other AWS services to provide a scalable, secure MCP server deployment. This service follows a pay-as-you-go pricing model, making it cost-effective for various workloads. 6 | 7 | ## Pricing Model 8 | 9 | This cost analysis estimate is based on the following pricing model: 10 | 11 | - **ON DEMAND** pricing (pay-as-you-go) unless otherwise specified 12 | - Standard service configurations without reserved capacity or savings plans 13 | - No caching or optimization techniques applied 14 | 15 | ## Assumptions 16 | 17 | - Average monthly usage with moderate traffic 18 | - Deployment region is us-east-1 19 | - Single MCP server deployed with Python sample weather implementation 20 | - Moderate user authentication traffic through Cognito 21 | - 1 TB monthly data transfer for CloudFront 22 | - DynamoDB is used for token storage with on-demand capacity mode 23 | - Two Availability Zones are used as configured in VPC stack 24 | - NAT Gateway deployed for private subnet connectivity 25 | 26 | ## Limitations and Exclusions 27 | 28 | - Custom domain and SSL certificate costs (optional in the stack) 29 | - Data transfer between regions 30 | - Development and maintenance costs 31 | - Reserved capacity costs that might reduce pricing if committed 32 | - Optional MCP server implementations beyond the sample weather server 33 | 34 | ## Cost Breakdown 35 | 36 | ### Unit Pricing Details 37 | 38 | _Pricing verified as of August 2025 for us-east-1 region using AWS Pricing API via MCP server_ 39 | 40 | | Service | Resource Type | Unit | Price | Free Tier | 41 | | ---------------------- | --------------------------- | -------------------------------- | ------------- | ---------------------------------------------------------------------------------------------------- | 42 | | VPC | Nat Gateway | hour | $0.045 | No free tier for NAT Gateway | 43 | | VPC | Nat Gateway Data Processing | GB | $0.045 | No free tier for NAT Gateway | 44 | | Elastic Load Balancing | Load Balancer Hours | hour | $0.0225 | No free tier for ALB | 45 | | Elastic Load Balancing | Lcu | LCU-hour | $0.008 | No free tier for ALB | 46 | | Amazon Cognito | Mau Essentials | MAU after free tier | $0.0055 | First 50,000 MAUs are free with Essentials tier | 47 | | CloudFront | Data Transfer | GB after first TB | $0.085 | First 1 TB data transfer is free | 48 | | CloudFront | Https Requests | 10,000 requests after first 10M | $0.01 | First 10M HTTPS requests free per month, first 1 TB data transfer is free | 49 | | WAF | Web Acl | Web ACL per month | $5.00 | No free tier for WAF | 50 | | WAF | Rule Evaluations | million rule evaluations | $0.60 | No free tier for WAF | 51 | | DynamoDB | Storage | GB-month after free tier | $0.25 | 25 GB storage and limited read/write capacity included in free tier | 52 | | DynamoDB | Read Request Units | million RRUs (on-demand mode) | $0.25 | 25 GB storage and limited read/write capacity included in free tier | 53 | | DynamoDB | Write Request Units | million WRUs (on-demand mode) | $1.25 | 25 GB storage and limited read/write capacity included in free tier | 54 | | ECS | Fargate Vcpu | vCPU-hour | $0.04048 | No free tier specific to ECS, but underlying EC2 or Fargate resources may have free tier eligibility | 55 | | ECS | Fargate Memory | GB-hour | $0.004445 | No free tier specific to ECS, but underlying EC2 or Fargate resources may have free tier eligibility | 56 | | Secrets Manager | Secrets | secret per month | $0.40 | No free tier for Secrets Manager | 57 | | Secrets Manager | Api Calls | 10,000 API calls | $0.05 | No free tier for Secrets Manager | 58 | | Lambda | Requests | million requests after free tier | $0.20 | 1M free requests per month and 400,000 GB-seconds of compute time | 59 | | Lambda | Duration | GB-second after free tier | $0.0000166667 | 1M free requests per month and 400,000 GB-seconds of compute time | 60 | 61 | ### Cost Calculation 62 | 63 | | Service | Usage | Calculation | Monthly Cost | 64 | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------- | ----------------- | 65 | | VPC | 2 Availability Zones with public and private subnets, 1 NAT Gateway (Nat Gateway: 1 NAT Gateway × 730 hours, Data Processed: 100 GB (estimated)) | $0.045/hr × 730 hours = $32.85 for NAT Gateway + $0.045/GB × 100 GB = $4.50 for data processing | $37.35 | 66 | | Elastic Load Balancing | Application Load Balancer for MCP Server (Load Balancer Hours: 730 hours, Lcu: 50 LCUs (estimated average for moderate traffic)) | $0.0225/hr × 730 hours = $16.43 + $0.008/LCU × 50 LCU × 730 hours / 730 = $16.43 + $0.40 = $16.83 | $16.83 | 67 | | Amazon Cognito | User authentication for MCP Server (Mau Essentials: 10,500 MAUs (estimated)) | 10,500 MAUs under 50,000 free tier limit = $0.00 | $0.00 | 68 | | CloudFront | Content delivery for MCP server frontend (Data Transfer: 2 TB (2,000 GB), Https Requests: 15 million) | $0.085/GB × (2,000 GB - 1,024 GB) = $0.085 × 976 = $82.96 + $0.01/10k × (15M - 10M)/10k = $5.00 | $87.96 | 69 | | WAF | Web Application Firewall for both CloudFront and regional protection (Web Acl: 2 Web ACLs (CloudFront and Regional)) | $5.00 × 2 Web ACLs = $10.00 | $10.00 | 70 | | DynamoDB | Token storage for MCP server (Storage: 1 GB, Read Request Units: 5 million, Write Request Units: 3 million) | Storage covered by free tier, $0.25/M × (5M - free tier) + $1.25/M × (3M - free tier) ≈ $5.40 | $5.40 | 71 | | ECS | Container service for MCP server (Fargate Vcpu: 1 vCPU × 730 hours, Fargate Memory: 2 GB × 730 hours) | $0.04048 × 1 × 730 + $0.004445 × 2 × 730 = $29.55 + $6.49 = $36.04 | $36.04 | 72 | | Secrets Manager | Managing Cognito client secrets (Secrets: 1 secret, Api Calls: minimal, under 10,000) | $0.40 × 1 secret = $0.40 | $0.40 | 73 | | Lambda | Custom resources for configuration (Requests: minimal, under free tier, Duration: minimal, under free tier) | Likely covered by free tier, estimated minimal usage cost of $0.20 | $0.20 | 74 | | **Total** | **All services** | **Sum of all calculations** | **$194.18/month** | 75 | 76 | ### Free Tier 77 | 78 | Free tier information by service: 79 | 80 | - **VPC**: No free tier for NAT Gateway 81 | - **Elastic Load Balancing**: No free tier for ALB 82 | - **Amazon Cognito**: First 50,000 MAUs are free with Essentials tier 83 | - **CloudFront**: First 1 TB data transfer is free 84 | - **WAF**: No free tier for WAF 85 | - **DynamoDB**: 25 GB storage and limited read/write capacity included in free tier 86 | - **ECS**: No free tier specific to ECS, but underlying EC2 or Fargate resources may have free tier eligibility 87 | - **Secrets Manager**: No free tier for Secrets Manager 88 | - **Lambda**: 1M free requests per month and 400,000 GB-seconds of compute time 89 | 90 | ## Cost Scaling with Usage 91 | 92 | The following table illustrates how cost estimates scale with different usage levels: 93 | 94 | | Service | Low Usage | Medium Usage | High Usage | 95 | | ---------------------- | --------- | ------------ | ---------- | 96 | | VPC | $18/month | $37/month | $75/month | 97 | | Elastic Load Balancing | $10/month | $17/month | $35/month | 98 | | Amazon Cognito | $0/month | $0/month | $3/month | 99 | | CloudFront | $45/month | $88/month | $175/month | 100 | | WAF | $5/month | $10/month | $20/month | 101 | | DynamoDB | $2/month | $5/month | $12/month | 102 | | ECS | $18/month | $36/month | $72/month | 103 | | Secrets Manager | $0/month | $0/month | $1/month | 104 | | Lambda | $0/month | $0/month | $0/month | 105 | 106 | ### Key Cost Factors 107 | 108 | - **VPC**: 2 Availability Zones with public and private subnets, 1 NAT Gateway 109 | - **Elastic Load Balancing**: Application Load Balancer for MCP Server 110 | - **Amazon Cognito**: User authentication for MCP Server 111 | - **CloudFront**: Content delivery for MCP server frontend 112 | - **WAF**: Web Application Firewall for both CloudFront and regional protection 113 | - **DynamoDB**: Token storage for MCP server 114 | - **ECS**: Container service for MCP server 115 | - **Secrets Manager**: Managing Cognito client secrets 116 | - **Lambda**: Custom resources for configuration 117 | 118 | ## Projected Costs Over Time 119 | 120 | The following projections show estimated monthly costs over a 12-month period based on different growth patterns: 121 | 122 | Base monthly cost calculation: 123 | 124 | | Service | Monthly Cost | 125 | | ---------------------- | ------------ | 126 | | VPC | $37.35 | 127 | | Elastic Load Balancing | $16.83 | 128 | | Amazon Cognito | $0.00 | 129 | | CloudFront | $87.96 | 130 | | WAF | $10.00 | 131 | | DynamoDB | $5.40 | 132 | | ECS | $36.04 | 133 | | Secrets Manager | $0.40 | 134 | | Lambda | $0.20 | 135 | | **Total Monthly Cost** | **$194** | 136 | 137 | | Growth Pattern | Month 1 | Month 3 | Month 6 | Month 12 | 138 | | -------------- | ------- | ------- | ------- | -------- | 139 | | Steady | $194/mo | $194/mo | $194/mo | $194/mo | 140 | | Moderate | $194/mo | $214/mo | $248/mo | $333/mo | 141 | | Rapid | $194/mo | $235/mo | $314/mo | $558/mo | 142 | 143 | - Steady: No monthly growth (1.0x) 144 | - Moderate: 5% monthly growth (1.05x) 145 | - Rapid: 10% monthly growth (1.1x) 146 | 147 | ## Detailed Cost Analysis 148 | 149 | ### Pricing Model 150 | 151 | ON DEMAND 152 | 153 | ### Exclusions 154 | 155 | - Custom domain and SSL certificate costs (optional in the stack) 156 | - Data transfer between regions 157 | - Development and maintenance costs 158 | - Reserved capacity costs that might reduce pricing if committed 159 | - Optional MCP server implementations beyond the sample weather server 160 | 161 | ### Recommendations 162 | 163 | #### Immediate Actions 164 | 165 | - Monitor Cognito MAU usage to stay within free tier limits 166 | - Consider using reserved capacity for predictable ECS workloads 167 | - Evaluate if you need a NAT Gateway in both availability zones or if one is sufficient 168 | 169 | #### Best Practices 170 | 171 | - Use CloudWatch to monitor resource utilization and adjust capacity as needed 172 | - Consider Reserved Instances for long-term ECS usage to reduce costs 173 | - Set up AWS Budgets to alert on unexpected cost increases 174 | - Implement lifecycle policies for DynamoDB token table to automatically expire old tokens 175 | 176 | ## Cost Optimization Recommendations 177 | 178 | ### Immediate Actions 179 | 180 | - Take advantage of Cognito's increased free tier (50,000 MAUs vs previous 10,000) 181 | - Monitor usage patterns and consider reserved capacity for predictable workloads 182 | - Evaluate optimal CloudFront data transfer patterns 183 | 184 | ### Best Practices 185 | 186 | - Use CloudWatch to monitor resource utilization and adjust capacity as needed 187 | - Consider Reserved Instances for long-term ECS usage to reduce costs 188 | - Set up AWS Budgets to alert on unexpected cost increases 189 | 190 | ## Conclusion 191 | 192 | The updated cost analysis shows a total estimated monthly cost of $194.18, with the main cost drivers being CloudFront ($87.96), VPC NAT Gateway ($37.35), and ECS Fargate ($36.04). The Cognito free tier increase to 50,000 MAUs provides significant cost savings for moderate usage scenarios. By following the recommendations in this report, you can optimize your AWS MCP Server Infrastructure costs while maintaining performance and reliability. Regular monitoring and adjustment of your usage patterns will help ensure cost efficiency as your workload evolves. 193 | -------------------------------------------------------------------------------- /source/sample-clients/simple-auth-client-python/mcp_simple_auth_client/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Simple MCP client example with OAuth authentication support. 4 | 5 | This client connects to an MCP server using streamable HTTP transport with OAuth. 6 | 7 | """ 8 | 9 | import asyncio 10 | import os 11 | import threading 12 | import time 13 | import webbrowser 14 | from datetime import timedelta 15 | from http.server import BaseHTTPRequestHandler, HTTPServer 16 | from typing import Any 17 | from urllib.parse import parse_qs, urlparse 18 | 19 | from mcp.client.auth import OAuthClientProvider, TokenStorage 20 | from mcp.client.session import ClientSession 21 | from mcp.client.sse import sse_client 22 | from mcp.client.streamable_http import streamablehttp_client 23 | from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthToken 24 | 25 | 26 | class InMemoryTokenStorage(TokenStorage): 27 | """Simple in-memory token storage implementation.""" 28 | 29 | def __init__(self): 30 | self._tokens: OAuthToken | None = None 31 | self._client_info: OAuthClientInformationFull | None = None 32 | 33 | async def get_tokens(self) -> OAuthToken | None: 34 | return self._tokens 35 | 36 | async def set_tokens(self, tokens: OAuthToken) -> None: 37 | self._tokens = tokens 38 | 39 | async def get_client_info(self) -> OAuthClientInformationFull | None: 40 | return self._client_info 41 | 42 | async def set_client_info(self, client_info: OAuthClientInformationFull) -> None: 43 | self._client_info = client_info 44 | 45 | 46 | class CallbackHandler(BaseHTTPRequestHandler): 47 | """Simple HTTP handler to capture OAuth callback.""" 48 | 49 | def __init__(self, request, client_address, server, callback_data): 50 | """Initialize with callback data storage.""" 51 | self.callback_data = callback_data 52 | super().__init__(request, client_address, server) 53 | 54 | def do_GET(self): 55 | """Handle GET request from OAuth redirect.""" 56 | parsed = urlparse(self.path) 57 | query_params = parse_qs(parsed.query) 58 | 59 | if "code" in query_params: 60 | self.callback_data["authorization_code"] = query_params["code"][0] 61 | self.callback_data["state"] = query_params.get("state", [None])[0] 62 | self.send_response(200) 63 | self.send_header("Content-type", "text/html") 64 | self.end_headers() 65 | self.wfile.write(b""" 66 | 67 | 68 |

Authorization Successful!

69 |

You can close this window and return to the terminal.

70 | 71 | 72 | 73 | """) 74 | elif "error" in query_params: 75 | self.callback_data["error"] = query_params["error"][0] 76 | self.send_response(400) 77 | self.send_header("Content-type", "text/html") 78 | self.end_headers() 79 | self.wfile.write( 80 | f""" 81 | 82 | 83 |

Authorization Failed

84 |

Error: {query_params["error"][0]}

85 |

You can close this window and return to the terminal.

86 | 87 | 88 | """.encode() 89 | ) 90 | else: 91 | self.send_response(404) 92 | self.end_headers() 93 | 94 | def log_message(self, format, *args): 95 | """Suppress default logging.""" 96 | pass 97 | 98 | 99 | class CallbackServer: 100 | """Simple server to handle OAuth callbacks.""" 101 | 102 | def __init__(self, port=3000): 103 | self.port = port 104 | self.server = None 105 | self.thread = None 106 | self.callback_data = {"authorization_code": None, "state": None, "error": None} 107 | 108 | def _create_handler_with_data(self): 109 | """Create a handler class with access to callback data.""" 110 | callback_data = self.callback_data 111 | 112 | class DataCallbackHandler(CallbackHandler): 113 | def __init__(self, request, client_address, server): 114 | super().__init__(request, client_address, server, callback_data) 115 | 116 | return DataCallbackHandler 117 | 118 | def start(self): 119 | """Start the callback server in a background thread.""" 120 | handler_class = self._create_handler_with_data() 121 | self.server = HTTPServer(("localhost", self.port), handler_class) 122 | self.thread = threading.Thread(target=self.server.serve_forever, daemon=True) 123 | self.thread.start() 124 | print(f"🖥️ Started callback server on http://localhost:{self.port}") 125 | 126 | def stop(self): 127 | """Stop the callback server.""" 128 | if self.server: 129 | self.server.shutdown() 130 | self.server.server_close() 131 | if self.thread: 132 | self.thread.join(timeout=1) 133 | 134 | def wait_for_callback(self, timeout=300): 135 | """Wait for OAuth callback with timeout.""" 136 | start_time = time.time() 137 | while time.time() - start_time < timeout: 138 | if self.callback_data["authorization_code"]: 139 | return self.callback_data["authorization_code"] 140 | elif self.callback_data["error"]: 141 | raise Exception(f"OAuth error: {self.callback_data['error']}") 142 | time.sleep(0.1) 143 | raise Exception("Timeout waiting for OAuth callback") 144 | 145 | def get_state(self): 146 | """Get the received state parameter.""" 147 | return self.callback_data["state"] 148 | 149 | 150 | class SimpleAuthClient: 151 | """Simple MCP client with auth support.""" 152 | 153 | def __init__(self, server_url: str, transport_type: str = "streamable_http"): 154 | self.server_url = server_url 155 | self.transport_type = transport_type 156 | self.session: ClientSession | None = None 157 | 158 | async def connect(self): 159 | """Connect to the MCP server.""" 160 | print(f"🔗 Attempting to connect to {self.server_url}...") 161 | 162 | try: 163 | callback_server = CallbackServer(port=2299) 164 | callback_server.start() 165 | 166 | async def callback_handler() -> tuple[str, str | None]: 167 | """Wait for OAuth callback and return auth code and state.""" 168 | print("⏳ Waiting for authorization callback...") 169 | try: 170 | auth_code = callback_server.wait_for_callback(timeout=300) 171 | return auth_code, callback_server.get_state() 172 | finally: 173 | callback_server.stop() 174 | 175 | client_metadata_dict = { 176 | "client_name": "Simple Auth Client", 177 | "redirect_uris": ["http://localhost:2299/callback"], 178 | "grant_types": ["authorization_code", "refresh_token"], 179 | "response_types": ["code"], 180 | "token_endpoint_auth_method": "client_secret_post", 181 | "scope": "openid email profile", # Add this line 182 | } 183 | 184 | async def _default_redirect_handler(authorization_url: str) -> None: 185 | """Default redirect handler that opens the URL in a browser.""" 186 | print(f"Opening browser for authorization: {authorization_url}") 187 | webbrowser.open(authorization_url) 188 | 189 | # Create storage and set client credentials 190 | storage = InMemoryTokenStorage() 191 | client_info = OAuthClientInformationFull( 192 | client_id=os.getenv("OAUTH_CLIENT_ID"), 193 | client_secret=os.getenv("OAUTH_CLIENT_SECRET"), 194 | client_name="Simple Auth Client", 195 | redirect_uris=["http://localhost:2299/callback"], 196 | grant_types=["authorization_code", "refresh_token"], 197 | response_types=["code"], 198 | token_endpoint_auth_method="client_secret_post", 199 | ) 200 | await storage.set_client_info(client_info) 201 | 202 | # Create OAuth authentication handler using the new interface 203 | oauth_auth = OAuthClientProvider( 204 | server_url=self.server_url.replace("/mcp", ""), 205 | client_metadata=OAuthClientMetadata.model_validate(client_metadata_dict), 206 | storage=storage, 207 | redirect_handler=_default_redirect_handler, 208 | callback_handler=callback_handler, 209 | ) 210 | 211 | # Create transport with auth handler based on transport type 212 | if self.transport_type == "sse": 213 | print("📡 Opening SSE transport connection with auth...") 214 | async with sse_client( 215 | url=self.server_url, 216 | auth=oauth_auth, 217 | timeout=60, 218 | ) as (read_stream, write_stream): 219 | await self._run_session(read_stream, write_stream, None) 220 | else: 221 | print("📡 Opening StreamableHTTP transport connection with auth...") 222 | async with streamablehttp_client( 223 | url=self.server_url, 224 | auth=oauth_auth, 225 | timeout=timedelta(seconds=60), 226 | ) as (read_stream, write_stream, get_session_id): 227 | await self._run_session(read_stream, write_stream, get_session_id) 228 | 229 | except Exception as e: 230 | print(f"❌ Failed to connect: {e}") 231 | import traceback 232 | 233 | traceback.print_exc() 234 | 235 | async def _run_session(self, read_stream, write_stream, get_session_id): 236 | """Run the MCP session with the given streams.""" 237 | print("🤝 Initializing MCP session...") 238 | async with ClientSession(read_stream, write_stream) as session: 239 | self.session = session 240 | print("⚡ Starting session initialization...") 241 | await session.initialize() 242 | print("✨ Session initialization complete!") 243 | 244 | print(f"\n✅ Connected to MCP server at {self.server_url}") 245 | if get_session_id: 246 | session_id = get_session_id() 247 | if session_id: 248 | print(f"Session ID: {session_id}") 249 | 250 | # Run interactive loop 251 | await self.interactive_loop() 252 | 253 | async def list_tools(self): 254 | """List available tools from the server.""" 255 | if not self.session: 256 | print("❌ Not connected to server") 257 | return 258 | 259 | try: 260 | result = await self.session.list_tools() 261 | if hasattr(result, "tools") and result.tools: 262 | print("\n📋 Available tools:") 263 | for i, tool in enumerate(result.tools, 1): 264 | print(f"{i}. {tool.name}") 265 | if tool.description: 266 | print(f" Description: {tool.description}") 267 | print() 268 | else: 269 | print("No tools available") 270 | except Exception as e: 271 | print(f"❌ Failed to list tools: {e}") 272 | 273 | async def call_tool(self, tool_name: str, arguments: dict[str, Any] | None = None): 274 | """Call a specific tool.""" 275 | if not self.session: 276 | print("❌ Not connected to server") 277 | return 278 | 279 | try: 280 | result = await self.session.call_tool(tool_name, arguments or {}) 281 | print(f"\n🔧 Tool '{tool_name}' result:") 282 | if hasattr(result, "content"): 283 | for content in result.content: 284 | if content.type == "text": 285 | print(content.text) 286 | else: 287 | print(content) 288 | else: 289 | print(result) 290 | except Exception as e: 291 | print(f"❌ Failed to call tool '{tool_name}': {e}") 292 | 293 | async def interactive_loop(self): 294 | """Run interactive command loop.""" 295 | print("\n🎯 Interactive MCP Client") 296 | print("Commands:") 297 | print(" list - List available tools") 298 | print(" call [args] - Call a tool") 299 | print(" quit - Exit the client") 300 | print() 301 | 302 | while True: 303 | try: 304 | command = input("mcp> ").strip() 305 | 306 | if not command: 307 | continue 308 | 309 | if command == "quit": 310 | break 311 | 312 | elif command == "list": 313 | await self.list_tools() 314 | 315 | elif command.startswith("call "): 316 | parts = command.split(maxsplit=2) 317 | tool_name = parts[1] if len(parts) > 1 else "" 318 | 319 | if not tool_name: 320 | print("❌ Please specify a tool name") 321 | continue 322 | 323 | # Parse arguments (simple JSON-like format) 324 | arguments = {} 325 | if len(parts) > 2: 326 | import json 327 | 328 | try: 329 | arguments = json.loads(parts[2]) 330 | except json.JSONDecodeError: 331 | print("❌ Invalid arguments format (expected JSON)") 332 | continue 333 | 334 | await self.call_tool(tool_name, arguments) 335 | 336 | else: 337 | print("❌ Unknown command. Try 'list', 'call ', or 'quit'") 338 | 339 | except KeyboardInterrupt: 340 | print("\n\n👋 Goodbye!") 341 | break 342 | except EOFError: 343 | break 344 | 345 | 346 | async def main(): 347 | """Main entry point.""" 348 | # Default server URL - can be overridden with environment variable 349 | # Most MCP streamable HTTP servers use /mcp as the endpoint 350 | server_url = os.environ["MCP_SERVER_URL"] 351 | transport_type = os.getenv("MCP_TRANSPORT_TYPE", "streamable_http") 352 | 353 | print("🚀 Simple MCP Auth Client") 354 | print(f"Connecting to: {server_url}") 355 | print(f"Transport type: {transport_type}") 356 | 357 | # Start connection flow - OAuth will be handled automatically 358 | client = SimpleAuthClient(server_url, transport_type) 359 | await client.connect() 360 | 361 | 362 | def cli(): 363 | """CLI entry point for uv script.""" 364 | asyncio.run(main()) 365 | 366 | 367 | if __name__ == "__main__": 368 | cli() 369 | -------------------------------------------------------------------------------- /source/cdk/ecs-and-lambda/lib/stacks/mcp-server-stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from "aws-cdk-lib"; 2 | import { Construct } from "constructs"; 3 | import * as path from "path"; 4 | import * as ec2 from "aws-cdk-lib/aws-ec2"; 5 | import * as elbv2 from "aws-cdk-lib/aws-elasticloadbalancingv2"; 6 | import * as ecs from "aws-cdk-lib/aws-ecs"; 7 | import * as acm from "aws-cdk-lib/aws-certificatemanager"; 8 | import * as cloudfront from "aws-cdk-lib/aws-cloudfront"; 9 | import * as route53 from "aws-cdk-lib/aws-route53"; 10 | import * as route53targets from "aws-cdk-lib/aws-route53-targets"; 11 | import * as ssm from "aws-cdk-lib/aws-ssm"; 12 | import * as lambda from "aws-cdk-lib/aws-lambda"; 13 | import * as origins from "aws-cdk-lib/aws-cloudfront-origins"; 14 | import { McpFargateServerConstruct } from "../constructs/mcp-fargate-server-construct"; 15 | import { NagSuppressions } from "cdk-nag"; 16 | import { McpLambdaServerlessConstruct } from "../constructs/mcp-lambda-serverless-construct"; 17 | import { getAllowedCountries } from "../constants/geo-restrictions"; 18 | import { Platform } from "aws-cdk-lib/aws-ecr-assets"; 19 | 20 | export interface MCPServerStackProps extends cdk.StackProps { 21 | /** 22 | * Suffix to append to resource names 23 | */ 24 | resourceSuffix: string; 25 | vpc: ec2.IVpc; 26 | } 27 | 28 | /** 29 | * Combined stack for MCP platform and servers to avoid circular dependencies 30 | */ 31 | export class MCPServerStack extends cdk.Stack { 32 | public readonly albSecurityGroup: ec2.SecurityGroup; 33 | public readonly loadBalancer: elbv2.ApplicationLoadBalancer; 34 | public readonly cluster: ecs.Cluster; 35 | public readonly distribution: cloudfront.Distribution; 36 | 37 | constructor(scope: Construct, id: string, props: MCPServerStackProps) { 38 | super(scope, id, props); 39 | 40 | // Get CloudFront WAF ARN from SSM (written by CloudFrontWafStack) 41 | const cloudFrontWafArnParam = 42 | ssm.StringParameter.fromStringParameterAttributes( 43 | this, 44 | "CloudFrontWafArnParam", 45 | { 46 | parameterName: `/mcp/cloudfront-waf-arn-${props.resourceSuffix}`, 47 | } 48 | ); 49 | 50 | // Get Cognito User Pool ID from SSM (written by SecurityStack) 51 | const userPoolIdParam = ssm.StringParameter.fromStringParameterAttributes( 52 | this, 53 | "UserPoolIdParam", 54 | { 55 | parameterName: `/mcp/cognito/user-pool-id-${props.resourceSuffix}`, 56 | } 57 | ); 58 | 59 | // Get Cognito User Pool Client ID from SSM (written by SecurityStack) 60 | const userPoolClientIdParam = 61 | ssm.StringParameter.fromStringParameterAttributes( 62 | this, 63 | "UserPoolClientIdParam", 64 | { 65 | parameterName: `/mcp/cognito/user-pool-client-id-${props.resourceSuffix}`, 66 | } 67 | ); 68 | 69 | // Create shared ECS cluster for all MCP servers 70 | this.cluster = new ecs.Cluster(this, "MCPCluster", { 71 | vpc: props.vpc, 72 | //containerInsights: true, 73 | containerInsightsV2: ecs.ContainerInsights.ENHANCED, 74 | }); 75 | 76 | // Add suppression for Container Insight (Deprecated) not be enabled while Container Insight V2 is enabled 77 | NagSuppressions.addResourceSuppressions(this.cluster, [ 78 | { 79 | id: "AwsSolutions-ECS4", 80 | reason: 81 | "Container Insights V2 is Enabled with Enhanced capabilities, the Nag findings is about Container Insights (v1) which is deprecated", 82 | }, 83 | ]); 84 | 85 | // Create context parameters for multi-region certificate support 86 | const cdnCertificateArn = this.node.tryGetContext("cdnCertificateArn"); 87 | const albCertificateArn = this.node.tryGetContext("albCertificateArn"); 88 | const customDomain = this.node.tryGetContext("customDomain"); 89 | 90 | // Validate certificate and domain requirements 91 | if ((cdnCertificateArn || albCertificateArn) && !customDomain) { 92 | throw new Error( 93 | "Custom domain name must be provided when using certificates. " + 94 | "CloudFront and ALB require a valid domain name for certificate association." 95 | ); 96 | } 97 | 98 | // Validate CloudFront certificate is in us-east-1 if provided 99 | if (cdnCertificateArn) { 100 | const cfCertRegion = cdk.Arn.split( 101 | cdnCertificateArn, 102 | cdk.ArnFormat.SLASH_RESOURCE_NAME 103 | ).region; 104 | if (cfCertRegion !== "us-east-1") { 105 | throw new Error( 106 | `CloudFront certificate must be in us-east-1 region, but found in ${cfCertRegion}. ` + 107 | "Use cdnCertificateArn context parameter with a certificate from us-east-1." 108 | ); 109 | } 110 | } 111 | 112 | // Validate ALB certificate is in the current stack region if provided 113 | if (albCertificateArn) { 114 | const albCertRegion = cdk.Arn.split( 115 | albCertificateArn, 116 | cdk.ArnFormat.SLASH_RESOURCE_NAME 117 | ).region; 118 | if (albCertRegion !== this.region) { 119 | throw new Error( 120 | `ALB certificate must be in the same region as the stack (${this.region}), but found in ${albCertRegion}. ` + 121 | "Use albCertificateArn context parameter with a certificate from the deployment region." 122 | ); 123 | } 124 | } 125 | 126 | // Create HTTP and HTTPS security groups for the ALB 127 | const httpSecurityGroup = new ec2.SecurityGroup( 128 | this, 129 | `HttpSecurityGroup-${props.resourceSuffix}`, 130 | { 131 | vpc: props.vpc, 132 | allowAllOutbound: true, 133 | description: `HTTP Security group for MCP-Server Stack ALB`, 134 | } 135 | ); 136 | 137 | const httpsSecurityGroup = new ec2.SecurityGroup( 138 | this, 139 | `HttpsSecurityGroup-${props.resourceSuffix}`, 140 | { 141 | vpc: props.vpc, 142 | allowAllOutbound: true, 143 | description: `HTTPS Security group for MCP-Server Stack ALB`, 144 | } 145 | ); 146 | 147 | const cloudFrontPrefixList = ec2.PrefixList.fromLookup( 148 | this, 149 | "CloudFrontOriginFacing", 150 | { 151 | prefixListName: "com.amazonaws.global.cloudfront.origin-facing", 152 | } 153 | ); 154 | 155 | // Add ingress rules to appropriate security group 156 | httpSecurityGroup.addIngressRule( 157 | ec2.Peer.prefixList(cloudFrontPrefixList.prefixListId), 158 | ec2.Port.tcp(80), 159 | "Allow HTTP traffic from CloudFront edge locations" 160 | ); 161 | 162 | httpsSecurityGroup.addIngressRule( 163 | ec2.Peer.prefixList(cloudFrontPrefixList.prefixListId), 164 | ec2.Port.tcp(443), 165 | "Allow HTTPS traffic from CloudFront edge locations" 166 | ); 167 | 168 | // Use the appropriate security group based on ALB certificate presence 169 | this.albSecurityGroup = albCertificateArn 170 | ? httpsSecurityGroup 171 | : httpSecurityGroup; 172 | 173 | // Create S3 bucket for ALB and CloudFront access logs with proper encryption and lifecycle rules 174 | const accessLogsBucket = new cdk.aws_s3.Bucket(this, "AccessLogsBucket", { 175 | encryption: cdk.aws_s3.BucketEncryption.S3_MANAGED, 176 | enforceSSL: true, 177 | blockPublicAccess: cdk.aws_s3.BlockPublicAccess.BLOCK_ALL, 178 | removalPolicy: cdk.RemovalPolicy.DESTROY, // For dev environment (use RETAIN for prod) 179 | autoDeleteObjects: true, 180 | lifecycleRules: [ 181 | { 182 | expiration: cdk.Duration.days(30), // Retain logs for 30 days 183 | }, 184 | ], 185 | serverAccessLogsPrefix: "server-access-logs/", // Separate prefix for server access logs 186 | objectOwnership: cdk.aws_s3.ObjectOwnership.BUCKET_OWNER_PREFERRED, // Required for CloudFront logging 187 | }); 188 | 189 | // Create Application Load Balancer dedicated to this MCP server 190 | this.loadBalancer = new elbv2.ApplicationLoadBalancer( 191 | this, 192 | `ApplicationLoadBalancer`, 193 | { 194 | vpc: props.vpc, 195 | internetFacing: true, 196 | securityGroup: this.albSecurityGroup, 197 | http2Enabled: true, 198 | } 199 | ); 200 | 201 | // Enable access logging to S3 202 | this.loadBalancer.logAccessLogs(accessLogsBucket); 203 | 204 | const paramName = `/mcp/https-url`; 205 | 206 | // **************************************************************** 207 | // Model Context Prototcol Server(s) built on ECS Fargate 208 | // **************************************************************** 209 | 210 | // Deploy the NodeJs weather server with CloudFront 211 | const weatherNodeJsServer = new McpFargateServerConstruct( 212 | this, 213 | "WeatherNodeJsServer", 214 | { 215 | platform: { 216 | vpc: props.vpc, 217 | cluster: this.cluster, 218 | }, 219 | serverName: "WeatherNodeJs", 220 | serverPath: path.join( 221 | __dirname, 222 | "../../servers/sample-ecs-weather-streamablehttp-stateless-nodejs-express" 223 | ), 224 | healthCheckPath: "/weather-nodejs/", 225 | environment: { 226 | PORT: "8080", 227 | BASE_PATH: "/weather-nodejs", 228 | AWS_REGION: this.region, 229 | COGNITO_USER_POOL_ID: userPoolIdParam.stringValue, 230 | COGNITO_CLIENT_ID: userPoolClientIdParam.stringValue, 231 | }, 232 | albSecurityGroup: this.albSecurityGroup, 233 | urlParameterName: paramName, 234 | } 235 | ); 236 | 237 | // **************************************************************** 238 | // Model Context Prototcol Server(s) built on Lambda 239 | // **************************************************************** 240 | 241 | // Deploy the NodeJS weather server using Streamable HTTP Transport (according to 2025-03-26 specification) 242 | const weatherLambda = new lambda.DockerImageFunction( 243 | this, 244 | "WeatherNodeJsLambda", 245 | { 246 | code: lambda.DockerImageCode.fromImageAsset( 247 | path.join( 248 | __dirname, 249 | "../../servers/sample-lambda-weather-streamablehttp-stateless-nodejs-express" 250 | ), 251 | { platform: Platform.LINUX_AMD64 } 252 | ), 253 | timeout: cdk.Duration.minutes(1), 254 | memorySize: 1024, 255 | environment: { 256 | COGNITO_USER_POOL_ID: userPoolIdParam.stringValue, 257 | COGNITO_CLIENT_ID: userPoolClientIdParam.stringValue, 258 | PORT: "8080", 259 | }, 260 | vpc: props.vpc, 261 | } 262 | ); 263 | 264 | const weatherNodeJsLambdaServer = new McpLambdaServerlessConstruct( 265 | this, 266 | "WeatherNodeJsLambdaServer", 267 | { 268 | vpc: props.vpc, 269 | function: weatherLambda, 270 | } 271 | ); 272 | 273 | // Add suppression for Lambda basic execution role 274 | NagSuppressions.addResourceSuppressions( 275 | weatherLambda, 276 | [ 277 | { 278 | id: "AwsSolutions-IAM4", 279 | reason: 280 | "Lambda function requires basic VPC and CloudWatch Logs permissions through managed policy", 281 | appliesTo: [ 282 | "Policy::arn::iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", 283 | "Policy::arn::iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole", 284 | ], 285 | }, 286 | ], 287 | true 288 | ); 289 | 290 | // Create either HTTP or HTTPS listener based on ALB certificate presence 291 | const listener = albCertificateArn 292 | ? this.loadBalancer.addListener("HttpsListener", { 293 | port: 443, 294 | protocol: elbv2.ApplicationProtocol.HTTPS, 295 | certificates: [ 296 | acm.Certificate.fromCertificateArn( 297 | this, 298 | "AlbCertificate", 299 | albCertificateArn 300 | ), 301 | ], 302 | open: false, 303 | defaultAction: elbv2.ListenerAction.fixedResponse(404, { 304 | contentType: "text/plain", 305 | messageBody: "No matching route found", 306 | }), 307 | }) 308 | : this.loadBalancer.addListener("HttpListener", { 309 | port: 80, 310 | protocol: elbv2.ApplicationProtocol.HTTP, 311 | open: false, 312 | defaultAction: elbv2.ListenerAction.fixedResponse(404, { 313 | contentType: "text/plain", 314 | messageBody: "No matching route found", 315 | }), 316 | }); 317 | 318 | // Add routing rules to the listener 319 | 320 | listener.addAction("WeatherNodeJsRoute", { 321 | priority: 21, 322 | conditions: [elbv2.ListenerCondition.pathPatterns(["/weather-nodejs/*"])], 323 | action: elbv2.ListenerAction.forward([weatherNodeJsServer.targetGroup]), 324 | }); 325 | 326 | // Add a rule to route auth-related paths to the auth server 327 | listener.addAction("WeatherNodeJsLambdaRoute", { 328 | priority: 22, // Lower number means higher priority 329 | conditions: [ 330 | elbv2.ListenerCondition.pathPatterns(["/weather-nodejs-lambda/*"]), 331 | ], 332 | action: elbv2.ListenerAction.forward([ 333 | weatherNodeJsLambdaServer.targetGroup, 334 | ]), 335 | }); 336 | 337 | // Create CloudFront distribution with protocol matching ALB listener 338 | const albOrigin = new origins.LoadBalancerV2Origin(this.loadBalancer, { 339 | protocolPolicy: albCertificateArn 340 | ? cloudfront.OriginProtocolPolicy.HTTPS_ONLY 341 | : cloudfront.OriginProtocolPolicy.HTTP_ONLY, 342 | httpPort: 80, 343 | httpsPort: 443, 344 | connectionAttempts: 3, 345 | connectionTimeout: cdk.Duration.seconds(10), 346 | readTimeout: cdk.Duration.seconds(30), 347 | keepaliveTimeout: cdk.Duration.seconds(5), 348 | }); 349 | 350 | const geoRestriction = cloudfront.GeoRestriction.allowlist( 351 | ...getAllowedCountries() 352 | ); 353 | 354 | // Create the CloudFront distribution with conditional properties 355 | if (customDomain && cdnCertificateArn) { 356 | // With custom domain and CDN certificate 357 | const certificate = acm.Certificate.fromCertificateArn( 358 | this, 359 | `MCPServerStackCertificate`, 360 | cdnCertificateArn 361 | ); 362 | 363 | this.distribution = new cloudfront.Distribution( 364 | this, 365 | `MCPServerStackDistribution`, 366 | { 367 | defaultBehavior: { 368 | origin: albOrigin, 369 | allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL, 370 | cachePolicy: cloudfront.CachePolicy.CACHING_DISABLED, 371 | viewerProtocolPolicy: 372 | cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, 373 | originRequestPolicy: cloudfront.OriginRequestPolicy.ALL_VIEWER, 374 | }, 375 | domainNames: [customDomain], 376 | certificate: certificate, 377 | enabled: true, 378 | minimumProtocolVersion: 379 | cloudfront.SecurityPolicyProtocol.TLS_V1_2_2021, 380 | httpVersion: cloudfront.HttpVersion.HTTP2_AND_3, 381 | priceClass: cloudfront.PriceClass.PRICE_CLASS_100, 382 | comment: `CloudFront distribution for MCP-Server Stack with custom domain`, 383 | geoRestriction, 384 | webAclId: cloudFrontWafArnParam.stringValue, 385 | logBucket: accessLogsBucket, 386 | logFilePrefix: "cloudfront-logs/", 387 | } 388 | ); 389 | } else { 390 | // Default CloudFront domain 391 | this.distribution = new cloudfront.Distribution( 392 | this, 393 | `MCPServerStackDistribution`, 394 | { 395 | defaultBehavior: { 396 | origin: albOrigin, 397 | allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL, 398 | cachePolicy: cloudfront.CachePolicy.CACHING_DISABLED, 399 | viewerProtocolPolicy: 400 | cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, 401 | originRequestPolicy: cloudfront.OriginRequestPolicy.ALL_VIEWER, 402 | }, 403 | enabled: true, 404 | httpVersion: cloudfront.HttpVersion.HTTP2_AND_3, 405 | priceClass: cloudfront.PriceClass.PRICE_CLASS_100, 406 | comment: `CloudFront distribution for MCP-Server stack`, 407 | geoRestriction, 408 | webAclId: cloudFrontWafArnParam.stringValue, 409 | logBucket: accessLogsBucket, 410 | logFilePrefix: "cloudfront-logs/", 411 | } 412 | ); 413 | } 414 | 415 | // Add suppressions for CloudFront TLS warnings 416 | NagSuppressions.addResourceSuppressions(this.distribution, [ 417 | { 418 | id: "AwsSolutions-CFR4", 419 | reason: 420 | "Development environment using default CloudFront certificate without custom domain - TLS settings are managed by CloudFront", 421 | }, 422 | { 423 | id: "AwsSolutions-CFR5", 424 | reason: 425 | "Development environment using HTTP-only communication to ALB origin which is internal to VPC", 426 | }, 427 | ]); 428 | 429 | // Create Route 53 records if custom domain and CDN certificate are provided 430 | if (customDomain && cdnCertificateArn) { 431 | // Look up the hosted zone 432 | const hostedZone = route53.HostedZone.fromLookup(this, "HostedZone", { 433 | domainName: customDomain, 434 | }); 435 | 436 | // Create A record for the custom domain 437 | new route53.ARecord(this, "McpServerARecord", { 438 | zone: hostedZone, 439 | recordName: customDomain, 440 | target: route53.RecordTarget.fromAlias( 441 | new route53targets.CloudFrontTarget(this.distribution) 442 | ), 443 | }); 444 | } 445 | 446 | // Set the HTTPS URL 447 | const httpsUrl = 448 | customDomain && cdnCertificateArn 449 | ? `https://${customDomain}` 450 | : `https://${this.distribution.distributionDomainName}`; 451 | 452 | // Output CloudFront distribution details 453 | new cdk.CfnOutput(this, "CloudFrontDistributions", { 454 | value: httpsUrl, 455 | description: "CloudFront HTTPS URLs for all MCP servers", 456 | }); 457 | } 458 | } 459 | -------------------------------------------------------------------------------- /source/cdk/ecs-and-lambda/servers/sample-ecs-weather-streamablehttp-stateless-nodejs-express/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sample-weather-nodejs", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "sample-weather-nodejs", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "@modelcontextprotocol/sdk": "^1.17.2", 13 | "@types/jsonwebtoken": "^9.0.9", 14 | "@types/uuid": "^10.0.0", 15 | "cors": "^2.8.5", 16 | "express": "^5.1.0", 17 | "jose": "^6.0.11", 18 | "jsonwebtoken": "^9.0.2", 19 | "node-fetch": "^3.3.2", 20 | "uuid": "^11.1.0", 21 | "zod": "^3.24.2" 22 | }, 23 | "bin": { 24 | "weather": "build/index.js" 25 | }, 26 | "devDependencies": { 27 | "@types/express": "^5.0.1", 28 | "@types/node": "^22.14.1", 29 | "typescript": "^5.8.3" 30 | } 31 | }, 32 | "node_modules/@modelcontextprotocol/sdk": { 33 | "version": "1.17.2", 34 | "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.17.2.tgz", 35 | "integrity": "sha512-EFLRNXR/ixpXQWu6/3Cu30ndDFIFNaqUXcTqsGebujeMan9FzhAaFFswLRiFj61rgygDRr8WO1N+UijjgRxX9g==", 36 | "dependencies": { 37 | "ajv": "^6.12.6", 38 | "content-type": "^1.0.5", 39 | "cors": "^2.8.5", 40 | "cross-spawn": "^7.0.5", 41 | "eventsource": "^3.0.2", 42 | "eventsource-parser": "^3.0.0", 43 | "express": "^5.0.1", 44 | "express-rate-limit": "^7.5.0", 45 | "pkce-challenge": "^5.0.0", 46 | "raw-body": "^3.0.0", 47 | "zod": "^3.23.8", 48 | "zod-to-json-schema": "^3.24.1" 49 | }, 50 | "engines": { 51 | "node": ">=18" 52 | } 53 | }, 54 | "node_modules/@types/body-parser": { 55 | "version": "1.19.5", 56 | "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", 57 | "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", 58 | "dev": true, 59 | "dependencies": { 60 | "@types/connect": "*", 61 | "@types/node": "*" 62 | } 63 | }, 64 | "node_modules/@types/connect": { 65 | "version": "3.4.38", 66 | "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", 67 | "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", 68 | "dev": true, 69 | "dependencies": { 70 | "@types/node": "*" 71 | } 72 | }, 73 | "node_modules/@types/express": { 74 | "version": "5.0.1", 75 | "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.1.tgz", 76 | "integrity": "sha512-UZUw8vjpWFXuDnjFTh7/5c2TWDlQqeXHi6hcN7F2XSVT5P+WmUnnbFS3KA6Jnc6IsEqI2qCVu2bK0R0J4A8ZQQ==", 77 | "dev": true, 78 | "dependencies": { 79 | "@types/body-parser": "*", 80 | "@types/express-serve-static-core": "^5.0.0", 81 | "@types/serve-static": "*" 82 | } 83 | }, 84 | "node_modules/@types/express-serve-static-core": { 85 | "version": "5.0.6", 86 | "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz", 87 | "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==", 88 | "dev": true, 89 | "dependencies": { 90 | "@types/node": "*", 91 | "@types/qs": "*", 92 | "@types/range-parser": "*", 93 | "@types/send": "*" 94 | } 95 | }, 96 | "node_modules/@types/http-errors": { 97 | "version": "2.0.4", 98 | "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", 99 | "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", 100 | "dev": true 101 | }, 102 | "node_modules/@types/jsonwebtoken": { 103 | "version": "9.0.9", 104 | "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.9.tgz", 105 | "integrity": "sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==", 106 | "dependencies": { 107 | "@types/ms": "*", 108 | "@types/node": "*" 109 | } 110 | }, 111 | "node_modules/@types/mime": { 112 | "version": "1.3.5", 113 | "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", 114 | "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", 115 | "dev": true 116 | }, 117 | "node_modules/@types/ms": { 118 | "version": "2.1.0", 119 | "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", 120 | "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==" 121 | }, 122 | "node_modules/@types/node": { 123 | "version": "22.15.3", 124 | "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.3.tgz", 125 | "integrity": "sha512-lX7HFZeHf4QG/J7tBZqrCAXwz9J5RD56Y6MpP0eJkka8p+K0RY/yBTW7CYFJ4VGCclxqOLKmiGP5juQc6MKgcw==", 126 | "dependencies": { 127 | "undici-types": "~6.21.0" 128 | } 129 | }, 130 | "node_modules/@types/qs": { 131 | "version": "6.9.18", 132 | "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", 133 | "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", 134 | "dev": true 135 | }, 136 | "node_modules/@types/range-parser": { 137 | "version": "1.2.7", 138 | "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", 139 | "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", 140 | "dev": true 141 | }, 142 | "node_modules/@types/send": { 143 | "version": "0.17.4", 144 | "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", 145 | "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", 146 | "dev": true, 147 | "dependencies": { 148 | "@types/mime": "^1", 149 | "@types/node": "*" 150 | } 151 | }, 152 | "node_modules/@types/serve-static": { 153 | "version": "1.15.7", 154 | "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", 155 | "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", 156 | "dev": true, 157 | "dependencies": { 158 | "@types/http-errors": "*", 159 | "@types/node": "*", 160 | "@types/send": "*" 161 | } 162 | }, 163 | "node_modules/@types/uuid": { 164 | "version": "10.0.0", 165 | "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", 166 | "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==" 167 | }, 168 | "node_modules/accepts": { 169 | "version": "2.0.0", 170 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", 171 | "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", 172 | "dependencies": { 173 | "mime-types": "^3.0.0", 174 | "negotiator": "^1.0.0" 175 | }, 176 | "engines": { 177 | "node": ">= 0.6" 178 | } 179 | }, 180 | "node_modules/ajv": { 181 | "version": "6.12.6", 182 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", 183 | "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", 184 | "dependencies": { 185 | "fast-deep-equal": "^3.1.1", 186 | "fast-json-stable-stringify": "^2.0.0", 187 | "json-schema-traverse": "^0.4.1", 188 | "uri-js": "^4.2.2" 189 | }, 190 | "funding": { 191 | "type": "github", 192 | "url": "https://github.com/sponsors/epoberezkin" 193 | } 194 | }, 195 | "node_modules/body-parser": { 196 | "version": "2.2.0", 197 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", 198 | "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", 199 | "dependencies": { 200 | "bytes": "^3.1.2", 201 | "content-type": "^1.0.5", 202 | "debug": "^4.4.0", 203 | "http-errors": "^2.0.0", 204 | "iconv-lite": "^0.6.3", 205 | "on-finished": "^2.4.1", 206 | "qs": "^6.14.0", 207 | "raw-body": "^3.0.0", 208 | "type-is": "^2.0.0" 209 | }, 210 | "engines": { 211 | "node": ">=18" 212 | } 213 | }, 214 | "node_modules/buffer-equal-constant-time": { 215 | "version": "1.0.1", 216 | "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", 217 | "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" 218 | }, 219 | "node_modules/bytes": { 220 | "version": "3.1.2", 221 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", 222 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", 223 | "engines": { 224 | "node": ">= 0.8" 225 | } 226 | }, 227 | "node_modules/call-bind-apply-helpers": { 228 | "version": "1.0.2", 229 | "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", 230 | "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", 231 | "dependencies": { 232 | "es-errors": "^1.3.0", 233 | "function-bind": "^1.1.2" 234 | }, 235 | "engines": { 236 | "node": ">= 0.4" 237 | } 238 | }, 239 | "node_modules/call-bound": { 240 | "version": "1.0.4", 241 | "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", 242 | "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", 243 | "dependencies": { 244 | "call-bind-apply-helpers": "^1.0.2", 245 | "get-intrinsic": "^1.3.0" 246 | }, 247 | "engines": { 248 | "node": ">= 0.4" 249 | }, 250 | "funding": { 251 | "url": "https://github.com/sponsors/ljharb" 252 | } 253 | }, 254 | "node_modules/content-disposition": { 255 | "version": "1.0.0", 256 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", 257 | "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", 258 | "dependencies": { 259 | "safe-buffer": "5.2.1" 260 | }, 261 | "engines": { 262 | "node": ">= 0.6" 263 | } 264 | }, 265 | "node_modules/content-type": { 266 | "version": "1.0.5", 267 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", 268 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", 269 | "engines": { 270 | "node": ">= 0.6" 271 | } 272 | }, 273 | "node_modules/cookie": { 274 | "version": "0.7.2", 275 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", 276 | "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", 277 | "engines": { 278 | "node": ">= 0.6" 279 | } 280 | }, 281 | "node_modules/cookie-signature": { 282 | "version": "1.2.2", 283 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", 284 | "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", 285 | "engines": { 286 | "node": ">=6.6.0" 287 | } 288 | }, 289 | "node_modules/cors": { 290 | "version": "2.8.5", 291 | "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", 292 | "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", 293 | "dependencies": { 294 | "object-assign": "^4", 295 | "vary": "^1" 296 | }, 297 | "engines": { 298 | "node": ">= 0.10" 299 | } 300 | }, 301 | "node_modules/cross-spawn": { 302 | "version": "7.0.6", 303 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", 304 | "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", 305 | "dependencies": { 306 | "path-key": "^3.1.0", 307 | "shebang-command": "^2.0.0", 308 | "which": "^2.0.1" 309 | }, 310 | "engines": { 311 | "node": ">= 8" 312 | } 313 | }, 314 | "node_modules/data-uri-to-buffer": { 315 | "version": "4.0.1", 316 | "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", 317 | "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", 318 | "engines": { 319 | "node": ">= 12" 320 | } 321 | }, 322 | "node_modules/debug": { 323 | "version": "4.4.0", 324 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", 325 | "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", 326 | "dependencies": { 327 | "ms": "^2.1.3" 328 | }, 329 | "engines": { 330 | "node": ">=6.0" 331 | }, 332 | "peerDependenciesMeta": { 333 | "supports-color": { 334 | "optional": true 335 | } 336 | } 337 | }, 338 | "node_modules/depd": { 339 | "version": "2.0.0", 340 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 341 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", 342 | "engines": { 343 | "node": ">= 0.8" 344 | } 345 | }, 346 | "node_modules/dunder-proto": { 347 | "version": "1.0.1", 348 | "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", 349 | "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", 350 | "dependencies": { 351 | "call-bind-apply-helpers": "^1.0.1", 352 | "es-errors": "^1.3.0", 353 | "gopd": "^1.2.0" 354 | }, 355 | "engines": { 356 | "node": ">= 0.4" 357 | } 358 | }, 359 | "node_modules/ecdsa-sig-formatter": { 360 | "version": "1.0.11", 361 | "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", 362 | "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", 363 | "dependencies": { 364 | "safe-buffer": "^5.0.1" 365 | } 366 | }, 367 | "node_modules/ee-first": { 368 | "version": "1.1.1", 369 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 370 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" 371 | }, 372 | "node_modules/encodeurl": { 373 | "version": "2.0.0", 374 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", 375 | "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", 376 | "engines": { 377 | "node": ">= 0.8" 378 | } 379 | }, 380 | "node_modules/es-define-property": { 381 | "version": "1.0.1", 382 | "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", 383 | "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", 384 | "engines": { 385 | "node": ">= 0.4" 386 | } 387 | }, 388 | "node_modules/es-errors": { 389 | "version": "1.3.0", 390 | "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", 391 | "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", 392 | "engines": { 393 | "node": ">= 0.4" 394 | } 395 | }, 396 | "node_modules/es-object-atoms": { 397 | "version": "1.1.1", 398 | "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", 399 | "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", 400 | "dependencies": { 401 | "es-errors": "^1.3.0" 402 | }, 403 | "engines": { 404 | "node": ">= 0.4" 405 | } 406 | }, 407 | "node_modules/escape-html": { 408 | "version": "1.0.3", 409 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 410 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" 411 | }, 412 | "node_modules/etag": { 413 | "version": "1.8.1", 414 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 415 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", 416 | "engines": { 417 | "node": ">= 0.6" 418 | } 419 | }, 420 | "node_modules/eventsource": { 421 | "version": "3.0.7", 422 | "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", 423 | "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", 424 | "dependencies": { 425 | "eventsource-parser": "^3.0.1" 426 | }, 427 | "engines": { 428 | "node": ">=18.0.0" 429 | } 430 | }, 431 | "node_modules/eventsource-parser": { 432 | "version": "3.0.3", 433 | "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.3.tgz", 434 | "integrity": "sha512-nVpZkTMM9rF6AQ9gPJpFsNAMt48wIzB5TQgiTLdHiuO8XEDhUgZEhqKlZWXbIzo9VmJ/HvysHqEaVeD5v9TPvA==", 435 | "engines": { 436 | "node": ">=20.0.0" 437 | } 438 | }, 439 | "node_modules/express": { 440 | "version": "5.1.0", 441 | "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", 442 | "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", 443 | "dependencies": { 444 | "accepts": "^2.0.0", 445 | "body-parser": "^2.2.0", 446 | "content-disposition": "^1.0.0", 447 | "content-type": "^1.0.5", 448 | "cookie": "^0.7.1", 449 | "cookie-signature": "^1.2.1", 450 | "debug": "^4.4.0", 451 | "encodeurl": "^2.0.0", 452 | "escape-html": "^1.0.3", 453 | "etag": "^1.8.1", 454 | "finalhandler": "^2.1.0", 455 | "fresh": "^2.0.0", 456 | "http-errors": "^2.0.0", 457 | "merge-descriptors": "^2.0.0", 458 | "mime-types": "^3.0.0", 459 | "on-finished": "^2.4.1", 460 | "once": "^1.4.0", 461 | "parseurl": "^1.3.3", 462 | "proxy-addr": "^2.0.7", 463 | "qs": "^6.14.0", 464 | "range-parser": "^1.2.1", 465 | "router": "^2.2.0", 466 | "send": "^1.1.0", 467 | "serve-static": "^2.2.0", 468 | "statuses": "^2.0.1", 469 | "type-is": "^2.0.1", 470 | "vary": "^1.1.2" 471 | }, 472 | "engines": { 473 | "node": ">= 18" 474 | }, 475 | "funding": { 476 | "type": "opencollective", 477 | "url": "https://opencollective.com/express" 478 | } 479 | }, 480 | "node_modules/express-rate-limit": { 481 | "version": "7.5.1", 482 | "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", 483 | "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", 484 | "engines": { 485 | "node": ">= 16" 486 | }, 487 | "funding": { 488 | "url": "https://github.com/sponsors/express-rate-limit" 489 | }, 490 | "peerDependencies": { 491 | "express": ">= 4.11" 492 | } 493 | }, 494 | "node_modules/fast-deep-equal": { 495 | "version": "3.1.3", 496 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", 497 | "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" 498 | }, 499 | "node_modules/fast-json-stable-stringify": { 500 | "version": "2.1.0", 501 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", 502 | "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" 503 | }, 504 | "node_modules/fetch-blob": { 505 | "version": "3.2.0", 506 | "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", 507 | "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", 508 | "funding": [ 509 | { 510 | "type": "github", 511 | "url": "https://github.com/sponsors/jimmywarting" 512 | }, 513 | { 514 | "type": "paypal", 515 | "url": "https://paypal.me/jimmywarting" 516 | } 517 | ], 518 | "dependencies": { 519 | "node-domexception": "^1.0.0", 520 | "web-streams-polyfill": "^3.0.3" 521 | }, 522 | "engines": { 523 | "node": "^12.20 || >= 14.13" 524 | } 525 | }, 526 | "node_modules/finalhandler": { 527 | "version": "2.1.0", 528 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", 529 | "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", 530 | "dependencies": { 531 | "debug": "^4.4.0", 532 | "encodeurl": "^2.0.0", 533 | "escape-html": "^1.0.3", 534 | "on-finished": "^2.4.1", 535 | "parseurl": "^1.3.3", 536 | "statuses": "^2.0.1" 537 | }, 538 | "engines": { 539 | "node": ">= 0.8" 540 | } 541 | }, 542 | "node_modules/formdata-polyfill": { 543 | "version": "4.0.10", 544 | "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", 545 | "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", 546 | "dependencies": { 547 | "fetch-blob": "^3.1.2" 548 | }, 549 | "engines": { 550 | "node": ">=12.20.0" 551 | } 552 | }, 553 | "node_modules/forwarded": { 554 | "version": "0.2.0", 555 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", 556 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", 557 | "engines": { 558 | "node": ">= 0.6" 559 | } 560 | }, 561 | "node_modules/fresh": { 562 | "version": "2.0.0", 563 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", 564 | "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", 565 | "engines": { 566 | "node": ">= 0.8" 567 | } 568 | }, 569 | "node_modules/function-bind": { 570 | "version": "1.1.2", 571 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 572 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 573 | "funding": { 574 | "url": "https://github.com/sponsors/ljharb" 575 | } 576 | }, 577 | "node_modules/get-intrinsic": { 578 | "version": "1.3.0", 579 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", 580 | "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", 581 | "dependencies": { 582 | "call-bind-apply-helpers": "^1.0.2", 583 | "es-define-property": "^1.0.1", 584 | "es-errors": "^1.3.0", 585 | "es-object-atoms": "^1.1.1", 586 | "function-bind": "^1.1.2", 587 | "get-proto": "^1.0.1", 588 | "gopd": "^1.2.0", 589 | "has-symbols": "^1.1.0", 590 | "hasown": "^2.0.2", 591 | "math-intrinsics": "^1.1.0" 592 | }, 593 | "engines": { 594 | "node": ">= 0.4" 595 | }, 596 | "funding": { 597 | "url": "https://github.com/sponsors/ljharb" 598 | } 599 | }, 600 | "node_modules/get-proto": { 601 | "version": "1.0.1", 602 | "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", 603 | "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", 604 | "dependencies": { 605 | "dunder-proto": "^1.0.1", 606 | "es-object-atoms": "^1.0.0" 607 | }, 608 | "engines": { 609 | "node": ">= 0.4" 610 | } 611 | }, 612 | "node_modules/gopd": { 613 | "version": "1.2.0", 614 | "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", 615 | "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", 616 | "engines": { 617 | "node": ">= 0.4" 618 | }, 619 | "funding": { 620 | "url": "https://github.com/sponsors/ljharb" 621 | } 622 | }, 623 | "node_modules/has-symbols": { 624 | "version": "1.1.0", 625 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", 626 | "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", 627 | "engines": { 628 | "node": ">= 0.4" 629 | }, 630 | "funding": { 631 | "url": "https://github.com/sponsors/ljharb" 632 | } 633 | }, 634 | "node_modules/hasown": { 635 | "version": "2.0.2", 636 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", 637 | "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", 638 | "dependencies": { 639 | "function-bind": "^1.1.2" 640 | }, 641 | "engines": { 642 | "node": ">= 0.4" 643 | } 644 | }, 645 | "node_modules/http-errors": { 646 | "version": "2.0.0", 647 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", 648 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 649 | "dependencies": { 650 | "depd": "2.0.0", 651 | "inherits": "2.0.4", 652 | "setprototypeof": "1.2.0", 653 | "statuses": "2.0.1", 654 | "toidentifier": "1.0.1" 655 | }, 656 | "engines": { 657 | "node": ">= 0.8" 658 | } 659 | }, 660 | "node_modules/iconv-lite": { 661 | "version": "0.6.3", 662 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", 663 | "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", 664 | "dependencies": { 665 | "safer-buffer": ">= 2.1.2 < 3.0.0" 666 | }, 667 | "engines": { 668 | "node": ">=0.10.0" 669 | } 670 | }, 671 | "node_modules/inherits": { 672 | "version": "2.0.4", 673 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 674 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 675 | }, 676 | "node_modules/ipaddr.js": { 677 | "version": "1.9.1", 678 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 679 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", 680 | "engines": { 681 | "node": ">= 0.10" 682 | } 683 | }, 684 | "node_modules/is-promise": { 685 | "version": "4.0.0", 686 | "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", 687 | "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" 688 | }, 689 | "node_modules/isexe": { 690 | "version": "2.0.0", 691 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 692 | "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" 693 | }, 694 | "node_modules/jose": { 695 | "version": "6.0.11", 696 | "resolved": "https://registry.npmjs.org/jose/-/jose-6.0.11.tgz", 697 | "integrity": "sha512-QxG7EaliDARm1O1S8BGakqncGT9s25bKL1WSf6/oa17Tkqwi8D2ZNglqCF+DsYF88/rV66Q/Q2mFAy697E1DUg==", 698 | "funding": { 699 | "url": "https://github.com/sponsors/panva" 700 | } 701 | }, 702 | "node_modules/json-schema-traverse": { 703 | "version": "0.4.1", 704 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 705 | "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" 706 | }, 707 | "node_modules/jsonwebtoken": { 708 | "version": "9.0.2", 709 | "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", 710 | "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", 711 | "dependencies": { 712 | "jws": "^3.2.2", 713 | "lodash.includes": "^4.3.0", 714 | "lodash.isboolean": "^3.0.3", 715 | "lodash.isinteger": "^4.0.4", 716 | "lodash.isnumber": "^3.0.3", 717 | "lodash.isplainobject": "^4.0.6", 718 | "lodash.isstring": "^4.0.1", 719 | "lodash.once": "^4.0.0", 720 | "ms": "^2.1.1", 721 | "semver": "^7.5.4" 722 | }, 723 | "engines": { 724 | "node": ">=12", 725 | "npm": ">=6" 726 | } 727 | }, 728 | "node_modules/jwa": { 729 | "version": "1.4.1", 730 | "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", 731 | "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", 732 | "dependencies": { 733 | "buffer-equal-constant-time": "1.0.1", 734 | "ecdsa-sig-formatter": "1.0.11", 735 | "safe-buffer": "^5.0.1" 736 | } 737 | }, 738 | "node_modules/jws": { 739 | "version": "3.2.2", 740 | "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", 741 | "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", 742 | "dependencies": { 743 | "jwa": "^1.4.1", 744 | "safe-buffer": "^5.0.1" 745 | } 746 | }, 747 | "node_modules/lodash.includes": { 748 | "version": "4.3.0", 749 | "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", 750 | "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" 751 | }, 752 | "node_modules/lodash.isboolean": { 753 | "version": "3.0.3", 754 | "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", 755 | "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" 756 | }, 757 | "node_modules/lodash.isinteger": { 758 | "version": "4.0.4", 759 | "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", 760 | "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" 761 | }, 762 | "node_modules/lodash.isnumber": { 763 | "version": "3.0.3", 764 | "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", 765 | "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" 766 | }, 767 | "node_modules/lodash.isplainobject": { 768 | "version": "4.0.6", 769 | "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", 770 | "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" 771 | }, 772 | "node_modules/lodash.isstring": { 773 | "version": "4.0.1", 774 | "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", 775 | "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" 776 | }, 777 | "node_modules/lodash.once": { 778 | "version": "4.1.1", 779 | "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", 780 | "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" 781 | }, 782 | "node_modules/math-intrinsics": { 783 | "version": "1.1.0", 784 | "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", 785 | "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", 786 | "engines": { 787 | "node": ">= 0.4" 788 | } 789 | }, 790 | "node_modules/media-typer": { 791 | "version": "1.1.0", 792 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", 793 | "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", 794 | "engines": { 795 | "node": ">= 0.8" 796 | } 797 | }, 798 | "node_modules/merge-descriptors": { 799 | "version": "2.0.0", 800 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", 801 | "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", 802 | "engines": { 803 | "node": ">=18" 804 | }, 805 | "funding": { 806 | "url": "https://github.com/sponsors/sindresorhus" 807 | } 808 | }, 809 | "node_modules/mime-db": { 810 | "version": "1.54.0", 811 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", 812 | "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", 813 | "engines": { 814 | "node": ">= 0.6" 815 | } 816 | }, 817 | "node_modules/mime-types": { 818 | "version": "3.0.1", 819 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", 820 | "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", 821 | "dependencies": { 822 | "mime-db": "^1.54.0" 823 | }, 824 | "engines": { 825 | "node": ">= 0.6" 826 | } 827 | }, 828 | "node_modules/ms": { 829 | "version": "2.1.3", 830 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 831 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 832 | }, 833 | "node_modules/negotiator": { 834 | "version": "1.0.0", 835 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", 836 | "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", 837 | "engines": { 838 | "node": ">= 0.6" 839 | } 840 | }, 841 | "node_modules/node-domexception": { 842 | "version": "1.0.0", 843 | "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", 844 | "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", 845 | "deprecated": "Use your platform's native DOMException instead", 846 | "funding": [ 847 | { 848 | "type": "github", 849 | "url": "https://github.com/sponsors/jimmywarting" 850 | }, 851 | { 852 | "type": "github", 853 | "url": "https://paypal.me/jimmywarting" 854 | } 855 | ], 856 | "engines": { 857 | "node": ">=10.5.0" 858 | } 859 | }, 860 | "node_modules/node-fetch": { 861 | "version": "3.3.2", 862 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", 863 | "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", 864 | "dependencies": { 865 | "data-uri-to-buffer": "^4.0.0", 866 | "fetch-blob": "^3.1.4", 867 | "formdata-polyfill": "^4.0.10" 868 | }, 869 | "engines": { 870 | "node": "^12.20.0 || ^14.13.1 || >=16.0.0" 871 | }, 872 | "funding": { 873 | "type": "opencollective", 874 | "url": "https://opencollective.com/node-fetch" 875 | } 876 | }, 877 | "node_modules/object-assign": { 878 | "version": "4.1.1", 879 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 880 | "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", 881 | "engines": { 882 | "node": ">=0.10.0" 883 | } 884 | }, 885 | "node_modules/object-inspect": { 886 | "version": "1.13.4", 887 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", 888 | "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", 889 | "engines": { 890 | "node": ">= 0.4" 891 | }, 892 | "funding": { 893 | "url": "https://github.com/sponsors/ljharb" 894 | } 895 | }, 896 | "node_modules/on-finished": { 897 | "version": "2.4.1", 898 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", 899 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", 900 | "dependencies": { 901 | "ee-first": "1.1.1" 902 | }, 903 | "engines": { 904 | "node": ">= 0.8" 905 | } 906 | }, 907 | "node_modules/once": { 908 | "version": "1.4.0", 909 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 910 | "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", 911 | "dependencies": { 912 | "wrappy": "1" 913 | } 914 | }, 915 | "node_modules/parseurl": { 916 | "version": "1.3.3", 917 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 918 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", 919 | "engines": { 920 | "node": ">= 0.8" 921 | } 922 | }, 923 | "node_modules/path-key": { 924 | "version": "3.1.1", 925 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", 926 | "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", 927 | "engines": { 928 | "node": ">=8" 929 | } 930 | }, 931 | "node_modules/path-to-regexp": { 932 | "version": "8.2.0", 933 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", 934 | "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", 935 | "engines": { 936 | "node": ">=16" 937 | } 938 | }, 939 | "node_modules/pkce-challenge": { 940 | "version": "5.0.0", 941 | "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", 942 | "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", 943 | "engines": { 944 | "node": ">=16.20.0" 945 | } 946 | }, 947 | "node_modules/proxy-addr": { 948 | "version": "2.0.7", 949 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", 950 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", 951 | "dependencies": { 952 | "forwarded": "0.2.0", 953 | "ipaddr.js": "1.9.1" 954 | }, 955 | "engines": { 956 | "node": ">= 0.10" 957 | } 958 | }, 959 | "node_modules/punycode": { 960 | "version": "2.3.1", 961 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", 962 | "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", 963 | "engines": { 964 | "node": ">=6" 965 | } 966 | }, 967 | "node_modules/qs": { 968 | "version": "6.14.0", 969 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", 970 | "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", 971 | "dependencies": { 972 | "side-channel": "^1.1.0" 973 | }, 974 | "engines": { 975 | "node": ">=0.6" 976 | }, 977 | "funding": { 978 | "url": "https://github.com/sponsors/ljharb" 979 | } 980 | }, 981 | "node_modules/range-parser": { 982 | "version": "1.2.1", 983 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 984 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", 985 | "engines": { 986 | "node": ">= 0.6" 987 | } 988 | }, 989 | "node_modules/raw-body": { 990 | "version": "3.0.0", 991 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", 992 | "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", 993 | "dependencies": { 994 | "bytes": "3.1.2", 995 | "http-errors": "2.0.0", 996 | "iconv-lite": "0.6.3", 997 | "unpipe": "1.0.0" 998 | }, 999 | "engines": { 1000 | "node": ">= 0.8" 1001 | } 1002 | }, 1003 | "node_modules/router": { 1004 | "version": "2.2.0", 1005 | "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", 1006 | "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", 1007 | "dependencies": { 1008 | "debug": "^4.4.0", 1009 | "depd": "^2.0.0", 1010 | "is-promise": "^4.0.0", 1011 | "parseurl": "^1.3.3", 1012 | "path-to-regexp": "^8.0.0" 1013 | }, 1014 | "engines": { 1015 | "node": ">= 18" 1016 | } 1017 | }, 1018 | "node_modules/safe-buffer": { 1019 | "version": "5.2.1", 1020 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 1021 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 1022 | "funding": [ 1023 | { 1024 | "type": "github", 1025 | "url": "https://github.com/sponsors/feross" 1026 | }, 1027 | { 1028 | "type": "patreon", 1029 | "url": "https://www.patreon.com/feross" 1030 | }, 1031 | { 1032 | "type": "consulting", 1033 | "url": "https://feross.org/support" 1034 | } 1035 | ] 1036 | }, 1037 | "node_modules/safer-buffer": { 1038 | "version": "2.1.2", 1039 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 1040 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 1041 | }, 1042 | "node_modules/semver": { 1043 | "version": "7.7.1", 1044 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", 1045 | "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", 1046 | "bin": { 1047 | "semver": "bin/semver.js" 1048 | }, 1049 | "engines": { 1050 | "node": ">=10" 1051 | } 1052 | }, 1053 | "node_modules/send": { 1054 | "version": "1.2.0", 1055 | "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", 1056 | "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", 1057 | "dependencies": { 1058 | "debug": "^4.3.5", 1059 | "encodeurl": "^2.0.0", 1060 | "escape-html": "^1.0.3", 1061 | "etag": "^1.8.1", 1062 | "fresh": "^2.0.0", 1063 | "http-errors": "^2.0.0", 1064 | "mime-types": "^3.0.1", 1065 | "ms": "^2.1.3", 1066 | "on-finished": "^2.4.1", 1067 | "range-parser": "^1.2.1", 1068 | "statuses": "^2.0.1" 1069 | }, 1070 | "engines": { 1071 | "node": ">= 18" 1072 | } 1073 | }, 1074 | "node_modules/serve-static": { 1075 | "version": "2.2.0", 1076 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", 1077 | "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", 1078 | "dependencies": { 1079 | "encodeurl": "^2.0.0", 1080 | "escape-html": "^1.0.3", 1081 | "parseurl": "^1.3.3", 1082 | "send": "^1.2.0" 1083 | }, 1084 | "engines": { 1085 | "node": ">= 18" 1086 | } 1087 | }, 1088 | "node_modules/setprototypeof": { 1089 | "version": "1.2.0", 1090 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 1091 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" 1092 | }, 1093 | "node_modules/shebang-command": { 1094 | "version": "2.0.0", 1095 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", 1096 | "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 1097 | "dependencies": { 1098 | "shebang-regex": "^3.0.0" 1099 | }, 1100 | "engines": { 1101 | "node": ">=8" 1102 | } 1103 | }, 1104 | "node_modules/shebang-regex": { 1105 | "version": "3.0.0", 1106 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", 1107 | "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", 1108 | "engines": { 1109 | "node": ">=8" 1110 | } 1111 | }, 1112 | "node_modules/side-channel": { 1113 | "version": "1.1.0", 1114 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", 1115 | "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", 1116 | "dependencies": { 1117 | "es-errors": "^1.3.0", 1118 | "object-inspect": "^1.13.3", 1119 | "side-channel-list": "^1.0.0", 1120 | "side-channel-map": "^1.0.1", 1121 | "side-channel-weakmap": "^1.0.2" 1122 | }, 1123 | "engines": { 1124 | "node": ">= 0.4" 1125 | }, 1126 | "funding": { 1127 | "url": "https://github.com/sponsors/ljharb" 1128 | } 1129 | }, 1130 | "node_modules/side-channel-list": { 1131 | "version": "1.0.0", 1132 | "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", 1133 | "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", 1134 | "dependencies": { 1135 | "es-errors": "^1.3.0", 1136 | "object-inspect": "^1.13.3" 1137 | }, 1138 | "engines": { 1139 | "node": ">= 0.4" 1140 | }, 1141 | "funding": { 1142 | "url": "https://github.com/sponsors/ljharb" 1143 | } 1144 | }, 1145 | "node_modules/side-channel-map": { 1146 | "version": "1.0.1", 1147 | "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", 1148 | "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", 1149 | "dependencies": { 1150 | "call-bound": "^1.0.2", 1151 | "es-errors": "^1.3.0", 1152 | "get-intrinsic": "^1.2.5", 1153 | "object-inspect": "^1.13.3" 1154 | }, 1155 | "engines": { 1156 | "node": ">= 0.4" 1157 | }, 1158 | "funding": { 1159 | "url": "https://github.com/sponsors/ljharb" 1160 | } 1161 | }, 1162 | "node_modules/side-channel-weakmap": { 1163 | "version": "1.0.2", 1164 | "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", 1165 | "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", 1166 | "dependencies": { 1167 | "call-bound": "^1.0.2", 1168 | "es-errors": "^1.3.0", 1169 | "get-intrinsic": "^1.2.5", 1170 | "object-inspect": "^1.13.3", 1171 | "side-channel-map": "^1.0.1" 1172 | }, 1173 | "engines": { 1174 | "node": ">= 0.4" 1175 | }, 1176 | "funding": { 1177 | "url": "https://github.com/sponsors/ljharb" 1178 | } 1179 | }, 1180 | "node_modules/statuses": { 1181 | "version": "2.0.1", 1182 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", 1183 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", 1184 | "engines": { 1185 | "node": ">= 0.8" 1186 | } 1187 | }, 1188 | "node_modules/toidentifier": { 1189 | "version": "1.0.1", 1190 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 1191 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", 1192 | "engines": { 1193 | "node": ">=0.6" 1194 | } 1195 | }, 1196 | "node_modules/type-is": { 1197 | "version": "2.0.1", 1198 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", 1199 | "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", 1200 | "dependencies": { 1201 | "content-type": "^1.0.5", 1202 | "media-typer": "^1.1.0", 1203 | "mime-types": "^3.0.0" 1204 | }, 1205 | "engines": { 1206 | "node": ">= 0.6" 1207 | } 1208 | }, 1209 | "node_modules/typescript": { 1210 | "version": "5.8.3", 1211 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", 1212 | "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", 1213 | "dev": true, 1214 | "bin": { 1215 | "tsc": "bin/tsc", 1216 | "tsserver": "bin/tsserver" 1217 | }, 1218 | "engines": { 1219 | "node": ">=14.17" 1220 | } 1221 | }, 1222 | "node_modules/undici-types": { 1223 | "version": "6.21.0", 1224 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", 1225 | "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" 1226 | }, 1227 | "node_modules/unpipe": { 1228 | "version": "1.0.0", 1229 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 1230 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", 1231 | "engines": { 1232 | "node": ">= 0.8" 1233 | } 1234 | }, 1235 | "node_modules/uri-js": { 1236 | "version": "4.4.1", 1237 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", 1238 | "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", 1239 | "dependencies": { 1240 | "punycode": "^2.1.0" 1241 | } 1242 | }, 1243 | "node_modules/uuid": { 1244 | "version": "11.1.0", 1245 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", 1246 | "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", 1247 | "funding": [ 1248 | "https://github.com/sponsors/broofa", 1249 | "https://github.com/sponsors/ctavan" 1250 | ], 1251 | "bin": { 1252 | "uuid": "dist/esm/bin/uuid" 1253 | } 1254 | }, 1255 | "node_modules/vary": { 1256 | "version": "1.1.2", 1257 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 1258 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", 1259 | "engines": { 1260 | "node": ">= 0.8" 1261 | } 1262 | }, 1263 | "node_modules/web-streams-polyfill": { 1264 | "version": "3.3.3", 1265 | "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", 1266 | "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", 1267 | "engines": { 1268 | "node": ">= 8" 1269 | } 1270 | }, 1271 | "node_modules/which": { 1272 | "version": "2.0.2", 1273 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 1274 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 1275 | "dependencies": { 1276 | "isexe": "^2.0.0" 1277 | }, 1278 | "bin": { 1279 | "node-which": "bin/node-which" 1280 | }, 1281 | "engines": { 1282 | "node": ">= 8" 1283 | } 1284 | }, 1285 | "node_modules/wrappy": { 1286 | "version": "1.0.2", 1287 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 1288 | "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" 1289 | }, 1290 | "node_modules/zod": { 1291 | "version": "3.24.4", 1292 | "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.4.tgz", 1293 | "integrity": "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==", 1294 | "funding": { 1295 | "url": "https://github.com/sponsors/colinhacks" 1296 | } 1297 | }, 1298 | "node_modules/zod-to-json-schema": { 1299 | "version": "3.24.6", 1300 | "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz", 1301 | "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", 1302 | "peerDependencies": { 1303 | "zod": "^3.24.1" 1304 | } 1305 | } 1306 | } 1307 | } 1308 | --------------------------------------------------------------------------------