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..074cb3bb2 --- /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 (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 + +``` +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 (requires [AWS SigV4 signing](https://docs.aws.amazon.com/general/latest/gr/sigv4-signed-request-examples.html)): + +```bash +# 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 +awscurl --service lambda -X POST \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' + +# Call ask_bedrock tool +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?"}}}' +``` + +## 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.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/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..eb6bf61b5 --- /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.AWS_IAM, + 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"] +}