From e69141857fa65db08f2343dfb3023b8e3e34f6f9 Mon Sep 17 00:00:00 2001 From: Nithin Chandran Rajashankar Date: Sun, 26 Apr 2026 15:47:11 +0000 Subject: [PATCH 1/2] feat: add Lambda MCP Server with Bedrock pattern --- lambda-mcp-server-bedrock-cdk/.gitignore | 5 + lambda-mcp-server-bedrock-cdk/.npmignore | 2 + lambda-mcp-server-bedrock-cdk/README.md | 102 ++++++++++++++++ lambda-mcp-server-bedrock-cdk/bin/app.ts | 6 + lambda-mcp-server-bedrock-cdk/cdk.json | 3 + .../example-pattern.json | 38 ++++++ .../lib/lambda-mcp-server-bedrock-stack.ts | 45 +++++++ lambda-mcp-server-bedrock-cdk/package.json | 21 ++++ lambda-mcp-server-bedrock-cdk/src/index.js | 115 ++++++++++++++++++ lambda-mcp-server-bedrock-cdk/tsconfig.json | 24 ++++ 10 files changed, 361 insertions(+) create mode 100644 lambda-mcp-server-bedrock-cdk/.gitignore create mode 100644 lambda-mcp-server-bedrock-cdk/.npmignore create mode 100644 lambda-mcp-server-bedrock-cdk/README.md create mode 100644 lambda-mcp-server-bedrock-cdk/bin/app.ts create mode 100644 lambda-mcp-server-bedrock-cdk/cdk.json create mode 100644 lambda-mcp-server-bedrock-cdk/example-pattern.json create mode 100644 lambda-mcp-server-bedrock-cdk/lib/lambda-mcp-server-bedrock-stack.ts create mode 100644 lambda-mcp-server-bedrock-cdk/package.json create mode 100644 lambda-mcp-server-bedrock-cdk/src/index.js create mode 100644 lambda-mcp-server-bedrock-cdk/tsconfig.json diff --git a/lambda-mcp-server-bedrock-cdk/.gitignore b/lambda-mcp-server-bedrock-cdk/.gitignore new file mode 100644 index 000000000..ffa11f083 --- /dev/null +++ b/lambda-mcp-server-bedrock-cdk/.gitignore @@ -0,0 +1,5 @@ +node_modules +build +cdk.out +*.js +*.d.ts diff --git a/lambda-mcp-server-bedrock-cdk/.npmignore b/lambda-mcp-server-bedrock-cdk/.npmignore new file mode 100644 index 000000000..783d56649 --- /dev/null +++ b/lambda-mcp-server-bedrock-cdk/.npmignore @@ -0,0 +1,2 @@ +build +cdk.out diff --git a/lambda-mcp-server-bedrock-cdk/README.md b/lambda-mcp-server-bedrock-cdk/README.md new file mode 100644 index 000000000..feb00be15 --- /dev/null +++ b/lambda-mcp-server-bedrock-cdk/README.md @@ -0,0 +1,102 @@ +# Lambda MCP Server with Amazon Bedrock + +This pattern deploys a stateless [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) server on AWS Lambda with a Function URL. The server exposes Bedrock-powered tools that any MCP client can use. + +Learn more about this pattern at Serverless Land Patterns: https://serverlessland.com/patterns/lambda-mcp-server-bedrock-cdk + +Important: this application uses various AWS services and there are costs associated with these services after the Free Tier usage - please see the [AWS Pricing page](https://aws.amazon.com/pricing/) for details. You are responsible for any AWS costs incurred. No warranty is implied in this example. + +## Requirements + +* [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one and log in. +* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured +* [Node and NPM](https://nodejs.org/en/download/) installed +* [AWS CDK](https://docs.aws.amazon.com/cdk/latest/guide/cli.html) installed + +## How it works + +MCP (Model Context Protocol) is an open standard for connecting AI assistants to external tools and data sources. This pattern deploys a serverless MCP server that: + +- **Runs on Lambda** with a Function URL — no API Gateway needed +- **Implements MCP protocol** (JSON-RPC 2.0 over HTTP) with `initialize`, `tools/list`, and `tools/call` +- **Exposes two tools**: `ask_bedrock` (general Q&A) and `summarize` (text summarization) +- **Stateless design** — scales horizontally with no session management + +``` +MCP Client (Claude Desktop / Kiro / VS Code) + ↓ HTTP POST (JSON-RPC) +Lambda Function URL + ↓ tools/call +Amazon Bedrock (Claude) → Response +``` + +## Available MCP Tools + +| Tool | Description | Input | +|------|-------------|-------| +| `ask_bedrock` | Ask Bedrock a question | `prompt` (string), `max_tokens` (number, optional) | +| `summarize` | Summarize text | `text` (string) | + +## Deployment Instructions + +1. Clone the repository and navigate to the pattern directory: + ```bash + git clone https://github.com/aws-samples/serverless-patterns + cd serverless-patterns/lambda-mcp-server-bedrock-cdk + ``` + +2. Install dependencies: + ```bash + npm install + ``` + +3. Deploy the stack: + ```bash + cdk deploy + ``` + +## Testing + +Test the MCP server directly with curl: + +```bash +# Initialize +curl -X POST \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' + +# List tools +curl -X POST \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' + +# Call ask_bedrock tool +curl -X POST \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"ask_bedrock","arguments":{"prompt":"What is MCP?"}}}' +``` + +## Connecting MCP Clients + +Add to your MCP client configuration (e.g., Claude Desktop `claude_desktop_config.json`): + +```json +{ + "mcpServers": { + "lambda-bedrock": { + "url": "" + } + } +} +``` + +## Cleanup + +```bash +cdk destroy +``` + +---- +Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: MIT-0 diff --git a/lambda-mcp-server-bedrock-cdk/bin/app.ts b/lambda-mcp-server-bedrock-cdk/bin/app.ts new file mode 100644 index 000000000..20479b7b6 --- /dev/null +++ b/lambda-mcp-server-bedrock-cdk/bin/app.ts @@ -0,0 +1,6 @@ +#!/usr/bin/env node +import * as cdk from 'aws-cdk-lib'; +import { LambdaMcpServerBedrockStack } from '../lib/lambda-mcp-server-bedrock-stack'; + +const app = new cdk.App(); +new LambdaMcpServerBedrockStack(app, 'LambdaMcpServerBedrockStack'); diff --git a/lambda-mcp-server-bedrock-cdk/cdk.json b/lambda-mcp-server-bedrock-cdk/cdk.json new file mode 100644 index 000000000..27fe6d2ec --- /dev/null +++ b/lambda-mcp-server-bedrock-cdk/cdk.json @@ -0,0 +1,3 @@ +{ + "app": "npx ts-node bin/app.ts" +} diff --git a/lambda-mcp-server-bedrock-cdk/example-pattern.json b/lambda-mcp-server-bedrock-cdk/example-pattern.json new file mode 100644 index 000000000..120b10ed9 --- /dev/null +++ b/lambda-mcp-server-bedrock-cdk/example-pattern.json @@ -0,0 +1,38 @@ +{ + "title": "Lambda MCP Server with Amazon Bedrock", + "description": "Deploy a stateless MCP (Model Context Protocol) server on Lambda with Function URL that exposes Bedrock tools", + "language": "TypeScript", + "level": "300", + "framework": "CDK", + "introBox": { + "headline": "How it works", + "text": [ + "This pattern deploys a stateless MCP server on AWS Lambda with a Function URL endpoint.", + "The server implements the MCP Streamable HTTP transport protocol and exposes two Bedrock-powered tools: ask_bedrock and summarize.", + "Any MCP client (Claude Desktop, Kiro, VS Code extensions) can connect to the Function URL and use the tools to interact with Amazon Bedrock." + ] + }, + "gitHub": { + "template": "https://github.com/aws-samples/serverless-patterns/tree/main/lambda-mcp-server-bedrock-cdk", + "templateURL": "serverless-patterns/lambda-mcp-server-bedrock-cdk" + }, + "resources": { + "bullets": [ + { "text": "MCP Specification - Streamable HTTP Transport", "link": "https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http" }, + { "text": "Sample Serverless MCP Servers", "link": "https://github.com/aws-samples/sample-serverless-mcp-servers" } + ] + }, + "deploy": { + "text": ["cdk deploy"] + }, + "cleanup": { + "text": ["cdk destroy"] + }, + "authors": [ + { + "name": "Nithin Chandran R", + "bio": "Technical Account Manager at AWS", + "linkedin": "nithin-chandran-r" + } + ] +} diff --git a/lambda-mcp-server-bedrock-cdk/lib/lambda-mcp-server-bedrock-stack.ts b/lambda-mcp-server-bedrock-cdk/lib/lambda-mcp-server-bedrock-stack.ts new file mode 100644 index 000000000..8318c16bd --- /dev/null +++ b/lambda-mcp-server-bedrock-cdk/lib/lambda-mcp-server-bedrock-stack.ts @@ -0,0 +1,45 @@ +import * as cdk from 'aws-cdk-lib'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import { Construct } from 'constructs'; + +export class LambdaMcpServerBedrockStack extends cdk.Stack { + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + const modelId = new cdk.CfnParameter(this, 'BedrockModelId', { + type: 'String', + default: 'us.anthropic.claude-sonnet-4-20250514-v1:0', + description: 'Bedrock model ID (inference profile)', + }); + + // MCP Server Lambda with Function URL (Streamable HTTP transport) + const mcpFn = new lambda.Function(this, 'McpServerFunction', { + runtime: lambda.Runtime.NODEJS_22_X, + handler: 'index.handler', + code: lambda.Code.fromAsset('src'), + timeout: cdk.Duration.seconds(60), + memorySize: 256, + environment: { + MODEL_ID: modelId.valueAsString, + }, + }); + + mcpFn.addToRolePolicy(new iam.PolicyStatement({ + actions: ['bedrock:InvokeModel'], + resources: ['*'], + })); + + // Function URL for MCP Streamable HTTP transport + const fnUrl = mcpFn.addFunctionUrl({ + authType: lambda.FunctionUrlAuthType.NONE, + invokeMode: lambda.InvokeMode.BUFFERED, + }); + + new cdk.CfnOutput(this, 'McpServerUrl', { value: fnUrl.url }); + new cdk.CfnOutput(this, 'McpEndpoint', { + value: cdk.Fn.join('', [fnUrl.url, 'mcp']), + description: 'MCP endpoint URL for client configuration', + }); + } +} diff --git a/lambda-mcp-server-bedrock-cdk/package.json b/lambda-mcp-server-bedrock-cdk/package.json new file mode 100644 index 000000000..d7d659e4e --- /dev/null +++ b/lambda-mcp-server-bedrock-cdk/package.json @@ -0,0 +1,21 @@ +{ + "name": "lambda-mcp-server-bedrock-cdk", + "version": "0.1.0", + "bin": { + "lambda-mcp-server-bedrock-cdk": "bin/app.js" + }, + "scripts": { + "build": "tsc", + "cdk": "cdk" + }, + "devDependencies": { + "@types/node": "22.7.9", + "aws-cdk": "2.1003.0", + "ts-node": "^10.9.2", + "typescript": "~5.6.3" + }, + "dependencies": { + "aws-cdk-lib": "2.189.1", + "constructs": "^10.0.0" + } +} diff --git a/lambda-mcp-server-bedrock-cdk/src/index.js b/lambda-mcp-server-bedrock-cdk/src/index.js new file mode 100644 index 000000000..6c70bf5a1 --- /dev/null +++ b/lambda-mcp-server-bedrock-cdk/src/index.js @@ -0,0 +1,115 @@ +const { BedrockRuntimeClient, InvokeModelCommand } = require('@aws-sdk/client-bedrock-runtime'); + +const client = new BedrockRuntimeClient(); + +// Stateless MCP Server on Lambda — implements MCP Streamable HTTP transport +// Supports: tools/list, tools/call, initialize +exports.handler = async (event) => { + const method = event.requestContext?.http?.method || 'GET'; + const path = event.rawPath || '/'; + + // Health check + if (method === 'GET' && path === '/') { + return { statusCode: 200, body: JSON.stringify({ status: 'ok', server: 'lambda-mcp-bedrock' }) }; + } + + // MCP endpoint + if (method === 'POST') { + const body = JSON.parse(event.body || '{}'); + const response = await handleMcpRequest(body); + return { + statusCode: 200, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(response), + }; + } + + return { statusCode: 405, body: 'Method not allowed' }; +}; + +async function handleMcpRequest(request) { + const { method, id, params } = request; + + if (method === 'initialize') { + return { + jsonrpc: '2.0', id, + result: { + protocolVersion: '2025-03-26', + capabilities: { tools: { listChanged: false } }, + serverInfo: { name: 'lambda-bedrock-mcp', version: '1.0.0' }, + }, + }; + } + + if (method === 'tools/list') { + return { + jsonrpc: '2.0', id, + result: { + tools: [ + { + name: 'ask_bedrock', + description: 'Ask Amazon Bedrock (Claude) a question and get an AI-generated response', + inputSchema: { + type: 'object', + properties: { + prompt: { type: 'string', description: 'The question or prompt to send to Bedrock' }, + max_tokens: { type: 'number', description: 'Maximum tokens in response', default: 512 }, + }, + required: ['prompt'], + }, + }, + { + name: 'summarize', + description: 'Summarize text using Amazon Bedrock (Claude)', + inputSchema: { + type: 'object', + properties: { + text: { type: 'string', description: 'The text to summarize' }, + }, + required: ['text'], + }, + }, + ], + }, + }; + } + + if (method === 'tools/call') { + const toolName = params?.name; + const args = params?.arguments || {}; + + if (toolName === 'ask_bedrock') { + const answer = await invokeBedrock(args.prompt, args.max_tokens || 512); + return { jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: answer }] } }; + } + + if (toolName === 'summarize') { + const summary = await invokeBedrock(`Summarize the following text concisely:\n\n${args.text}`, 512); + return { jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: summary }] } }; + } + + return { jsonrpc: '2.0', id, error: { code: -32601, message: `Unknown tool: ${toolName}` } }; + } + + // notifications/initialized — acknowledge + if (method === 'notifications/initialized') { + return null; + } + + return { jsonrpc: '2.0', id, error: { code: -32601, message: `Unknown method: ${method}` } }; +} + +async function invokeBedrock(prompt, maxTokens) { + const response = await client.send(new InvokeModelCommand({ + modelId: process.env.MODEL_ID, + contentType: 'application/json', + accept: 'application/json', + body: JSON.stringify({ + anthropic_version: 'bedrock-2023-05-31', + max_tokens: maxTokens, + messages: [{ role: 'user', content: prompt }], + }), + })); + const body = JSON.parse(new TextDecoder().decode(response.body)); + return body.content[0].text; +} diff --git a/lambda-mcp-server-bedrock-cdk/tsconfig.json b/lambda-mcp-server-bedrock-cdk/tsconfig.json new file mode 100644 index 000000000..7ddcfe705 --- /dev/null +++ b/lambda-mcp-server-bedrock-cdk/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["es2020"], + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": false, + "inlineSourceMap": true, + "inlineSources": true, + "experimentalDecorators": true, + "strictPropertyInitialization": false, + "outDir": "./build", + "rootDir": "." + }, + "exclude": ["node_modules", "build"] +} From 02bb2878947155b96381af8b6e5c8bd158c1dd9b Mon Sep 17 00:00:00 2001 From: Nithin Chandran Rajashankar Date: Sun, 26 Apr 2026 15:50:59 +0000 Subject: [PATCH 2/2] fix: use AWS_IAM auth for Function URL instead of NONE Palisade flagged world-accessible Lambda. Changed Function URL auth from NONE to AWS_IAM for security. Updated README with awscurl examples for SigV4 signed requests. --- lambda-mcp-server-bedrock-cdk/README.md | 12 ++++++------ lambda-mcp-server-bedrock-cdk/cdk.context.json | 12 ++++++++++++ .../lib/lambda-mcp-server-bedrock-stack.ts | 2 +- 3 files changed, 19 insertions(+), 7 deletions(-) create mode 100644 lambda-mcp-server-bedrock-cdk/cdk.context.json diff --git a/lambda-mcp-server-bedrock-cdk/README.md b/lambda-mcp-server-bedrock-cdk/README.md index feb00be15..074cb3bb2 100644 --- a/lambda-mcp-server-bedrock-cdk/README.md +++ b/lambda-mcp-server-bedrock-cdk/README.md @@ -17,7 +17,7 @@ Important: this application uses various AWS services and there are costs associ MCP (Model Context Protocol) is an open standard for connecting AI assistants to external tools and data sources. This pattern deploys a serverless MCP server that: -- **Runs on Lambda** with a Function URL — no API Gateway needed +- **Runs on Lambda** with a Function URL (IAM auth) — no API Gateway needed - **Implements MCP protocol** (JSON-RPC 2.0 over HTTP) with `initialize`, `tools/list`, and `tools/call` - **Exposes two tools**: `ask_bedrock` (general Q&A) and `summarize` (text summarization) - **Stateless design** — scales horizontally with no session management @@ -57,21 +57,21 @@ Amazon Bedrock (Claude) → Response ## Testing -Test the MCP server directly with curl: +Test the MCP server directly with curl (requires [AWS SigV4 signing](https://docs.aws.amazon.com/general/latest/gr/sigv4-signed-request-examples.html)): ```bash -# Initialize -curl -X POST \ +# Initialize (using awscurl for SigV4) +awscurl --service lambda -X POST \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' # List tools -curl -X POST \ +awscurl --service lambda -X POST \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' # Call ask_bedrock tool -curl -X POST \ +awscurl --service lambda -X POST \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"ask_bedrock","arguments":{"prompt":"What is MCP?"}}}' ``` diff --git a/lambda-mcp-server-bedrock-cdk/cdk.context.json b/lambda-mcp-server-bedrock-cdk/cdk.context.json new file mode 100644 index 000000000..88a3e2851 --- /dev/null +++ b/lambda-mcp-server-bedrock-cdk/cdk.context.json @@ -0,0 +1,12 @@ +{ + "acknowledged-issue-numbers": [ + 33623, + 34635, + 34892, + 32775, + 33623, + 34635, + 34892, + 32775 + ] +} diff --git a/lambda-mcp-server-bedrock-cdk/lib/lambda-mcp-server-bedrock-stack.ts b/lambda-mcp-server-bedrock-cdk/lib/lambda-mcp-server-bedrock-stack.ts index 8318c16bd..eb6bf61b5 100644 --- a/lambda-mcp-server-bedrock-cdk/lib/lambda-mcp-server-bedrock-stack.ts +++ b/lambda-mcp-server-bedrock-cdk/lib/lambda-mcp-server-bedrock-stack.ts @@ -32,7 +32,7 @@ export class LambdaMcpServerBedrockStack extends cdk.Stack { // Function URL for MCP Streamable HTTP transport const fnUrl = mcpFn.addFunctionUrl({ - authType: lambda.FunctionUrlAuthType.NONE, + authType: lambda.FunctionUrlAuthType.AWS_IAM, invokeMode: lambda.InvokeMode.BUFFERED, });