Skip to content

Commit f13e05a

Browse files
committed
feat(mcp): add stateless mcp on ecs sample
1 parent 26cd81d commit f13e05a

24 files changed

+7201
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ This repo provides samples to demonstrate how to build your own Generative AI so
2222
| [Code Expert](samples/code-expert/) | This project addresses the scalability limitations of manual code reviews by leveraging artificial intelligence to perform expert code reviews automatically. It leverages the [Bedrock Batch Step Functions CDK construct](https://github.com/awslabs/generative-ai-cdk-constructs/blob/main/src/patterns/gen-ai/aws-bedrock-batch-stepfn/README.md). | Backend | Python for Backend and Demo, TypeScript for CDK |
2323
|[Bedrock Agent UI Wrapper](samples/bedrock-agent-ui-wrapper/)| This sample provides a CDK construct that creates an API layer and frontend application for Amazon Bedrock Agents. It includes authentication with Amazon Cognito, agent trace streaming, and can be deployed locally or on ECS Fargate. | API layer + Frontend | Python|
2424
|[Stateless MCP Server on AWS Lambda](samples/mcp-stateless-lambda/)| Sample MCP Server running natively on AWS Lambda and API Gateway without any extra bridging components or custom transports and a test MCP client. | API layer | TypeScript |
25+
|[Stateless MCP Server on ECS](samples/mcp-stateless-ecs/)| Sample MCP Server running natively on ECS Fargate and ALB without any extra bridging components or custom transports and a test MCP client. | API layer | TypeScript |
2526

2627
## Contributing
2728

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
!jest.config.js
2+
*.d.ts
3+
node_modules
4+
5+
# CDK asset staging directory
6+
.cdk.staging
7+
cdk.out
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
*.ts
2+
!*.d.ts
3+
4+
# CDK asset staging directory
5+
.cdk.staging
6+
cdk.out
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
# Stateless MCP Server on ECS Fargate
2+
3+
This is a sample MCP Server running natively on on ECS Fargate and ALB without any extra bridging components or custom transports. This is now possible thanks to the [Streamable HTTP transport](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http) introduced in v2025-03-26.
4+
5+
The solution provides the CDK code to deploy the server on AWS, as well as a sample client to test the deployed server.
6+
7+
The original Terraform version of this sample is available [here](https://github.com/aws-samples/sample-serverless-mcp-servers/tree/main/stateless-mcp-on-lambda-nodejs)
8+
9+
## Overview
10+
11+
MCP Server can run in two modes - stateless and stateful. This repo demonstrates the stateless mode.
12+
13+
In stateless mode, clients do not establish persistent SSE connections to MCP Server. This means clients will not receive proactive notifications from the server. On the other hand, stateless mode allows you to scale your server horizontally.
14+
15+
This sample implements simple authorization demo with API Gateway Custom Authorizer.
16+
17+
![Architecture Diagram](./doc/architecture.png)
18+
19+
## Folder Structure
20+
21+
This sample application codebase is organized into folders : the backend code lives in ```bin/mcp-stateless-lambda.ts``` and uses the AWS CDK resources defined in the ```lib``` folder.
22+
23+
The key folders are:
24+
25+
```
26+
samples/mcp-stateless-lambda
27+
28+
├── bin
29+
│ └── mcp-stateless-lambda.ts # Backend - CDK app
30+
├── lib # CDK Stacks
31+
│ ├── mcp-stateless-lambda-stack.ts # Stack deploying the resources
32+
├── mcp_client # test mcp client to connect to the remote server
33+
├── lambdas # lambda functions
34+
```
35+
36+
## Getting started
37+
38+
### Prerequisites
39+
40+
- An AWS account. We recommend you deploy this solution in a new account.
41+
- [AWS CLI](https://aws.amazon.com/cli/): configure your credentials
42+
43+
```
44+
aws configure --profile [your-profile]
45+
AWS Access Key ID [None]: xxxxxx
46+
AWS Secret Access Key [None]:yyyyyyyyyy
47+
Default region name [None]: us-east-1
48+
Default output format [None]: json
49+
```
50+
51+
- Node.js: v18.12.1
52+
- [AWS CDK](https://github.com/aws/aws-cdk/releases/tag/v2.114.0): 2.114.0
53+
- jq: jq-1.6
54+
55+
### Deploy the solution
56+
57+
This project is built using the [AWS Cloud Development Kit (CDK)](https://aws.amazon.com/cdk/). See [Getting Started With the AWS CDK](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html) for additional details and prerequisites.
58+
59+
1. Clone this repository.
60+
61+
```shell
62+
git clone https://github.com/aws-samples/generative-ai-cdk-constructs-samples.git
63+
```
64+
65+
2. Enter the code sample backend directory.
66+
67+
```shell
68+
cd samples/mcp-stateless-lambda
69+
```
70+
71+
3. Install packages
72+
73+
```shell
74+
npm install
75+
```
76+
77+
4. Install the dependencies
78+
79+
```shell
80+
(cd src/mcpclient && npm install)
81+
(cd src/mcpserver && npm install)
82+
```
83+
84+
5. Boostrap AWS CDK resources on the AWS account.
85+
86+
```shell
87+
cdk bootstrap aws://ACCOUNT_ID/REGION
88+
```
89+
90+
6. Deploy the sample in your account.
91+
92+
```shell
93+
$ cdk deploy
94+
```
95+
96+
The command above will deploy one stack in your account. With the default configuration of this sample.
97+
98+
To protect you against unintended changes that affect your security posture, the AWS CDK Toolkit prompts you to approve security-related changes before deploying them. You will need to answer yes to get all the stack deployed.
99+
100+
7. Retrieve the value of the CfnOutput related to your remote MCP server endpoint from the stack
101+
102+
```shell
103+
$ aws cloudformation describe-stacks --stack-name McpStatelessLambdaStack --query "Stacks[0].Outputs[?contains(OutputKey, 'McpServerEndpoint')].OutputValue"
104+
105+
[
106+
"OutputValue": "https://<endpoint>/dev/mcp"
107+
]
108+
```
109+
110+
8. Export an env variable named `MCP_SERVER_ENDPOINT` with the previous output value
111+
112+
```shell
113+
export MCP_SERVER_ENDPOINT='value'
114+
```
115+
116+
### Test your remote MCP Server with MCP client
117+
118+
Use the provided mcp client to test your remote mcp server
119+
120+
```shell
121+
node mcp_client/index.js
122+
```
123+
124+
Observe the response:
125+
126+
```
127+
Connecting ENDPOINT_URL=XXXXXXXX.amazonaws.com/dev/mcp
128+
connected
129+
listTools response: { tools: [ { name: 'ping', inputSchema: [Object] } ] }
130+
callTool:ping response: {
131+
content: [
132+
{
133+
type: 'text',
134+
text: 'pong! logStream=2025/05/06/[$LATEST]7037eebd7f314fa18d6320801a54a50f v=0.0.12 d=49'
135+
}
136+
]
137+
}
138+
callTool:ping response: {
139+
content: [
140+
{
141+
type: 'text',
142+
text: 'pong! logStream=2025/05/06/[$LATEST]7037eebd7f314fa18d6320801a54a50f v=0.0.12 d=101'
143+
}
144+
]
145+
}
146+
```
147+
148+
## Clean up
149+
150+
Do not forget to delete the stack to avoid unexpected charges.
151+
152+
```shell
153+
$ cdk destroy
154+
```
155+
156+
Delete associated logs created by the different services in Amazon CloudWatch logs.
157+
158+
## Content Security Legal Disclaimer
159+
160+
The sample code; software libraries; command line tools; proofs of concept; templates; or other related technology (including any of the foregoing that are provided by our personnel) is provided to you as AWS Content under the AWS Customer Agreement, or the relevant written agreement between you and AWS (whichever applies). You should not use this AWS Content in your production accounts, or on production or other critical data. You are responsible for testing, securing, and optimizing the AWS Content, such as sample code, as appropriate for production grade use based on your specific quality control practices and standards. Deploying AWS Content may incur AWS charges for creating or using AWS chargeable resources, such as running Amazon EC2 instances or using Amazon S3 storage.
161+
162+
## Operational Metrics Collection
163+
164+
This solution collects anonymous operational metrics to help AWS improve the quality and features of the solution. Data collection is subject to the AWS Privacy Policy (https://aws.amazon.com/privacy/). To opt out of this feature, simply remove the tag(s) starting with “uksb-” or “SO” from the description(s) in any CloudFormation templates or CDK TemplateOptions.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#!/usr/bin/env node
2+
import * as cdk from 'aws-cdk-lib';
3+
import { McpStatelessEcsStack } from '../lib/mcp-stateless-ecs-stack';
4+
5+
const app = new cdk.App();
6+
new McpStatelessEcsStack(app, 'McpStatelessEcsStack', {
7+
/* If you don't specify 'env', this stack will be environment-agnostic.
8+
* Account/Region-dependent features and context lookups will not work,
9+
* but a single synthesized template can be deployed anywhere. */
10+
11+
/* Uncomment the next line to specialize this stack for the AWS Account
12+
* and Region that are implied by the current CLI configuration. */
13+
// env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION },
14+
15+
/* Uncomment the next line if you know exactly what Account and Region you
16+
* want to deploy the stack to. */
17+
// env: { account: '123456789012', region: 'us-east-1' },
18+
19+
/* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */
20+
description: 'Description: (uksb-1tupboc43) (tag:mcp-stateless-ecs-sample)'
21+
});

samples/mcp-stateless-ecs/cdk.json

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
{
2+
"app": "npx ts-node --prefer-ts-exts bin/mcp-stateless-ecs.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:enableImdsBlockingDeprecatedFeature": false,
72+
"@aws-cdk/aws-ecs:disableEcsImdsBlocking": true,
73+
"@aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions": true,
74+
"@aws-cdk/aws-dynamodb:resourcePolicyPerReplica": true,
75+
"@aws-cdk/aws-ec2:ec2SumTImeoutEnabled": true,
76+
"@aws-cdk/aws-appsync:appSyncGraphQLAPIScopeLambdaPermission": true,
77+
"@aws-cdk/aws-rds:setCorrectValueForDatabaseInstanceReadReplicaInstanceResourceId": true,
78+
"@aws-cdk/core:cfnIncludeRejectComplexResourceUpdateCreatePolicyIntrinsics": true,
79+
"@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages": true,
80+
"@aws-cdk/aws-stepfunctions-tasks:fixRunEcsTaskPolicy": true,
81+
"@aws-cdk/aws-ec2:bastionHostUseAmazonLinux2023ByDefault": true,
82+
"@aws-cdk/aws-route53-targets:userPoolDomainNameMethodWithoutCustomResource": true,
83+
"@aws-cdk/aws-elasticloadbalancingV2:albDualstackWithoutPublicIpv4SecurityGroupRulesDefault": true,
84+
"@aws-cdk/aws-iam:oidcRejectUnauthorizedConnections": true,
85+
"@aws-cdk/core:enableAdditionalMetadataCollection": true,
86+
"@aws-cdk/aws-lambda:createNewPoliciesWithAddToRolePolicy": true,
87+
"@aws-cdk/aws-s3:setUniqueReplicationRoleName": true
88+
}
89+
}
58.5 KB
Loading
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
module.exports = {
2+
testEnvironment: 'node',
3+
roots: ['<rootDir>/test'],
4+
testMatch: ['**/*.test.ts'],
5+
transform: {
6+
'^.+\\.tsx?$': 'ts-jest'
7+
}
8+
};
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import * as cdk from 'aws-cdk-lib';
2+
import { Construct } from 'constructs';
3+
// import * as sqs from 'aws-cdk-lib/aws-sqs';
4+
5+
export class McpStatelessEcsStack extends cdk.Stack {
6+
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
7+
super(scope, id, props);
8+
9+
// The code that defines your stack goes here
10+
11+
// example resource
12+
// const queue = new sqs.Queue(this, 'McpStatelessEcsQueue', {
13+
// visibilityTimeout: cdk.Duration.seconds(300)
14+
// });
15+
}
16+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
3+
4+
const ENDPOINT_URL = process.env.MCP_SERVER_ENDPOINT || 'http://localhost:3000/mcp';
5+
6+
console.log(`Connecting ENDPOINT_URL=${ENDPOINT_URL}`);
7+
8+
const transport = new StreamableHTTPClientTransport(new URL(ENDPOINT_URL));
9+
10+
const client = new Client({
11+
name: "node-client",
12+
version: "0.0.1"
13+
})
14+
15+
await client.connect(transport);
16+
console.log('connected');
17+
18+
const tools = await client.listTools();
19+
console.log(`listTools response: `, tools);
20+
21+
for (let i = 0; i < 2; i++) {
22+
let result = await client.callTool({
23+
name: "ping"
24+
});
25+
console.log(`callTool:ping response: `, result);
26+
}
27+
28+
await client.close();

0 commit comments

Comments
 (0)