├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md └── modules ├── anthropic-bedrock-python-ecs-mcp ├── README.md ├── docker │ ├── client │ └── server ├── infra │ └── mcp-sse-cdk │ │ ├── .gitignore │ │ ├── README.md │ │ ├── app.py │ │ ├── cdk.json │ │ ├── mcp_sse_cdk │ │ ├── __init__.py │ │ └── mcp_sse_cdk_stack.py │ │ ├── requirements-dev.txt │ │ ├── requirements.txt │ │ ├── source.bat │ │ └── tests │ │ ├── __init__.py │ │ └── unit │ │ ├── __init__.py │ │ └── test_mcp_sse_cdk_stack.py ├── pyproject.toml ├── requirements.txt └── src │ ├── client-requirements.txt │ ├── client.py │ ├── server-requirements.txt │ └── server.py ├── converse-client-server-sse-demo-docker ├── .gitignore ├── README.md ├── client │ ├── .dockerignore │ ├── Dockerfile │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── MCPClient.ts │ │ ├── MCPConverseClient.ts │ │ ├── config │ │ │ └── bedrock.ts │ │ ├── converse │ │ │ ├── ConverseAgent.ts │ │ │ ├── ConverseTools.ts │ │ │ └── types.ts │ │ └── index.ts │ └── tsconfig.json ├── docker-compose.yml ├── scripts │ ├── build.sh │ ├── run.sh │ └── stop.sh └── server │ ├── Dockerfile │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── app.ts │ ├── config │ │ └── environment.ts │ ├── services │ │ └── health.ts │ ├── tools │ │ ├── aws │ │ │ └── s3BucketCount.ts │ │ ├── index.ts │ │ └── time.ts │ ├── types.ts │ └── types │ │ └── index.ts │ └── tsconfig.json ├── converse-client-server-stdio-demo-local ├── .gitignore ├── README.md ├── app.py ├── converse_agent.py ├── converse_tools.py ├── mcp_client.py ├── mcp_server.py └── requirements.txt ├── java-mcp-bedrock-agent ├── .gitattributes ├── .gitignore ├── .mvn │ └── wrapper │ │ └── maven-wrapper.properties ├── README.md ├── mvnw ├── mvnw.cmd ├── pom.xml └── src │ └── main │ └── java │ └── javamcpbedrockagent │ └── Application.java ├── spring-ai-agent-ecs ├── .gitattributes ├── .gitignore ├── .mvn │ └── wrapper │ │ └── maven-wrapper.properties ├── README.md ├── client.http ├── client │ ├── pom.xml │ └── src │ │ └── main │ │ ├── kotlin │ │ └── mcpagentspringai │ │ │ └── Application.kt │ │ └── resources │ │ └── application.properties ├── infra.cfn ├── mvnw ├── mvnw.cmd ├── pom.xml └── server │ ├── pom.xml │ └── src │ └── main │ ├── kotlin │ └── mcpagentspringai │ │ ├── Application.kt │ │ └── SampleData.kt │ └── resources │ └── application.properties ├── spring-ai-java-bedrock-mcp-rag ├── .gitignore ├── README.md ├── adoptions │ ├── .gitattributes │ ├── .gitignore │ ├── .mvn │ │ └── wrapper │ │ │ └── maven-wrapper.properties │ ├── mvnw │ ├── mvnw.cmd │ ├── pom.xml │ └── src │ │ ├── main │ │ ├── java │ │ │ └── com │ │ │ │ └── example │ │ │ │ └── adoptions │ │ │ │ └── AdoptionsApplication.java │ │ └── resources │ │ │ └── application.properties │ │ └── test │ │ ├── java │ │ └── com │ │ │ └── example │ │ │ └── adoptions │ │ │ ├── DogTest.java │ │ │ ├── ServerTest.java │ │ │ └── TestAdoptionsApplication.java │ │ └── resources │ │ └── schema.sql ├── resources │ └── client.http └── scheduling │ ├── .gitattributes │ ├── .gitignore │ ├── .mvn │ └── wrapper │ │ └── maven-wrapper.properties │ ├── mvnw │ ├── mvnw.cmd │ ├── pom.xml │ └── src │ └── main │ ├── java │ └── com │ │ └── example │ │ └── adoptions │ │ └── SchedulingApplication.java │ └── resources │ └── application.properties ├── spring-ai-mcp-inter-agent-ecs ├── .gitattributes ├── .gitignore ├── .mvn │ └── wrapper │ │ └── maven-wrapper.properties ├── README.md ├── client-server │ ├── pom.xml │ └── src │ │ ├── main │ │ ├── java │ │ │ └── mcpagentspringai │ │ │ │ └── Application.java │ │ └── resources │ │ │ └── application.properties │ │ └── test │ │ ├── java │ │ └── mcpagentspringai │ │ │ └── TestApplication.java │ │ └── resources │ │ └── application.properties ├── client.http ├── client │ ├── pom.xml │ └── src │ │ └── main │ │ ├── java │ │ └── mcpagentspringai │ │ │ └── Application.java │ │ └── resources │ │ └── application.properties ├── infra.cfn ├── mvnw ├── mvnw.cmd ├── pom.xml └── server │ ├── pom.xml │ └── src │ └── main │ ├── kotlin │ └── mcpagentspringai │ │ ├── Application.kt │ │ └── SampleData.kt │ └── resources │ └── application.properties └── spring-ai-mcp-server-ecs ├── .gitattributes ├── .gitignore ├── .mvn └── wrapper │ └── maven-wrapper.properties ├── README.md ├── infra.cfn ├── mvnw ├── mvnw.cmd ├── pom.xml └── src └── main └── kotlin └── springaimcpserverecs └── Application.kt /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | # Local .terraform directories 3 | **/.terraform/* 4 | 5 | # .tfstate files 6 | *.tfstate 7 | *.tfstate.* 8 | 9 | # Crash log files 10 | crash.log 11 | crash.*.log 12 | 13 | # Exclude all .tfvars files, which are likely to contain sensitive data, such as 14 | # password, private keys, and other secrets. These should not be part of version 15 | # control as they are data points which are potentially sensitive and subject 16 | # to change depending on the environment. 17 | *.tfvars 18 | *.tfvars.json 19 | 20 | # Ignore override files as they are usually used to override resources locally and so 21 | # are not checked in 22 | override.tf 23 | override.tf.json 24 | *_override.tf 25 | *_override.tf.json 26 | 27 | # Ignore transient lock info files created by terraform apply 28 | .terraform.tfstate.lock.info 29 | 30 | # Include override files you do wish to add to version control using negated pattern 31 | # !example_override.tf 32 | 33 | # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan 34 | # example: *tfplan* 35 | 36 | # Ignore CLI configuration files 37 | .terraformrc 38 | terraform.rc 39 | 40 | # WIP Code 41 | modules/temp-ec2-instance/* -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /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 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 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. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | 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. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | 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. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /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. 17 | 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Model Context Protocol samples 2 | 3 | Collection of examples of how to use Model Context Protocol with AWS. 4 | 5 | List of modules: 6 | 7 | | Module | Lang | Description | 8 | |---------------------------------------------------------------------------------------------------------------|--------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 9 | | [Server Client MCP/SSE Demo](./modules/converse-client-server-sse-demo-docker/) | TypeScript | This full demo creates an Amazon Bedrock MCP client using the converse API and MCP server. The sample is deployed in containers that connect over MCP/SSE. | 10 | | [Server Client MCP/stdio Demo](./modules/converse-client-server-stdio-demo-local/) | Python | This is a demo Amazon Bedrock MCP client using the converse API and a simple MCP stdio server. The sample runs locally connected with Amazon Bedrock. | 11 | | [Server Client MCP/SSE on ECS](./modules/spring-ai-agent-ecs/) | Spring AI & Kotlin | Provides a sample Spring AI MCP Server that runs on ECS; which is used by a Spring AI Agent using Bedrock; which also runs on ECS and is exposed publicly via a Load Balancer. | 12 | | [Server Client MCP/SSE in Bedrock Converse Client w/ pgVector RAG](./modules/spring-ai-java-bedrock-mcp-rag/) | Spring AI & Java | A Spring AI dog adoption agent built on Bedrock using PostgreSQL with pgvector for RAG, and an MCP Server for managing adoption appointments. | 13 | | [Server MCP/SSE on ECS](./modules/spring-ai-mcp-server-ecs/) | Spring AI & Kotlin | Very basic Spring AI MCP Server over SSE running on ECS. | 14 | | [MCP/SSE Server - FastAPI Client with Anthropic Bedrock](./modules/anthropic-bedrock-python-ecs-mcp/) | Python | An MCP SSE server with a FastAPI client that leverages Anthropic Bedrock. The sample runs on ECS Fargate with public access through an Application Load Balancer. | 15 | 16 | ## Security 17 | 18 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. 19 | 20 | ## License 21 | 22 | This library is licensed under the MIT-0 License. See the LICENSE file. 23 | -------------------------------------------------------------------------------- /modules/anthropic-bedrock-python-ecs-mcp/README.md: -------------------------------------------------------------------------------- 1 | # Model Context Protocol (MCP) Service with AWS CDK 2 | 3 | ## Architecture Overview 4 | 5 | ```mermaid 6 | flowchart LR 7 | subgraph aws[AWS] 8 | alb[Application Load Balancer] 9 | 10 | subgraph vpc[VPC] 11 | server[MCP Server\nECS Service] 12 | client[MCP Client / Bedrock Agent\nECS Service] 13 | end 14 | 15 | subgraph services[AWS Services] 16 | bedrock[Bedrock] 17 | end 18 | end 19 | 20 | internet((Internet)) 21 | 22 | %% Connections 23 | internet <--> alb 24 | alb --> client 25 | client <--> bedrock 26 | client <--> server 27 | 28 | %% Styling 29 | style aws fill:#f5f5f5,stroke:#232F3E,stroke-width:2px 30 | style vpc fill:#E8F4FA,stroke:#147EBA,stroke-width:2px 31 | style services fill:#E8F4FA,stroke:#147EBA,stroke-width:2px 32 | 33 | style alb fill:#FF9900,color:#fff,stroke:#FF9900 34 | style server fill:#2196f3,color:#fff,stroke:#2196f3 35 | style client fill:#2196f3,color:#fff,stroke:#2196f3 36 | style bedrock fill:#FF9900,color:#fff,stroke:#FF9900 37 | style internet fill:#fff,stroke:#666,stroke-width:2px 38 | 39 | %% Link styling 40 | linkStyle default stroke:#666,stroke-width:2px 41 | ``` 42 | 43 | ## Prerequisites 44 | 45 | - AWS CLI configured 46 | - Docker installed 47 | - Node.js (for CDK) 48 | - Python 3.11+ 49 | - UV package manager 50 | 51 | ## Project Structure 52 | ``` 53 | . 54 | ├── docker/ # Docker configurations 55 | ├── infra/ # Infrastructure code 56 | │ └── mcp-sse-cdk/ # CDK application 57 | ├── src/ # Application code 58 | └── requirements.txt # Project dependencies 59 | ``` 60 | 61 | ## Setup Instructions 62 | 63 | 1. **Install Dependencies** 64 | ```bash 65 | # Create and activate virtual environment 66 | uv venv 67 | source .venv/bin/activate 68 | 69 | # Install project dependencies 70 | uv pip install -r requirements.txt 71 | ``` 72 | 73 | 2. **Build Docker Images** 74 | ```bash 75 | # Get AWS account ID 76 | export AWS_ACCOUNT=$(aws sts get-caller-identity --query Account --output text) 77 | export AWS_REGION=us-east-1 78 | 79 | # Create ECR repository if it doesn't exist 80 | aws ecr create-repository --repository-name mcp-sse 81 | 82 | # Login to ECR 83 | aws ecr get-login-password --region ${AWS_REGION} | docker login --username AWS --password-stdin ${AWS_ACCOUNT}.dkr.ecr.${AWS_REGION}.amazonaws.com 84 | 85 | # Build images locally 86 | docker build -f docker/server/Dockerfile -t server-image . 87 | docker build -f docker/client/Dockerfile -t client-image . 88 | 89 | # Tag images for ECR 90 | docker tag server-image ${AWS_ACCOUNT}.dkr.ecr.${AWS_REGION}.amazonaws.com/mcp-sse:server-image 91 | docker tag client-image ${AWS_ACCOUNT}.dkr.ecr.${AWS_REGION}.amazonaws.com/mcp-sse:client-image 92 | 93 | # Push images to ECR 94 | docker push ${AWS_ACCOUNT}.dkr.ecr.${AWS_REGION}.amazonaws.com/mcp-sse:server-image 95 | docker push ${AWS_ACCOUNT}.dkr.ecr.${AWS_REGION}.amazonaws.com/mcp-sse:client-image 96 | ``` 97 | 98 | 3. **Deploy Infrastructure** 99 | ```bash 100 | # Navigate to CDK directory 101 | cd infra/mcp-sse-cdk 102 | 103 | # Install CDK dependencies 104 | uv pip install -r requirements.txt 105 | 106 | # Bootstrap CDK (first time only) 107 | cdk bootstrap 108 | 109 | # Synthesize CloudFormation template 110 | cdk synth 111 | 112 | # Deploy stack 113 | cdk deploy 114 | ``` 115 | 116 | 4. **Verify Deployment** 117 | ```bash 118 | # Get Load Balancer DNS 119 | export ALB_DNS=$(aws elbv2 describe-load-balancers --query 'LoadBalancers[*].DNSName' --output text) 120 | 121 | # Test health endpoint 122 | curl http://${ALB_DNS}/health 123 | 124 | # Test query endpoint 125 | curl -X POST http://${ALB_DNS}/query \ 126 | -H "Content-Type: application/json" \ 127 | -d '{"text": "Get me a greeting for Sarah"}' 128 | 129 | # output: {"response":"I'll help you get a greeting for Sarah using the greeting function. 130 | #[Calling tool greeting with args {'name': 'Sarah'}] 131 | #Hello Sarah! 👋 Hope you're having a wonderful day!"}% 132 | ``` 133 | 134 | ## Cleanup 135 | 136 | To avoid incurring charges, clean up resources: 137 | ```bash 138 | # Delete CDK stack 139 | cd infra/mcp-sse-cdk 140 | cdk destroy 141 | 142 | # Delete ECR images 143 | aws ecr delete-repository --repository-name mcp-sse --force 144 | ``` 145 | 146 | ## Troubleshooting 147 | 148 | 1. **Container Health Checks** 149 | - Verify target group health in EC2 Console 150 | - Ensure security group rules allow traffic 151 | 152 | 2. **Service Connect Issues** 153 | - Verify namespace creation in Cloud Map 154 | - Check service discovery endpoints 155 | 156 | 3. **Bedrock Access** 157 | - Verify IAM role permissions 158 | - Check regional endpoints 159 | - Validate model ARNs 160 | 161 | For more detailed information, consult the AWS documentation or raise an issue in the repository. -------------------------------------------------------------------------------- /modules/anthropic-bedrock-python-ecs-mcp/docker/client: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/astral-sh/uv:bookworm 2 | 3 | # Set working directory 4 | WORKDIR /app 5 | 6 | # Install dependencies 7 | COPY ./src/client-requirements.txt . 8 | RUN uv venv 9 | RUN uv pip install -r client-requirements.txt 10 | 11 | # Copy client code 12 | COPY ./src/client.py . 13 | 14 | # Expose port 8080 15 | EXPOSE 8080 16 | 17 | # Command to run the client 18 | CMD ["uv", "run", "client.py"] 19 | -------------------------------------------------------------------------------- /modules/anthropic-bedrock-python-ecs-mcp/docker/server: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/astral-sh/uv:bookworm 2 | # Set working directory 3 | WORKDIR /app 4 | 5 | # Copy requirements and install dependencies 6 | COPY ./src/server-requirements.txt . 7 | RUN uv venv 8 | RUN uv pip install -r server-requirements.txt 9 | 10 | # Copy server code 11 | COPY ./src/server.py . 12 | 13 | # Expose port 8000 14 | EXPOSE 8000 15 | 16 | # Command to run the server 17 | CMD ["uv", "run", "server.py", "--transport", "sse", "--port", "8000"] 18 | -------------------------------------------------------------------------------- /modules/anthropic-bedrock-python-ecs-mcp/infra/mcp-sse-cdk/.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | package-lock.json 3 | __pycache__ 4 | .pytest_cache 5 | .venv 6 | *.egg-info 7 | 8 | # CDK asset staging directory 9 | .cdk.staging 10 | cdk.out 11 | -------------------------------------------------------------------------------- /modules/anthropic-bedrock-python-ecs-mcp/infra/mcp-sse-cdk/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Welcome to your CDK Python project! 3 | 4 | This is a blank project for CDK development with Python. 5 | 6 | The `cdk.json` file tells the CDK Toolkit how to execute your app. 7 | 8 | This project is set up like a standard Python project. The initialization 9 | process also creates a virtualenv within this project, stored under the `.venv` 10 | directory. To create the virtualenv it assumes that there is a `python3` 11 | (or `python` for Windows) executable in your path with access to the `venv` 12 | package. If for any reason the automatic creation of the virtualenv fails, 13 | you can create the virtualenv manually. 14 | 15 | To manually create a virtualenv on MacOS and Linux: 16 | 17 | ``` 18 | $ python3 -m venv .venv 19 | ``` 20 | 21 | After the init process completes and the virtualenv is created, you can use the following 22 | step to activate your virtualenv. 23 | 24 | ``` 25 | $ source .venv/bin/activate 26 | ``` 27 | 28 | If you are a Windows platform, you would activate the virtualenv like this: 29 | 30 | ``` 31 | % .venv\Scripts\activate.bat 32 | ``` 33 | 34 | Once the virtualenv is activated, you can install the required dependencies. 35 | 36 | ``` 37 | $ pip install -r requirements.txt 38 | ``` 39 | 40 | At this point you can now synthesize the CloudFormation template for this code. 41 | 42 | ``` 43 | $ cdk synth 44 | ``` 45 | 46 | To add additional dependencies, for example other CDK libraries, just add 47 | them to your `setup.py` file and rerun the `pip install -r requirements.txt` 48 | command. 49 | 50 | ## Useful commands 51 | 52 | * `cdk ls` list all stacks in the app 53 | * `cdk synth` emits the synthesized CloudFormation template 54 | * `cdk deploy` deploy this stack to your default AWS account/region 55 | * `cdk diff` compare deployed stack with current state 56 | * `cdk docs` open CDK documentation 57 | 58 | Enjoy! 59 | -------------------------------------------------------------------------------- /modules/anthropic-bedrock-python-ecs-mcp/infra/mcp-sse-cdk/app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from mcp_sse_cdk.mcp_sse_cdk_stack import McpSseCdkStack 3 | from aws_cdk import App 4 | 5 | 6 | app = App() 7 | McpSseCdkStack(app, "McpSseCdkStack") 8 | app.synth() 9 | 10 | -------------------------------------------------------------------------------- /modules/anthropic-bedrock-python-ecs-mcp/infra/mcp-sse-cdk/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "python3 app.py", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "requirements*.txt", 11 | "source.bat", 12 | "**/__init__.py", 13 | "**/__pycache__", 14 | "tests" 15 | ] 16 | }, 17 | "context": { 18 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 19 | "@aws-cdk/core:checkSecretUsage": true, 20 | "@aws-cdk/core:target-partitions": [ 21 | "aws", 22 | "aws-cn" 23 | ], 24 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 25 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 26 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 27 | "@aws-cdk/aws-iam:minimizePolicies": true, 28 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 29 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 30 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 31 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 32 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 33 | "@aws-cdk/core:enablePartitionLiterals": true, 34 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, 35 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, 36 | "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, 37 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, 38 | "@aws-cdk/aws-route53-patters:useCertificate": true, 39 | "@aws-cdk/customresources:installLatestAwsSdkDefault": false, 40 | "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, 41 | "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, 42 | "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, 43 | "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, 44 | "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, 45 | "@aws-cdk/aws-redshift:columnId": true, 46 | "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, 47 | "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, 48 | "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, 49 | "@aws-cdk/aws-kms:aliasNameRef": true, 50 | "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, 51 | "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, 52 | "@aws-cdk/aws-efs:denyAnonymousAccess": true, 53 | "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, 54 | "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, 55 | "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, 56 | "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, 57 | "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, 58 | "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, 59 | "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, 60 | "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true, 61 | "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true, 62 | "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true, 63 | "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true, 64 | "@aws-cdk/aws-eks:nodegroupNameAttribute": true, 65 | "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true, 66 | "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true, 67 | "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false, 68 | "@aws-cdk/aws-s3:keepNotificationInImportedBucket": false, 69 | "@aws-cdk/aws-ecs:enableImdsBlockingDeprecatedFeature": false, 70 | "@aws-cdk/aws-ecs:disableEcsImdsBlocking": true, 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 | "@aws-cdk/aws-ec2:bastionHostUseAmazonLinux2023ByDefault": true, 80 | "@aws-cdk/aws-route53-targets:userPoolDomainNameMethodWithoutCustomResource": true, 81 | "@aws-cdk/aws-elasticloadbalancingV2:albDualstackWithoutPublicIpv4SecurityGroupRulesDefault": true, 82 | "@aws-cdk/aws-iam:oidcRejectUnauthorizedConnections": true, 83 | "@aws-cdk/core:enableAdditionalMetadataCollection": true, 84 | "@aws-cdk/aws-lambda:createNewPoliciesWithAddToRolePolicy": false, 85 | "@aws-cdk/aws-s3:setUniqueReplicationRoleName": true, 86 | "@aws-cdk/aws-events:requireEventBusPolicySid": true, 87 | "@aws-cdk/aws-dynamodb:retainTableReplica": true 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /modules/anthropic-bedrock-python-ecs-mcp/infra/mcp-sse-cdk/mcp_sse_cdk/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/Sample-Model-Context-Protocol-Demos/d714298de608c35ca4a518a5b4dc938a04a05ee9/modules/anthropic-bedrock-python-ecs-mcp/infra/mcp-sse-cdk/mcp_sse_cdk/__init__.py -------------------------------------------------------------------------------- /modules/anthropic-bedrock-python-ecs-mcp/infra/mcp-sse-cdk/requirements-dev.txt: -------------------------------------------------------------------------------- 1 | pytest==6.2.5 2 | -------------------------------------------------------------------------------- /modules/anthropic-bedrock-python-ecs-mcp/infra/mcp-sse-cdk/requirements.txt: -------------------------------------------------------------------------------- 1 | aws-cdk-lib==2.189.0 2 | constructs>=10.0.0,<11.0.0 3 | -------------------------------------------------------------------------------- /modules/anthropic-bedrock-python-ecs-mcp/infra/mcp-sse-cdk/source.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | rem The sole purpose of this script is to make the command 4 | rem 5 | rem source .venv/bin/activate 6 | rem 7 | rem (which activates a Python virtualenv on Linux or Mac OS X) work on Windows. 8 | rem On Windows, this command just runs this batch file (the argument is ignored). 9 | rem 10 | rem Now we don't need to document a Windows command for activating a virtualenv. 11 | 12 | echo Executing .venv\Scripts\activate.bat for you 13 | .venv\Scripts\activate.bat 14 | -------------------------------------------------------------------------------- /modules/anthropic-bedrock-python-ecs-mcp/infra/mcp-sse-cdk/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/Sample-Model-Context-Protocol-Demos/d714298de608c35ca4a518a5b4dc938a04a05ee9/modules/anthropic-bedrock-python-ecs-mcp/infra/mcp-sse-cdk/tests/__init__.py -------------------------------------------------------------------------------- /modules/anthropic-bedrock-python-ecs-mcp/infra/mcp-sse-cdk/tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/Sample-Model-Context-Protocol-Demos/d714298de608c35ca4a518a5b4dc938a04a05ee9/modules/anthropic-bedrock-python-ecs-mcp/infra/mcp-sse-cdk/tests/unit/__init__.py -------------------------------------------------------------------------------- /modules/anthropic-bedrock-python-ecs-mcp/infra/mcp-sse-cdk/tests/unit/test_mcp_sse_cdk_stack.py: -------------------------------------------------------------------------------- 1 | import aws_cdk as core 2 | import aws_cdk.assertions as assertions 3 | 4 | from mcp_sse_cdk.mcp_sse_cdk_stack import McpSseCdkStack 5 | 6 | # example tests. To run these tests, uncomment this file along with the example 7 | # resource in mcp_sse_cdk/mcp_sse_cdk_stack.py 8 | def test_sqs_queue_created(): 9 | app = core.App() 10 | stack = McpSseCdkStack(app, "mcp-sse-cdk") 11 | template = assertions.Template.from_stack(stack) 12 | 13 | # template.has_resource_properties("AWS::SQS::Queue", { 14 | # "VisibilityTimeout": 300 15 | # }) 16 | -------------------------------------------------------------------------------- /modules/anthropic-bedrock-python-ecs-mcp/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "mcp-sse" 3 | version = "0.1.0" 4 | description = "Add your description here" 5 | readme = "README.md" 6 | requires-python = ">=3.13" 7 | dependencies = [ 8 | "anthropic>=0.49.0", 9 | "aws-cdk-lib>=2.190.0", 10 | "boto3>=1.38.0", 11 | "botocore>=1.38.0", 12 | "constructs>=10.4.2", 13 | "fastapi>=0.115.12", 14 | "mcp[cli]>=1.6.0", 15 | ] 16 | -------------------------------------------------------------------------------- /modules/anthropic-bedrock-python-ecs-mcp/src/client.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from contextlib import AsyncExitStack 3 | from fastapi import FastAPI, HTTPException 4 | from pydantic import BaseModel 5 | import uvicorn 6 | 7 | from mcp import ClientSession 8 | from mcp.client.sse import sse_client 9 | 10 | from anthropic import AnthropicBedrock 11 | from dotenv import load_dotenv 12 | import os 13 | 14 | load_dotenv() 15 | SERVER_URL = os.getenv("SERVER_URL", "http://0.0.0.0:8080") 16 | app = FastAPI() 17 | 18 | 19 | class Query(BaseModel): 20 | text: str 21 | 22 | 23 | class MCPClient: 24 | def __init__(self): 25 | self.session: Optional[ClientSession] = None 26 | self.exit_stack = AsyncExitStack() 27 | self.anthropic = AnthropicBedrock() 28 | 29 | async def connect_to_sse_server(self, server_url: str): 30 | """Connect to an MCP server running with SSE transport""" 31 | self._streams_context = sse_client(url=f"{server_url}/sse") 32 | streams = await self._streams_context.__aenter__() 33 | 34 | self._session_context = ClientSession(*streams) 35 | self.session: ClientSession = await self._session_context.__aenter__() 36 | 37 | await self.session.initialize() 38 | 39 | print("Initialized SSE client...") 40 | print("Listing tools...") 41 | response = await self.session.list_tools() 42 | tools = response.tools 43 | print("\nConnected to server with tools:", [tool.name for tool in tools]) 44 | 45 | async def cleanup(self): 46 | """Properly clean up the session and streams""" 47 | if self._session_context: 48 | await self._session_context.__aexit__(None, None, None) 49 | if self._streams_context: 50 | await self._streams_context.__aexit__(None, None, None) 51 | 52 | async def process_query(self, query: str) -> str: 53 | """Process a query using Claude and available tools""" 54 | messages = [{"role": "user", "content": query}] 55 | 56 | response = await self.session.list_tools() 57 | available_tools = [ 58 | { 59 | "name": tool.name, 60 | "description": tool.description, 61 | "input_schema": tool.inputSchema, 62 | } 63 | for tool in response.tools 64 | ] 65 | 66 | response = self.anthropic.messages.create( 67 | model="us.anthropic.claude-3-5-sonnet-20241022-v2:0", 68 | max_tokens=1000, 69 | messages=messages, 70 | tools=available_tools, 71 | ) 72 | 73 | tool_results = [] 74 | final_text = [] 75 | 76 | for content in response.content: 77 | if content.type == "text": 78 | final_text.append(content.text) 79 | elif content.type == "tool_use": 80 | tool_name = content.name 81 | tool_args = content.input 82 | 83 | result = await self.session.call_tool(tool_name, tool_args) 84 | tool_results.append({"call": tool_name, "result": result}) 85 | final_text.append(f"[Calling tool {tool_name} with args {tool_args}]") 86 | 87 | if hasattr(content, "text") and content.text: 88 | messages.append({"role": "assistant", "content": content.text}) 89 | messages.append({"role": "user", "content": result.content}) 90 | 91 | response = self.anthropic.messages.create( 92 | model="us.anthropic.claude-3-5-sonnet-20241022-v2:0", 93 | max_tokens=1000, 94 | messages=messages, 95 | ) 96 | 97 | final_text.append(response.content[0].text) 98 | 99 | return "\n".join(final_text) 100 | 101 | 102 | # Create a global MCPClient instance 103 | mcp_client = MCPClient() 104 | 105 | 106 | @app.on_event("startup") 107 | async def startup_event(): 108 | """Initialize the MCP client when the FastAPI app starts""" 109 | await mcp_client.connect_to_sse_server(server_url=SERVER_URL) 110 | 111 | 112 | @app.on_event("shutdown") 113 | async def shutdown_event(): 114 | """Clean up the MCP client when the FastAPI app shuts down""" 115 | await mcp_client.cleanup() 116 | 117 | 118 | @app.post("/query") 119 | async def process_query(query: Query): 120 | """Handle POST requests with queries""" 121 | try: 122 | response = await mcp_client.process_query(query.text) 123 | return {"response": response} 124 | except Exception as e: 125 | raise HTTPException(status_code=500, detail=str(e)) 126 | 127 | 128 | @app.get("/health") 129 | async def health_check(): 130 | """Health check endpoint""" 131 | return {"status": "healthy"} 132 | 133 | 134 | if __name__ == "__main__": 135 | uvicorn.run(app, host="0.0.0.0", port=8080) 136 | -------------------------------------------------------------------------------- /modules/anthropic-bedrock-python-ecs-mcp/src/server.py: -------------------------------------------------------------------------------- 1 | from mcp.server.fastmcp import FastMCP 2 | 3 | mcp = FastMCP("Documentation") 4 | 5 | 6 | @mcp.tool() 7 | def greeting(name: str) -> str: 8 | """ 9 | Return a specialized, friendly greeting to the user, 10 | 11 | Args: 12 | name: The name of the person we are greeting 13 | 14 | Returns: 15 | Text greeting with the user's name. 16 | """ 17 | return f"Hello {name}!" 18 | 19 | 20 | if __name__ == "__main__": 21 | mcp.run(transport="sse") 22 | -------------------------------------------------------------------------------- /modules/converse-client-server-sse-demo-docker/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | .pnp/ 4 | .pnp.js 5 | 6 | # Build outputs 7 | dist/ 8 | build/ 9 | .next/ 10 | out/ 11 | *.tsbuildinfo 12 | 13 | # Environment and secrets 14 | .env 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | .env.* 20 | 21 | # Debug & Logs 22 | logs/ 23 | *.log 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | .pnpm-debug.log* 28 | 29 | # Testing 30 | coverage/ 31 | .nyc_output/ 32 | 33 | # TypeScript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | 37 | # Cache 38 | .npm/ 39 | .eslintcache 40 | .stylelintcache 41 | .cache/ 42 | 43 | # Editor directories and files 44 | .idea/ 45 | .vscode/* 46 | !.vscode/extensions.json 47 | !.vscode/launch.json 48 | !.vscode/settings.json 49 | *.swp 50 | *.swo 51 | *~ 52 | 53 | # OS generated files 54 | .DS_Store 55 | .DS_Store? 56 | ._* 57 | .Spotlight-V100 58 | .Trashes 59 | ehthumbs.db 60 | Thumbs.db 61 | 62 | # Docker 63 | .docker/ 64 | docker-compose.override.yml 65 | 66 | # Python (for any Python tools/scripts) 67 | __pycache__/ 68 | *.py[cod] 69 | *$py.class 70 | *.so 71 | .Python 72 | env/ 73 | develop-eggs/ 74 | downloads/ 75 | eggs/ 76 | .eggs/ 77 | lib/ 78 | lib64/ 79 | parts/ 80 | sdist/ 81 | var/ 82 | wheels/ 83 | *.egg-info/ 84 | .installed.cfg 85 | *.egg 86 | 87 | # Temporary files 88 | *.tmp 89 | *.temp 90 | .temp/ 91 | tmp/ -------------------------------------------------------------------------------- /modules/converse-client-server-sse-demo-docker/README.md: -------------------------------------------------------------------------------- 1 | # Amazon Bedrock Model Context Protocol (MCP) Demo 2 | 3 | This repository demonstrates the implementation and usage of the Model Context Protocol (MCP) with [Amazon Bedrock](https://docs.aws.amazon.com/bedrock/latest/userguide/what-is-bedrock.html?trk=64e03f01-b931-4384-846e-db0ba9fa89f5&sc_channel=code), showcasing how both client and server components can be built. The demo provides an interactive chat interface powered by Amazon Bedrock, allowing users to interact with various tools and services through natural language. 4 | 5 | ## Architecture Overview 6 | 7 | ```mermaid 8 | flowchart LR 9 | 10 | subgraph ClientContainer["Docker Container 1"] 11 | Client["MCP Client"] 12 | end 13 | 14 | subgraph ServerContainer["Docker Container 2"] 15 | Server["MCP Server"] 16 | Tools["Tools"] 17 | end 18 | 19 | subgraph s1[" "] 20 | ClientContainer 21 | User(("User")) 22 | ServerContainer 23 | end 24 | 25 | User --> Client["MCP Client"] 26 | Server --> Tools 27 | Client -- MCP/SSE --> Server 28 | 29 | Tools --> n1["AWS Services"] 30 | Client["MCP Client"] ----> n2["Amazon Bedrock"] 31 | ``` 32 | 33 | The project consists of two main components: 34 | 35 | 1. **MCP Server**: A TypeScript/Node.js server that: 36 | - Handles incoming requests from the client 37 | - Manages tool registration and execution 38 | - Interfaces with AWS services as resources 39 | - Provides type-safe implementation of tools and services 40 | 41 | 2. **MCP Client**: A TypeScript/Node.js client that: 42 | - Provides an interactive chat interface using [Amazon Bedrock Converse API](https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_Converse.html?trk=64e03f01-b931-4384-846e-db0ba9fa89f5&sc_channel=code) 43 | - Communicates with the MCP server 44 | - Handles user input and displays responses 45 | - Manages the chat session state 46 | 47 | The components are containerized using Docker for consistent deployment and easy setup. You may want to take this as a start for an MCP server deployment to Amazon ECS. 48 | 49 | ## Prerequisites 50 | 51 | Before getting started, you'll need: 52 | 53 | 1. **AWS Account and CLI Setup** 54 | - An AWS Account with appropriate permissions 55 | - AWS CLI version 2 installed and configured (version 1 is not supported) 56 | - [AWS CLI Installation Guide](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html?trk=64e03f01-b931-4384-846e-db0ba9fa89f5&sc_channel=code) 57 | - [AWS CLI Configuration Guide](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html?trk=64e03f01-b931-4384-846e-db0ba9fa89f5&sc_channel=code) 58 | 59 | 2. **Amazon Bedrock Model Access** 60 | - Enable access to Amazon Bedrock foundation models in your AWS region 61 | - This code defaults to the "anthropic.claude-3-5-sonnet-20241022-v2:0" model 62 | - [Request Model Access Guide](https://docs.aws.amazon.com/bedrock/latest/userguide/model-access.html?trk=64e03f01-b931-4384-846e-db0ba9fa89f5&sc_channel=code) 63 | 64 | 3. **Docker Environment** 65 | - Docker Desktop installed and running 66 | - Docker Compose installed 67 | - [Docker Installation Guide](https://docs.docker.com/get-docker/) 68 | - [Docker Compose Installation Guide](https://docs.docker.com/compose/install/) 69 | 70 | ## Project Structure 71 | 72 | ``` 73 | . 74 | └── converse-client-server-sse-demo-docker/ 75 | ├── client/ # MCP client implementation 76 | ├── server/ # MCP server implementation 77 | ├── scripts/ # Helper scripts for building and running 78 | └── docker-compose.yml # Container orchestration 79 | ``` 80 | 81 | ## Getting Started 82 | 83 | ### Building the Application 84 | 85 | Run the build script to create the Docker containers: 86 | 87 | ```bash 88 | ./scripts/build.sh 89 | ``` 90 | 91 | This script: 92 | 1. Builds the MCP server container 93 | 2. Builds the MCP client container 94 | 3. Handles any build errors gracefully 95 | 96 | ### Running the Application 97 | 98 | Start the application using: 99 | 100 | ```bash 101 | ./scripts/run.sh 102 | ``` 103 | 104 | This script: 105 | 1. Verifies AWS credentials and exports them 106 | 2. Sets up the AWS region configuration 107 | 3. Starts the Docker containers 108 | 4. Waits for the server to be ready 109 | 5. Optionally connects you to the client app 110 | 111 | ### Stopping the Application 112 | 113 | To stop all containers: 114 | 115 | ```bash 116 | ./scripts/stop.sh 117 | ``` 118 | 119 | ## Using the Chat Interface 120 | 121 | Once connected to the client app, you can interact with the system through natural language. Here are some example questions you can ask: 122 | 123 | 1. AWS-related S3 queries: 124 | - "How many S3 buckets do I have?" 125 | 126 | 2. Time queries: 127 | - "What's the current time in New York?" 128 | 129 | ## Extending the Server 130 | 131 | To add new tools to the server: 132 | 133 | 1. Create a new tool file in `server/src/tools/` 134 | 2. Implement the tool interface with required methods 135 | 3. Register the tool in `server/src/tools/index.ts` 136 | 4. Rebuild and restart the containers 137 | 138 | Example tool structure: 139 | ```typescript 140 | export interface Tool { 141 | name: string; 142 | description: string; 143 | execute: (params: any) => Promise; 144 | } 145 | ``` 146 | 147 | ## Contributing 148 | 149 | Contributions are welcome! Please: 150 | 151 | 1. Fork the repository 152 | 2. Create a feature branch 153 | 3. Submit a pull request with detailed description 154 | 4. Ensure all tests pass 155 | 156 | ## License 157 | 158 | [Add appropriate license information] -------------------------------------------------------------------------------- /modules/converse-client-server-sse-demo-docker/client/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | dist 4 | .git 5 | .gitignore 6 | .env 7 | *.log 8 | README.md 9 | .vscode 10 | .idea -------------------------------------------------------------------------------- /modules/converse-client-server-sse-demo-docker/client/Dockerfile: -------------------------------------------------------------------------------- 1 | # Build stage 2 | FROM node:20-alpine AS build 3 | 4 | WORKDIR /app 5 | 6 | # Copy package files 7 | COPY package*.json ./ 8 | 9 | # Install all dependencies (including devDependencies) 10 | RUN npm install 11 | 12 | # Copy source code 13 | COPY . . 14 | 15 | # Build TypeScript to JavaScript 16 | RUN npm run build 17 | 18 | # Production stage 19 | FROM node:20-alpine 20 | 21 | WORKDIR /app 22 | 23 | # Copy package files 24 | COPY package*.json ./ 25 | 26 | # Install only production dependencies 27 | RUN npm ci --only=production 28 | 29 | # Copy built JavaScript from builder stage 30 | COPY --from=build /app/dist ./dist 31 | 32 | # Set environment variables 33 | ENV NODE_ENV=production 34 | ENV MCP_SSE_URL=http://mcp-server:8000/sse 35 | 36 | # Create a non-root user and switch to it 37 | RUN addgroup --system --gid 1001 nodejs \ 38 | && adduser --system --uid 1001 --ingroup nodejs nodejs 39 | 40 | # Change ownership of the app directory to the non-root user 41 | RUN chown -R nodejs:nodejs . 42 | 43 | # Switch to non-root user 44 | USER nodejs 45 | 46 | # Start the app 47 | CMD ["node", "dist/index.js"] -------------------------------------------------------------------------------- /modules/converse-client-server-sse-demo-docker/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mcp-client", 3 | "version": "1.0.0", 4 | "description": "MCP Client in TypeScript", 5 | "main": "dist/index.js", 6 | "type": "module", 7 | "scripts": { 8 | "dev": "ts-node-dev --poll src/index.ts", 9 | "build": "rimraf ./dist && tsc", 10 | "start": "node dist/index.js", 11 | "lint": "eslint . --ext .ts", 12 | "test": "jest" 13 | }, 14 | "keywords": [], 15 | "author": "", 16 | "license": "ISC", 17 | "dependencies": { 18 | "@aws-sdk/client-bedrock-runtime": "^3.525.0", 19 | "@modelcontextprotocol/sdk": "^1.6.1", 20 | "chalk": "^5.3.0" 21 | }, 22 | "devDependencies": { 23 | "@types/jest": "^29.5.12", 24 | "@types/node": "^20.11.28", 25 | "@typescript-eslint/eslint-plugin": "^7.2.0", 26 | "@typescript-eslint/parser": "^7.2.0", 27 | "eslint": "^8.57.0", 28 | "jest": "^29.7.0", 29 | "rimraf": "^5.0.5", 30 | "ts-jest": "^29.1.2", 31 | "ts-node-dev": "^2.0.0", 32 | "typescript": "^5.4.2" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /modules/converse-client-server-sse-demo-docker/client/src/MCPClient.ts: -------------------------------------------------------------------------------- 1 | import { Client } from '@modelcontextprotocol/sdk/client/index.js'; 2 | import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'; 3 | import type { Tool, CallToolResult } from '@modelcontextprotocol/sdk/types.js'; 4 | import { ToolListChangedNotificationSchema, ResourceListChangedNotificationSchema, ResourceUpdatedNotificationSchema, type ResourceUpdatedNotification } from '@modelcontextprotocol/sdk/types.js'; 5 | import EventEmitter from 'events'; 6 | 7 | export class MCPClient extends EventEmitter { 8 | private client: Client; 9 | private transport: SSEClientTransport; 10 | 11 | constructor(serverUrl: string) { 12 | super(); 13 | this.transport = new SSEClientTransport(new URL(serverUrl)); 14 | this.client = new Client({ 15 | name: 'mcp-bedrock-demo', 16 | version: '1.0.0' 17 | }); 18 | 19 | // Set up notification handlers 20 | this.client.setNotificationHandler(ToolListChangedNotificationSchema, () => { 21 | this.emit('toolListChanged'); 22 | }); 23 | 24 | this.client.setNotificationHandler(ResourceListChangedNotificationSchema, () => { 25 | this.emit('resourceListChanged'); 26 | }); 27 | 28 | this.client.setNotificationHandler(ResourceUpdatedNotificationSchema, (notification: ResourceUpdatedNotification) => { 29 | this.emit('resourceUpdated', { uri: notification.params.uri }); 30 | }); 31 | } 32 | 33 | async connect(): Promise { 34 | await this.client.connect(this.transport); 35 | } 36 | 37 | async getAvailableTools(): Promise { 38 | const result = await this.client.listTools(); 39 | return result.tools; 40 | } 41 | 42 | async callTool(name: string, toolArgs: Record): Promise { 43 | return await this.client.callTool({ 44 | name, 45 | arguments: toolArgs 46 | }); 47 | } 48 | 49 | async close(): Promise { 50 | await this.transport.close(); 51 | } 52 | } -------------------------------------------------------------------------------- /modules/converse-client-server-sse-demo-docker/client/src/MCPConverseClient.ts: -------------------------------------------------------------------------------- 1 | import { ConverseAgent } from './converse/ConverseAgent.js'; 2 | import { ConverseTools } from './converse/ConverseTools.js'; 3 | import { MCPClient } from './MCPClient.js'; 4 | import { bedrockConfig, serverConfig } from './config/bedrock.js'; 5 | import chalk from 'chalk'; 6 | 7 | const SYSTEM_PROMPT = `You are a helpful assistant that can use tools to help you answer questions and perform tasks. 8 | 9 | When using tools, follow these guidelines to be efficient: 10 | 1. Plan your tool usage before making calls - determine exactly what information you need 11 | 2. Make each tool call count - don't repeat the same call with the same parameters 12 | 3. Only make additional tool calls if the information you have is insufficient 13 | 4. Trust the results you get - don't make verification calls unless explicitly asked 14 | 5. When getting location-based information (weather, time, etc.), get the location info once and reuse it 15 | 16 | Remember: Each tool call is expensive, so use them judiciously while still providing accurate and helpful responses.`; 17 | 18 | export class MCPConverseClient extends MCPClient { 19 | private converseAgent: ConverseAgent; 20 | private converseTools: ConverseTools; 21 | 22 | constructor(serverUrl: string = serverConfig.url, modelId: string = bedrockConfig.modelId) { 23 | super(serverUrl); 24 | this.converseAgent = new ConverseAgent(modelId, bedrockConfig.region, SYSTEM_PROMPT); 25 | this.converseTools = new ConverseTools(); 26 | } 27 | 28 | async connect(): Promise { 29 | await super.connect(); 30 | await this.setupTools(); 31 | } 32 | 33 | private async setupTools(): Promise { 34 | try { 35 | // Fetch available tools from the server 36 | const tools = await this.getAvailableTools(); 37 | console.log(chalk.cyan('Available Tools:')); 38 | 39 | // Register each tool 40 | for (const tool of tools) { 41 | const schema = { 42 | type: tool.inputSchema.type || 'object', 43 | properties: tool.inputSchema.properties || {}, 44 | required: Array.isArray(tool.inputSchema.required) ? tool.inputSchema.required : [] 45 | }; 46 | 47 | this.converseTools.registerTool( 48 | tool.name, 49 | async (name: string, input: any) => { 50 | return await this.callTool(name, input); 51 | }, 52 | tool.description, 53 | schema 54 | ); 55 | console.log(chalk.green(` • ${tool.name}: `) + tool.description); 56 | } 57 | console.log(); // Add blank line for spacing 58 | 59 | // Set the tools in the converse agent 60 | this.converseAgent.setTools(this.converseTools); 61 | } catch (error) { 62 | console.error(chalk.red('Error setting up tools:'), error); 63 | throw error; 64 | } 65 | } 66 | 67 | async processUserInput(input: string): Promise { 68 | try { 69 | if (!input.trim()) { 70 | return; 71 | } 72 | 73 | const timestamp = new Date().toLocaleTimeString(); 74 | console.log(chalk.blue(`[${timestamp}] You: `) + input); 75 | console.log(chalk.yellow('Thinking...')); 76 | 77 | const response = await this.converseAgent.invokeWithPrompt(input); 78 | console.log(chalk.green('Assistant: ') + response); 79 | } catch (error) { 80 | console.error(chalk.red('Error: ') + error); 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /modules/converse-client-server-sse-demo-docker/client/src/config/bedrock.ts: -------------------------------------------------------------------------------- 1 | export const bedrockConfig = { 2 | modelId: process.env.BEDROCK_MODEL_ID || 'anthropic.claude-3-5-sonnet-20241022-v2:0', 3 | region: process.env.AWS_REGION || 'us-west-2', 4 | systemPrompt: process.env.BEDROCK_SYSTEM_PROMPT || 'You are a helpful assistant.', 5 | inferenceConfig: { 6 | maxTokens: 8192, 7 | temperature: 0.7, 8 | topP: 0.999, 9 | stopSequences: [] 10 | }, 11 | anthropicVersion: "bedrock-2023-05-31" 12 | }; 13 | 14 | export const serverConfig = { 15 | url: process.env.MCP_SERVER_URL || 'http://localhost:3000' 16 | }; -------------------------------------------------------------------------------- /modules/converse-client-server-sse-demo-docker/client/src/converse/ConverseAgent.ts: -------------------------------------------------------------------------------- 1 | import { BedrockRuntimeClient, ConverseCommand, type Message, type ContentBlock, type ToolUseBlock, type ToolResultBlock } from "@aws-sdk/client-bedrock-runtime"; 2 | import { ConversationStopReason } from './types.js'; 3 | import type { ConversationToolConfig } from './types.js'; 4 | import { bedrockConfig } from '../config/bedrock.js'; 5 | 6 | export class ConverseAgent { 7 | private client: BedrockRuntimeClient; 8 | private messages: Message[]; 9 | private tools?: ConversationToolConfig; 10 | private responseOutputTags: string[]; 11 | 12 | constructor( 13 | private modelId: string = bedrockConfig.modelId, 14 | private region: string = bedrockConfig.region, 15 | private systemPrompt: string = bedrockConfig.systemPrompt 16 | ) { 17 | this.client = new BedrockRuntimeClient({ region: this.region }); 18 | this.messages = []; 19 | this.responseOutputTags = []; 20 | } 21 | 22 | async invokeWithPrompt(prompt: string): Promise { 23 | const content: ContentBlock[] = [{ text: prompt }]; 24 | return this.invoke(content); 25 | } 26 | 27 | async invoke(content: ContentBlock[]): Promise { 28 | this.messages.push({ 29 | role: 'user', 30 | content 31 | }); 32 | 33 | const response = await this.getConverseResponse(); 34 | return this.handleResponse(response); 35 | } 36 | 37 | private async getConverseResponse(): Promise { 38 | const tools = this.tools?.getTools().tools || []; 39 | // console.log('Tools:', JSON.stringify(tools, null, 2)); 40 | 41 | const requestBody = { 42 | modelId: this.modelId, 43 | messages: this.messages, 44 | system: [{ text: this.systemPrompt }], 45 | toolConfig: tools.length > 0 ? { 46 | tools, 47 | toolChoice: { auto: {} } 48 | } : undefined, 49 | ...bedrockConfig.inferenceConfig, 50 | anthropicVersion: bedrockConfig.anthropicVersion 51 | }; 52 | // console.log('Request body:', JSON.stringify(requestBody, null, 2)); 53 | 54 | const command = new ConverseCommand(requestBody); 55 | const response = await this.client.send(command); 56 | // console.log('Response:', JSON.stringify(response, null, 2)); 57 | return response; 58 | } 59 | 60 | private async handleResponse(response: any): Promise { 61 | // Add the response to the conversation history 62 | if (response.output?.message) { 63 | this.messages.push(response.output.message); 64 | } 65 | 66 | const stopReason = response.stopReason; 67 | 68 | if (stopReason === ConversationStopReason.END_TURN || stopReason === ConversationStopReason.STOP_SEQUENCE) { 69 | try { 70 | const message = response.output?.message; 71 | const content = message?.content || []; 72 | const text = content[0]?.text || ''; 73 | 74 | if (this.responseOutputTags.length === 2) { 75 | const pattern = new RegExp(`(?s).*${this.responseOutputTags[0]}(.*?)${this.responseOutputTags[1]}`); 76 | const match = pattern.exec(text); 77 | if (match) { 78 | return match[1]; 79 | } 80 | } 81 | return text; 82 | } catch (error) { 83 | return ''; 84 | } 85 | } else if (stopReason === ConversationStopReason.TOOL_USE) { 86 | try { 87 | const toolResults: ContentBlock[] = []; 88 | const toolUses = response.output?.message?.content?.filter(item => 'toolUse' in item) || []; 89 | 90 | for (const item of toolUses) { 91 | const toolUse = (item as { toolUse: ToolUseBlock }).toolUse; 92 | if (!toolUse) continue; 93 | 94 | const toolResult = await this.tools?.executeToolAsync(toolUse); 95 | if (toolResult) { 96 | toolResults.push({ toolResult }); 97 | } 98 | } 99 | 100 | return this.invoke(toolResults); 101 | } catch (error) { 102 | throw new Error(`Failed to execute tool: ${error}`); 103 | } 104 | } else if (stopReason === ConversationStopReason.MAX_TOKENS) { 105 | return this.invokeWithPrompt('Please continue.'); 106 | } else { 107 | throw new Error(`Unknown stop reason: ${stopReason}`); 108 | } 109 | } 110 | 111 | setTools(tools: ConversationToolConfig): void { 112 | this.tools = tools; 113 | } 114 | 115 | setResponseOutputTags(tags: string[]): void { 116 | this.responseOutputTags = tags; 117 | } 118 | 119 | clearMessages(): void { 120 | this.messages = []; 121 | } 122 | } -------------------------------------------------------------------------------- /modules/converse-client-server-sse-demo-docker/client/src/converse/ConverseTools.ts: -------------------------------------------------------------------------------- 1 | import type { ConversationToolConfig, ConversationToolSpec, ToolUseBlock, ToolResultBlock } from './types.js'; 2 | 3 | interface ToolDefinition { 4 | function: (name: string, input: any) => Promise; 5 | description: string; 6 | inputSchema: { 7 | type: string; 8 | properties: Record; 9 | required?: string[]; 10 | }; 11 | originalName: string; 12 | } 13 | 14 | export class ConverseTools implements ConversationToolConfig { 15 | private tools: Record = {}; 16 | private nameMapping: Record = {}; 17 | 18 | private sanitizeName(name: string): string { 19 | return name.replace(/-/g, '_'); 20 | } 21 | 22 | registerTool( 23 | name: string, 24 | func: (name: string, input: any) => Promise, 25 | description: string, 26 | inputSchema: { 27 | type: string; 28 | properties: Record; 29 | required?: string[]; 30 | } 31 | ): void { 32 | const sanitizedName = this.sanitizeName(name); 33 | console.log(`Registering tool - ${sanitizedName}`); 34 | 35 | this.nameMapping[sanitizedName] = name; 36 | this.tools[sanitizedName] = { 37 | function: func, 38 | description, 39 | inputSchema, 40 | originalName: name 41 | }; 42 | } 43 | 44 | getTools(): { tools: ConversationToolSpec[] } { 45 | const toolSpecs: ConversationToolSpec[] = []; 46 | 47 | for (const [sanitizedName, tool] of Object.entries(this.tools)) { 48 | toolSpecs.push({ 49 | toolSpec: { 50 | name: sanitizedName, 51 | description: tool.description, 52 | inputSchema: { 53 | json: tool.inputSchema 54 | } 55 | } 56 | }); 57 | } 58 | 59 | return { tools: toolSpecs }; 60 | } 61 | 62 | async executeToolAsync(payload: ToolUseBlock): Promise { 63 | const { toolUseId, name: sanitizedName, input } = payload; 64 | console.log(`Executing tool - Requested name: ${sanitizedName}`); 65 | 66 | if (!(sanitizedName in this.tools)) { 67 | throw new Error(`Unknown tool: ${sanitizedName}`); 68 | } 69 | 70 | try { 71 | const tool = this.tools[sanitizedName]; 72 | // Use original name when calling the actual function 73 | const result = await tool.function(tool.originalName, input); 74 | // console.log('Tool result:', JSON.stringify(result, null, 2)); 75 | 76 | // Handle Bedrock-style content blocks 77 | if (result?.content?.length > 0) { 78 | const textContent = result.content.find(item => item.type === 'text'); 79 | if (textContent) { 80 | return { 81 | toolUseId, 82 | content: [{ 83 | text: textContent.text 84 | }], 85 | status: 'success' 86 | }; 87 | } 88 | } 89 | 90 | // Fallback for non-content-block results 91 | const text = typeof result === 'string' ? result : JSON.stringify(result); 92 | return { 93 | toolUseId, 94 | content: [{ 95 | text 96 | }], 97 | status: 'success' 98 | }; 99 | } catch (error) { 100 | return { 101 | toolUseId, 102 | content: [{ 103 | text: `Error executing tool: ${error}` 104 | }], 105 | status: 'error' 106 | }; 107 | } 108 | } 109 | 110 | clearTools(): void { 111 | this.tools = {}; 112 | this.nameMapping = {}; 113 | } 114 | } -------------------------------------------------------------------------------- /modules/converse-client-server-sse-demo-docker/client/src/converse/types.ts: -------------------------------------------------------------------------------- 1 | import { ConversationRole } from "@aws-sdk/client-bedrock-runtime"; 2 | 3 | export interface ContentBlock { 4 | text?: string; 5 | toolUse?: ToolUseBlock; 6 | toolResult?: ToolResultBlock; 7 | } 8 | 9 | export interface ToolUseBlock { 10 | toolUseId: string; 11 | name: string; 12 | input: any; 13 | } 14 | 15 | export interface ToolResultBlock { 16 | toolUseId: string; 17 | content: Array<{ text: string }>; 18 | status: 'success' | 'error'; 19 | } 20 | 21 | export interface ConversationMessage { 22 | role: ConversationRole; 23 | content: ContentBlock[]; 24 | } 25 | 26 | export interface ConversationToolSpec { 27 | toolSpec: { 28 | name: string; 29 | description: string; 30 | inputSchema: { 31 | json: { 32 | type: string; 33 | properties: Record; 34 | required?: string[]; 35 | }; 36 | }; 37 | }; 38 | } 39 | 40 | export interface ConversationToolConfig { 41 | getTools(): { tools: ConversationToolSpec[] }; 42 | executeToolAsync(payload: ToolUseBlock): Promise; 43 | } 44 | 45 | export interface ConversationResponse { 46 | output?: { 47 | message: ConversationMessage; 48 | }; 49 | stopReason: 'end_turn' | 'stop_sequence' | 'tool_use' | 'max_tokens'; 50 | usage?: { 51 | inputTokens: number; 52 | outputTokens: number; 53 | totalTokens: number; 54 | }; 55 | metrics?: { 56 | latencyMs: number; 57 | }; 58 | } 59 | 60 | export enum ConversationStopReason { 61 | END_TURN = 'end_turn', 62 | STOP_SEQUENCE = 'stop_sequence', 63 | TOOL_USE = 'tool_use', 64 | MAX_TOKENS = 'max_tokens' 65 | } -------------------------------------------------------------------------------- /modules/converse-client-server-sse-demo-docker/client/src/index.ts: -------------------------------------------------------------------------------- 1 | import { MCPConverseClient } from './MCPConverseClient.js'; 2 | import * as readline from 'readline'; 3 | import chalk from 'chalk'; 4 | 5 | const DEFAULT_SERVER_URL = 'http://mcp-server:8000/sse'; 6 | 7 | async function main() { 8 | const serverUrl = process.env.MCP_SSE_URL || DEFAULT_SERVER_URL; 9 | const client = new MCPConverseClient(serverUrl); 10 | 11 | try { 12 | await client.connect(); 13 | console.log(chalk.cyan('Connected to MCP server')); 14 | console.log(chalk.cyan('Type "quit" or "exit" to end the session\n')); 15 | 16 | const rl = readline.createInterface({ 17 | input: process.stdin, 18 | output: process.stdout 19 | }); 20 | 21 | rl.setPrompt(chalk.blue('> ')); 22 | rl.prompt(); 23 | 24 | rl.on('line', async (line) => { 25 | const input = line.trim(); 26 | 27 | if (input.toLowerCase() === 'exit' || input.toLowerCase() === 'quit') { 28 | await client.close(); 29 | rl.close(); 30 | return; 31 | } 32 | 33 | if (!input) { 34 | rl.prompt(); 35 | return; 36 | } 37 | 38 | await client.processUserInput(input); 39 | rl.prompt(); 40 | }); 41 | 42 | rl.on('close', () => { 43 | console.log(chalk.cyan('\nGoodbye!')); 44 | process.exit(0); 45 | }); 46 | } catch (error) { 47 | console.error(chalk.red('Error:'), error); 48 | process.exit(1); 49 | } 50 | } 51 | 52 | main(); -------------------------------------------------------------------------------- /modules/converse-client-server-sse-demo-docker/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "ESNext", 5 | "lib": ["ES2020"], 6 | "outDir": "./dist", 7 | "rootDir": "./src", 8 | "strict": false, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "resolveJsonModule": true, 13 | "moduleResolution": "bundler", 14 | "verbatimModuleSyntax": true, 15 | "baseUrl": ".", 16 | "paths": { 17 | "@modelcontextprotocol/sdk": ["node_modules/@modelcontextprotocol/sdk/dist/esm/index.js"], 18 | "@modelcontextprotocol/sdk/*": ["node_modules/@modelcontextprotocol/sdk/dist/esm/*"] 19 | } 20 | }, 21 | "include": ["src/**/*"], 22 | "exclude": ["node_modules", "dist", "**/*.test.ts"] 23 | } -------------------------------------------------------------------------------- /modules/converse-client-server-sse-demo-docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | mcp-server: 3 | build: 4 | context: ./server 5 | dockerfile: Dockerfile 6 | ports: 7 | - "8000:8000" 8 | - "8001:8001" 9 | environment: 10 | - NODE_ENV=development 11 | - AWS_ACCESS_KEY_ID 12 | - AWS_SECRET_ACCESS_KEY 13 | - AWS_REGION 14 | - AWS_SESSION_TOKEN=${AWS_SESSION_TOKEN:-} 15 | volumes: 16 | - ./server/src:/app/src 17 | networks: 18 | - mcp-network 19 | 20 | mcp-client: 21 | build: 22 | context: ./client 23 | dockerfile: Dockerfile 24 | target: build 25 | environment: 26 | - NODE_ENV=development 27 | - MCP_SSE_URL=http://mcp-server:8000/sse 28 | - AWS_ACCESS_KEY_ID 29 | - AWS_SECRET_ACCESS_KEY 30 | - AWS_REGION 31 | - AWS_SESSION_TOKEN=${AWS_SESSION_TOKEN:-} 32 | volumes: 33 | - ./client/src:/app/src 34 | networks: 35 | - mcp-network 36 | stdin_open: true 37 | tty: true 38 | 39 | networks: 40 | mcp-network: 41 | driver: bridge -------------------------------------------------------------------------------- /modules/converse-client-server-sse-demo-docker/scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Exit on any error 4 | set -e 5 | 6 | echo "🚀 Building MCP Demo containers..." 7 | 8 | # Function to handle errors 9 | handle_error() { 10 | echo "❌ Error: Build failed!" 11 | exit 1 12 | } 13 | 14 | # Set up error handling 15 | trap 'handle_error' ERR 16 | 17 | # Build server 18 | echo "📦 Building server container..." 19 | if docker-compose build mcp-server; then 20 | echo "✅ Server container built successfully" 21 | else 22 | echo "❌ Server container build failed" 23 | exit 1 24 | fi 25 | 26 | # Build client 27 | echo "📦 Building client container..." 28 | if docker-compose build mcp-client; then 29 | echo "✅ Client container built successfully" 30 | else 31 | echo "❌ Client container build failed" 32 | exit 1 33 | fi 34 | 35 | echo "🎉 All containers built successfully!" -------------------------------------------------------------------------------- /modules/converse-client-server-sse-demo-docker/scripts/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Exit on any error 4 | set -e 5 | 6 | echo "🚀 Starting MCP Demo..." 7 | echo "📦 Getting AWS credentials from your current session..." 8 | 9 | # Get the current role being used and verify AWS access 10 | CURRENT_ROLE=$(aws sts get-caller-identity --query 'Arn' --output text) 11 | if [ $? -ne 0 ]; then 12 | echo "❌ Failed to get AWS credentials. Please check your AWS configuration." 13 | exit 1 14 | fi 15 | echo "🔑 Using AWS Role: $CURRENT_ROLE" 16 | 17 | # Get credentials from current session 18 | echo "🔄 Getting AWS credentials..." 19 | CREDS=$(aws configure export-credentials) 20 | if [ $? -ne 0 ]; then 21 | echo "❌ Failed to export AWS credentials" 22 | exit 1 23 | fi 24 | 25 | # Extract credentials without evaluating them 26 | AWS_ACCESS_KEY_ID=$(echo "$CREDS" | jq -r '.AccessKeyId') 27 | AWS_SECRET_ACCESS_KEY=$(echo "$CREDS" | jq -r '.SecretAccessKey') 28 | AWS_SESSION_TOKEN=$(echo "$CREDS" | jq -r '.SessionToken // empty') 29 | 30 | echo "🌍 Setting AWS Region..." 31 | AWS_REGION=$(aws configure get region) 32 | if [ -z "$AWS_REGION" ]; then 33 | AWS_REGION="us-west-2" 34 | echo "⚠️ No AWS region found in config, defaulting to $AWS_REGION" 35 | else 36 | echo "✅ Using region: $AWS_REGION" 37 | fi 38 | 39 | # Base environment variables that are always needed 40 | DOCKER_ENV="AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID \ 41 | AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY \ 42 | AWS_REGION=$AWS_REGION \ 43 | MCP_SERVER_URL=http://localhost:8000" 44 | 45 | # Only add session token if it exists and is not empty 46 | if [ -n "$AWS_SESSION_TOKEN" ]; then 47 | DOCKER_ENV="$DOCKER_ENV AWS_SESSION_TOKEN=$AWS_SESSION_TOKEN" 48 | fi 49 | 50 | echo "✅ AWS credentials obtained" 51 | 52 | echo "🐳 Starting Docker containers..." 53 | # Use the constructed environment variables 54 | eval "$DOCKER_ENV docker-compose up -d" 55 | 56 | echo "⏳ Waiting for server to be ready..." 57 | until curl -s http://localhost:8000/health > /dev/null; do 58 | sleep 1 59 | done 60 | echo "✅ Server is ready!" 61 | 62 | # Ask if user wants to connect to client app 63 | read -p "Do you want to connect to the client app? (y/N) " -n 1 -r 64 | echo 65 | if [[ $REPLY =~ ^[Yy]$ ]]; then 66 | echo "Connecting to client app..." 67 | echo "Using AWS credentials from: $CURRENT_ROLE" 68 | 69 | docker-compose exec mcp-client npm start 70 | fi 71 | 72 | # Show how to reconnect to the client later 73 | echo "To access the client later, run: docker-compose exec mcp-client npm start" -------------------------------------------------------------------------------- /modules/converse-client-server-sse-demo-docker/scripts/stop.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | echo "🛑 Stopping MCP Demo..." 6 | 7 | # Stop containers 8 | docker-compose down 9 | 10 | echo "✅ All containers stopped successfully!" -------------------------------------------------------------------------------- /modules/converse-client-server-sse-demo-docker/server/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use Node.js LTS slim image as base 2 | FROM node:20-slim 3 | 4 | # Accept build argument for project name 5 | ARG PROJECT_NAME=mcp-server 6 | 7 | # Set working directory 8 | WORKDIR /app 9 | 10 | # Set environment variable from build arg 11 | ENV PROJECT_NAME=${PROJECT_NAME} 12 | 13 | # Copy dependency files first to leverage Docker cache 14 | COPY package*.json ./ 15 | COPY tsconfig.json ./ 16 | 17 | # Copy source code 18 | COPY src ./src 19 | 20 | # Install dependencies and build 21 | RUN npm ci --only=production && \ 22 | # Install dev dependencies for build 23 | npm ci && \ 24 | # Build TypeScript 25 | npm run build && \ 26 | # Remove dev dependencies 27 | npm prune --production 28 | 29 | # Document the ports that will be exposed 30 | EXPOSE 8000 8001 31 | 32 | # Use non-root user for better security 33 | USER node 34 | 35 | # Start the server 36 | CMD ["npm", "start"] -------------------------------------------------------------------------------- /modules/converse-client-server-sse-demo-docker/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mcp-server", 3 | "version": "1.1.0", 4 | "description": "Simple MCP server demonstrating the protocol with a time tool", 5 | "main": "dist/app.js", 6 | "scripts": { 7 | "start": "node dist/app.js", 8 | "dev": "nodemon --exec ts-node src/app.ts", 9 | "build": "tsc", 10 | "clean": "rm -rf dist", 11 | "prebuild": "npm run clean" 12 | }, 13 | "keywords": ["mcp", "fastmcp", "typescript", "example"], 14 | "author": "", 15 | "license": "MIT", 16 | "dependencies": { 17 | "@aws-sdk/client-s3": "^3.535.0", 18 | "fastmcp": "^1.20.2", 19 | "zod": "^3.22.4" 20 | }, 21 | "devDependencies": { 22 | "@types/node": "^20.17.28", 23 | "nodemon": "^3.1.0", 24 | "ts-node": "^10.9.2", 25 | "typescript": "^5.4.2" 26 | }, 27 | "engines": { 28 | "node": ">=20.0.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /modules/converse-client-server-sse-demo-docker/server/src/app.ts: -------------------------------------------------------------------------------- 1 | import { FastMCP, FastMCPSession } from "fastmcp"; 2 | import { env, APP_VERSION } from './config/environment'; 3 | import { registerTools } from './tools'; 4 | import { startHealthServer } from './services/health'; 5 | 6 | // Print startup banner 7 | console.log(` 8 | === ${env.PROJECT_NAME} === 9 | Version: ${APP_VERSION} 10 | Mode: ${env.NODE_ENV} 11 | =================== 12 | `); 13 | 14 | // Create MCP server instance 15 | const server = new FastMCP({ 16 | name: env.PROJECT_NAME, 17 | version: APP_VERSION 18 | }); 19 | 20 | // Handle client connections 21 | server.on("connect", (event: { session: FastMCPSession }) => { 22 | console.log("Client connected"); 23 | 24 | // Log session errors without crashing 25 | event.session.on('error', (event: { error: Error }) => { 26 | console.warn('Session error:', event.error); 27 | }); 28 | }); 29 | 30 | server.on("disconnect", () => { 31 | console.log("Client disconnected"); 32 | }); 33 | 34 | // Register tools and start servers 35 | async function startServer() { 36 | try { 37 | // Register MCP tools 38 | registerTools(server); 39 | 40 | // Start health check endpoint 41 | startHealthServer(); 42 | 43 | // Start MCP server 44 | await server.start({ 45 | transportType: "sse", 46 | sse: { 47 | endpoint: "/sse", 48 | port: 8000 49 | } 50 | }); 51 | 52 | console.log(` 53 | Server is running! 54 | - MCP endpoint: http://localhost:8000/sse 55 | - Health check: http://localhost:8001/health 56 | `); 57 | } catch (error) { 58 | console.error('Failed to start server:', error); 59 | process.exit(1); 60 | } 61 | } 62 | 63 | // Start the server 64 | startServer(); 65 | 66 | // Handle uncaught errors 67 | process.on('uncaughtException', (error: Error) => { 68 | console.error('Uncaught exception:', error); 69 | // Don't exit the process, just log the error 70 | }); 71 | 72 | process.on('unhandledRejection', (error: Error) => { 73 | console.error('Unhandled rejection:', error); 74 | // Don't exit the process, just log the error 75 | }); -------------------------------------------------------------------------------- /modules/converse-client-server-sse-demo-docker/server/src/config/environment.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | // Environment schema validation 4 | const envSchema = z.object({ 5 | PROJECT_NAME: z.string().default('mcp-server'), 6 | NODE_ENV: z.string().default('development') 7 | }); 8 | 9 | // Validate and export environment variables 10 | export const env = envSchema.parse(process.env); 11 | 12 | // Version info 13 | export const APP_VERSION = "1.1.0"; -------------------------------------------------------------------------------- /modules/converse-client-server-sse-demo-docker/server/src/services/health.ts: -------------------------------------------------------------------------------- 1 | import * as http from 'http'; 2 | import { env, APP_VERSION } from '../config/environment'; 3 | 4 | export const startHealthServer = (port: number = 8001): void => { 5 | const healthServer = http.createServer((req: http.IncomingMessage, res: http.ServerResponse) => { 6 | res.writeHead(200, { 'Content-Type': 'application/json' }); 7 | res.end(JSON.stringify({ 8 | status: 'healthy' 9 | })); 10 | }); 11 | 12 | healthServer.listen(port, () => { 13 | console.log(`Health check server listening on port ${port}`); 14 | }); 15 | }; -------------------------------------------------------------------------------- /modules/converse-client-server-sse-demo-docker/server/src/tools/aws/s3BucketCount.ts: -------------------------------------------------------------------------------- 1 | import { S3Client, ListBucketsCommand } from "@aws-sdk/client-s3"; 2 | import { Tool } from 'fastmcp'; 3 | import { z } from 'zod'; 4 | 5 | /** 6 | * Schema definition for the S3 bucket count tool. 7 | * 8 | * This tool takes no parameters as it simply counts all buckets 9 | * in the current AWS account. 10 | */ 11 | export const s3BucketCountSchema = z.object({}); 12 | 13 | // Type inference from the schema 14 | export type S3BucketCountParams = z.infer; 15 | 16 | /** 17 | * S3 Bucket Count Tool 18 | * 19 | * Returns the total number of S3 buckets in the current AWS account. 20 | * This is a read-only operation that does not modify any resources. 21 | * 22 | * Prerequisites: 23 | * - AWS credentials must be configured (via environment variables or AWS CLI) 24 | * - AWS region must be set 25 | * - IAM permissions: s3:ListBuckets 26 | * 27 | * Example usage: 28 | * ```typescript 29 | * const count = await s3BucketCount.execute({}) 30 | * // Returns: "Total S3 buckets: 42" 31 | * ``` 32 | * 33 | * Response format: 34 | * - Returns a string with the total bucket count 35 | * - Throws an error if AWS credentials are invalid or missing 36 | * - Throws an error if IAM permissions are insufficient 37 | */ 38 | export const s3BucketCount: Tool | undefined, typeof s3BucketCountSchema> = { 39 | name: 's3BucketCount', 40 | description: 'Count S3 buckets in AWS account', 41 | parameters: s3BucketCountSchema, 42 | execute: async () => { 43 | console.log('Executing S3 bucket count tool'); 44 | 45 | try { 46 | // Get region from environment variables 47 | const region = process.env.AWS_REGION; 48 | if (!region) { 49 | throw new Error('AWS_REGION environment variable is not set'); 50 | } 51 | 52 | // Create S3 client with explicit region 53 | const client = new S3Client({ 54 | region: region 55 | }); 56 | 57 | // List all buckets 58 | const command = new ListBucketsCommand({}); 59 | const response = await client.send(command); 60 | 61 | // Get bucket count 62 | const count = response.Buckets?.length || 0; 63 | 64 | // Return as a string (FastMCP requirement) 65 | return `Total S3 buckets: ${count}`; 66 | } catch (error) { 67 | if (error instanceof Error) { 68 | // Enhance error message for common issues 69 | if (error.message.includes('credentials')) { 70 | throw new Error('AWS credentials not found or invalid. Please configure AWS credentials.'); 71 | } 72 | if (error.message.includes('AccessDenied')) { 73 | throw new Error('Access denied. Please check IAM permissions (requires s3:ListBuckets).'); 74 | } 75 | throw error; 76 | } 77 | throw new Error('An unknown error occurred while counting S3 buckets.'); 78 | } 79 | } 80 | }; -------------------------------------------------------------------------------- /modules/converse-client-server-sse-demo-docker/server/src/tools/index.ts: -------------------------------------------------------------------------------- 1 | import { FastMCP } from "fastmcp"; 2 | import { timeTool, TimeParams, timeToolSchema } from './time'; 3 | import { s3BucketCount, S3BucketCountParams, s3BucketCountSchema } from './aws/s3BucketCount'; 4 | 5 | // Export tool types for potential reuse 6 | export type { TimeParams, S3BucketCountParams }; 7 | export { timeToolSchema, s3BucketCountSchema }; 8 | 9 | /** 10 | * Register all available tools with the FastMCP server. 11 | * 12 | * This function initializes and registers each tool with the server, 13 | * making them available for use by MCP clients. Tools are registered 14 | * in the order they are listed here. 15 | * 16 | * Current tools: 17 | * - timeTool: Get current time in any timezone 18 | * - s3BucketCount: Count S3 buckets in AWS account 19 | * 20 | * @param server - The FastMCP server instance to register tools with 21 | */ 22 | export const registerTools = (server: FastMCP | undefined>): void => { 23 | console.log('\nRegistering tools...'); 24 | 25 | console.log('- Adding time tool'); 26 | server.addTool(timeTool); 27 | 28 | console.log('- Adding S3 bucket count tool'); 29 | server.addTool(s3BucketCount); 30 | 31 | console.log('All tools registered\n'); 32 | }; -------------------------------------------------------------------------------- /modules/converse-client-server-sse-demo-docker/server/src/tools/time.ts: -------------------------------------------------------------------------------- 1 | import { Tool } from 'fastmcp'; 2 | import { z } from 'zod'; 3 | 4 | /** 5 | * Schema definition for the time tool. 6 | * 7 | * Parameters: 8 | * - timezone (optional): The timezone to format the time in 9 | * 10 | * Note: The schema and description fields are used by MCP clients (LLMs) 11 | * to understand how to use this tool. 12 | */ 13 | export const timeToolSchema = z.object({ 14 | timezone: z.string().optional().describe("The timezone to format the time in (e.g. 'America/New_York', 'Australia/Brisbane')") 15 | }); 16 | 17 | // Type inference from the schema 18 | export type TimeParams = z.infer; 19 | 20 | /** 21 | * Time Tool 22 | * 23 | * Returns the current time formatted according to the specified timezone. 24 | * If no timezone is provided, UTC is used as the default. 25 | * 26 | * Prerequisites: 27 | * - None (uses system time) 28 | * 29 | * Example usage: 30 | * ```typescript 31 | * // Get time in New York 32 | * const nyTime = await timeTool.execute({ timezone: 'America/New_York' }) 33 | * // Returns: "Thursday, March 31, 2024 at 12:34:56 PM EDT" 34 | * 35 | * // Get UTC time (default) 36 | * const utcTime = await timeTool.execute({}) 37 | * // Returns: "Thursday, March 31, 2024 at 4:34:56 PM GMT" 38 | * ``` 39 | * 40 | * Response format: 41 | * - Returns a string with the full date and time in the requested timezone 42 | * - Uses US English locale for consistent formatting 43 | * - Throws an error if the timezone identifier is invalid 44 | */ 45 | export const timeTool: Tool | undefined, typeof timeToolSchema> = { 46 | name: 'time', 47 | description: 'Get the current time in any timezone', 48 | parameters: timeToolSchema, 49 | execute: async (params: TimeParams) => { 50 | console.log('Executing time tool with params:', params); 51 | 52 | // Default to UTC if no timezone provided 53 | const timezone = params.timezone || 'UTC'; 54 | 55 | try { 56 | // Create a date in the requested timezone 57 | const date = new Date(); 58 | const timeString = date.toLocaleString('en-US', { 59 | timeZone: timezone, 60 | dateStyle: 'full', 61 | timeStyle: 'long' 62 | }); 63 | 64 | // Return as a string (FastMCP requirement) 65 | return timeString; 66 | } catch (error) { 67 | if (error instanceof Error && error.message.includes('Invalid time zone')) { 68 | throw new Error(`Invalid timezone: ${timezone}. Use values like 'UTC', 'America/New_York', etc.`); 69 | } 70 | throw error; 71 | } 72 | } 73 | }; -------------------------------------------------------------------------------- /modules/converse-client-server-sse-demo-docker/server/src/types.ts: -------------------------------------------------------------------------------- 1 | interface TextContent { 2 | type: "text"; 3 | text: string; 4 | } 5 | 6 | interface ImageContent { 7 | type: "image"; 8 | url: string; 9 | alt?: string; 10 | } 11 | 12 | type ContentResult = TextContent | ImageContent; 13 | 14 | export interface Tool { 15 | name: string; 16 | description: string; 17 | inputSchema: { 18 | type: string; 19 | properties: Record; 20 | required: string[]; 21 | }; 22 | execute: (input?: any) => Promise; 23 | } -------------------------------------------------------------------------------- /modules/converse-client-server-sse-demo-docker/server/src/types/index.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export interface Tool { 4 | name: string; 5 | description: string; 6 | parameters: z.ZodType; 7 | execute: (params: any) => Promise; 8 | } -------------------------------------------------------------------------------- /modules/converse-client-server-sse-demo-docker/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "lib": ["ES2020", "DOM"], 6 | "outDir": "./dist", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "moduleResolution": "node" 13 | }, 14 | "include": ["src/**/*"], 15 | "exclude": ["node_modules", "dist"] 16 | } -------------------------------------------------------------------------------- /modules/converse-client-server-stdio-demo-local/.gitignore: -------------------------------------------------------------------------------- 1 | # Python 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | *.so 6 | .Python 7 | build/ 8 | develop-eggs/ 9 | dist/ 10 | downloads/ 11 | eggs/ 12 | .eggs/ 13 | lib/ 14 | lib64/ 15 | parts/ 16 | sdist/ 17 | var/ 18 | wheels/ 19 | *.egg-info/ 20 | .installed.cfg 21 | *.egg 22 | 23 | # Virtual Environment 24 | venv/ 25 | env/ 26 | ENV/ 27 | .env 28 | .venv 29 | 30 | # IDE 31 | .idea/ 32 | .vscode/ 33 | *.swp 34 | *.swo 35 | .DS_Store 36 | 37 | # Project specific 38 | mcp_server.log 39 | *.log 40 | .coverage 41 | htmlcov/ 42 | 43 | # Distribution / packaging 44 | .Python 45 | env/ 46 | build/ 47 | develop-eggs/ 48 | dist/ 49 | downloads/ 50 | eggs/ 51 | .eggs/ 52 | lib/ 53 | lib64/ 54 | parts/ 55 | sdist/ 56 | var/ 57 | wheels/ 58 | *.egg-info/ 59 | .installed.cfg 60 | *.egg 61 | 62 | # Unit test / coverage reports 63 | htmlcov/ 64 | .tox/ 65 | .coverage 66 | .coverage.* 67 | .cache 68 | nosetests.xml 69 | coverage.xml 70 | *.cover 71 | .hypothesis/ 72 | .pytest_cache/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # pyenv 78 | .python-version 79 | 80 | # dotenv 81 | .env 82 | 83 | # AWS 84 | .aws/ 85 | credentials 86 | .aws-sam/ 87 | -------------------------------------------------------------------------------- /modules/converse-client-server-stdio-demo-local/README.md: -------------------------------------------------------------------------------- 1 | # MCP Client Bedrock Demo (stdio) 2 | 3 | This project demonstrates an interactive CLI application that uses Model Context Protocol (MCP) with [Amazon Bedrock's Converse AP](https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_Converse.html?trk=64e03f01-b931-4384-846e-db0ba9fa89f5&sc_channel=code). It showcases how to build a chat interface with tool integration using the stdio communication protocol. 4 | 5 | ```mermaid 6 | flowchart LR 7 | User["User"] --> Client["MCP Client"] --"MCP/stdio"--> Server["MCP Server"] 8 | ``` 9 | 10 | ## Features 11 | 12 | - 🤖 Interactive CLI chat 13 | - 🔄 MCP server/client architecture implementation 14 | - 🛠️ Tool integration framework with example tools 15 | - 🔗 Integration with Amazon Bedrock's Converse API 16 | 17 | ## Prerequisites 18 | 19 | - Python 3.8 or higher 20 | - AWS account with Bedrock access (this code uses 'anthropic.claude-3-5-sonnet-20241022-v2:0') 21 | - [Request Model Access Guide](https://docs.aws.amazon.com/bedrock/latest/userguide/model-access.html?trk=64e03f01-b931-4384-846e-db0ba9fa89f5&sc_channel=code) 22 | - AWS credentials configured locally 23 | - An AWS Account with appropriate permissions 24 | 25 | 26 | - `pip` package manager 27 | 28 | ## Project Structure 29 | 30 | ``` 31 | . 32 | └── converse-client-server-stdio-demo-local/ 33 | ├── app.py # Main application entry point with CLI interface 34 | ├── mcp_server.py # MCP server implementation 35 | ├── mcp_client.py # MCP client implementation 36 | ├── converse_agent.py # Conversation agent for managing interactions 37 | ├── converse_tools.py # Tool management system 38 | └── requirements.txt # Project dependencies 39 | ``` 40 | 41 | ## Quick Start 42 | 43 | 1. **Clone the repository** 44 | 45 | 46 | 2. **Set up a virtual environment (recommended):** 47 | ```bash 48 | python -m venv venv 49 | source venv/bin/activate # On Windows: venv\Scripts\activate 50 | ``` 51 | 52 | 3. **Install dependencies:** 53 | ```bash 54 | pip install -r requirements.txt 55 | ``` 56 | 57 | 4. **Configure AWS credentials:** 58 | Ensure your AWS credentials are configured with Bedrock access e.g: 59 | Set up your credentials file at `~/.aws/credentials` 60 | Or: 61 | ```bash 62 | aws configure 63 | ``` 64 | Or use AWS SSO 65 | 66 | 67 | 5. **Run the application:** 68 | ```bash 69 | python app.py 70 | ``` 71 | 72 | ## Example Usage 73 | 74 | The demo includes two built-in tools: 75 | 1. **Calculator**: Performs mathematical calculations 76 | 2. **Weather Tool**: (Demo) Retrieves weather information 77 | 78 | Try these example interactions: 79 | 80 | ```txt 81 | User: What is 10 plus 132? 82 | 83 | User: What is the weather like in Brisbane right now? 84 | 85 | User: What is 123 plus the temperature in Brisbane right now? 86 | ``` 87 | 88 | ## Dependencies 89 | 90 | Core dependencies: 91 | - `fastmcp==0.4.1`: MCP protocol implementation 92 | - `boto3==1.37.19`: AWS SDK for Python 93 | - `botocore==1.37.19`: Low-level AWS functionality 94 | -------------------------------------------------------------------------------- /modules/converse-client-server-stdio-demo-local/app.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from mcp import StdioServerParameters 3 | from converse_agent import ConverseAgent 4 | from converse_tools import ConverseToolManager 5 | from mcp_client import MCPClient 6 | import os 7 | from datetime import datetime 8 | 9 | # ANSI color codes for beautiful output 10 | class Colors: 11 | HEADER = '\033[95m' 12 | BLUE = '\033[94m' 13 | CYAN = '\033[96m' 14 | GREEN = '\033[92m' 15 | YELLOW = '\033[93m' 16 | RED = '\033[91m' 17 | BOLD = '\033[1m' 18 | UNDERLINE = '\033[4m' 19 | END = '\033[0m' 20 | 21 | def clear_screen(): 22 | """Clear the terminal screen using ANSI escape codes""" 23 | print('\033[2J\033[H', end='') 24 | 25 | def print_welcome(): 26 | """Print a welcome message""" 27 | clear_screen() 28 | print(f"{Colors.HEADER}{Colors.BOLD}Welcome to AI Assistant!{Colors.END}") 29 | print(f"{Colors.CYAN}I'm here to help you with any questions or tasks.{Colors.END}") 30 | print(f"{Colors.CYAN}Type 'quit' to exit.{Colors.END}\n") 31 | 32 | def print_tools(tools): 33 | """Print available tools in a nice format""" 34 | print(f"{Colors.CYAN}Available Tools:{Colors.END}") 35 | for tool in tools: 36 | print(f" • {Colors.GREEN}{tool['name']}{Colors.END}: {tool['description']}") 37 | print() # Add a blank line for spacing 38 | 39 | def format_message(role: str, content: str) -> str: 40 | """Format a message with appropriate colors and styling""" 41 | timestamp = datetime.now().strftime("%H:%M:%S") 42 | if role == "user": 43 | return f"{Colors.BLUE}[{timestamp}] You: {Colors.END}{content}" 44 | else: 45 | return f"{Colors.GREEN}Assistant: {Colors.END}{content}" 46 | 47 | async def handle_resource_update(uri: str): 48 | """Handle updates to resources from the MCP server""" 49 | print(f"{Colors.YELLOW}Resource updated: {uri}{Colors.END}") 50 | # You could trigger a refresh of the resource here if needed 51 | 52 | async def main(): 53 | """ 54 | Main function that sets up and runs an interactive AI agent with tool integration. 55 | The agent can process user prompts and utilize registered tools to perform tasks. 56 | """ 57 | # Initialize model configuration 58 | model_id = "anthropic.claude-3-5-sonnet-20241022-v2:0" 59 | 60 | # Set up the agent and tool manager 61 | agent = ConverseAgent(model_id) 62 | agent.tools = ConverseToolManager() 63 | 64 | # Define the agent's behavior through system prompt 65 | agent.system_prompt = """You are a helpful assistant that can use tools to help you answer 66 | questions and perform tasks.""" 67 | 68 | # Create server parameters for stdio configuration 69 | server_params = StdioServerParameters( 70 | command="python", 71 | args=["mcp_server.py"], 72 | env=None 73 | ) 74 | 75 | # Initialize MCP client with server parameters 76 | async with MCPClient(server_params) as mcp_client: 77 | # Register resource update handler 78 | mcp_client.on_resource_update(handle_resource_update) 79 | 80 | # # Fetch and display available resources 81 | # resources = await mcp_client.get_available_resources() 82 | # print("Available resources:", resources) 83 | 84 | # for resource in resources: 85 | # try: 86 | # await mcp_client.session.subscribe_resource(resource.uri) 87 | # except Exception as e: 88 | # print(f"Error subscribing to resource {resource.uri}: {e}") 89 | 90 | # Fetch available tools from the MCP client 91 | tools = await mcp_client.get_available_tools() 92 | 93 | # Register each available tool with the agent 94 | for tool in tools: 95 | agent.tools.register_tool( 96 | name=tool['name'], 97 | func=mcp_client.call_tool, 98 | description=tool['description'], 99 | input_schema={'json': tool['inputSchema']} 100 | ) 101 | 102 | print_welcome() 103 | print_tools(tools) # Print available tools after welcome message 104 | 105 | # Start interactive prompt loop 106 | while True: 107 | try: 108 | # Get user input and check for exit commands 109 | user_prompt = input(f"\n{Colors.BOLD}User: {Colors.END}") 110 | if user_prompt.lower() in ['quit', 'exit', 'q']: 111 | print(f"\n{Colors.CYAN}Goodbye! Thanks for chatting!{Colors.END}") 112 | break 113 | 114 | # Skip empty input 115 | if not user_prompt.strip(): 116 | continue 117 | 118 | # Process the prompt and display the response 119 | print(f"\n{Colors.YELLOW}Thinking...{Colors.END}") 120 | response = await agent.invoke_with_prompt(user_prompt) 121 | print(f"\n{format_message('assistant', response)}") 122 | 123 | except KeyboardInterrupt: 124 | print(f"\n{Colors.CYAN}Goodbye! Thanks for chatting!{Colors.END}") 125 | break 126 | except Exception as e: 127 | print(f"\n{Colors.RED}Error: {str(e)}{Colors.END}") 128 | 129 | if __name__ == "__main__": 130 | # Run the async main function 131 | asyncio.run(main()) -------------------------------------------------------------------------------- /modules/converse-client-server-stdio-demo-local/converse_agent.py: -------------------------------------------------------------------------------- 1 | import boto3, json, re 2 | 3 | class ConverseAgent: 4 | def __init__(self, model_id, region='us-west-2', system_prompt='You are a helpful assistant.'): 5 | self.model_id = model_id 6 | self.region = region 7 | self.client = boto3.client('bedrock-runtime', region_name=self.region) 8 | self.system_prompt = system_prompt 9 | self.messages = [] 10 | self.tools = None 11 | self.response_output_tags = [] # ['', ''] 12 | 13 | async def invoke_with_prompt(self, prompt): 14 | content = [ 15 | { 16 | 'text': prompt 17 | } 18 | ] 19 | return await self.invoke(content) 20 | 21 | async def invoke(self, content): 22 | self.messages.append( 23 | { 24 | "role": "user", 25 | "content": content 26 | } 27 | ) 28 | response = self._get_converse_response() 29 | return await self._handle_response(response) 30 | 31 | def _get_converse_response(self): 32 | """ 33 | https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/bedrock-runtime/client/converse.html 34 | """ 35 | response = self.client.converse( 36 | modelId=self.model_id, 37 | messages=self.messages, 38 | system=[ 39 | { 40 | "text": self.system_prompt 41 | } 42 | ], 43 | inferenceConfig={ 44 | "maxTokens": 8192, 45 | "temperature": 0.7, 46 | }, 47 | toolConfig=self.tools.get_tools() 48 | ) 49 | return(response) 50 | 51 | async def _handle_response(self, response): 52 | # Add the response to the conversation history 53 | self.messages.append(response['output']['message']) 54 | 55 | # Do we need to do anything else? 56 | stop_reason = response['stopReason'] 57 | 58 | if stop_reason in ['end_turn', 'stop_sequence']: 59 | # Safely extract the text from the nested response structure 60 | try: 61 | message = response.get('output', {}).get('message', {}) 62 | content = message.get('content', []) 63 | text = content[0].get('text', '') 64 | if hasattr(self, 'response_output_tags') and len(self.response_output_tags) == 2: 65 | pattern = f"(?s).*{re.escape(self.response_output_tags[0])}(.*?){re.escape(self.response_output_tags[1])}" 66 | match = re.search(pattern, text) 67 | if match: 68 | return match.group(1) 69 | return text 70 | except (KeyError, IndexError): 71 | return '' 72 | 73 | elif stop_reason == 'tool_use': 74 | try: 75 | # Extract tool use details from response 76 | tool_response = [] 77 | for content_item in response['output']['message']['content']: 78 | if 'toolUse' in content_item: 79 | tool_request = { 80 | "toolUseId": content_item['toolUse']['toolUseId'], 81 | "name": content_item['toolUse']['name'], 82 | "input": content_item['toolUse']['input'] 83 | } 84 | 85 | tool_result = await self.tools.execute_tool(tool_request) 86 | tool_response.append({'toolResult': tool_result}) 87 | 88 | return await self.invoke(tool_response) 89 | 90 | except KeyError as e: 91 | raise ValueError(f"Missing required tool use field: {e}") 92 | except Exception as e: 93 | raise ValueError(f"Failed to execute tool: {e}") 94 | 95 | elif stop_reason == 'max_tokens': 96 | # Hit token limit (this is one way to handle it.) 97 | await self.invoke_with_prompt('Please continue.') 98 | 99 | else: 100 | raise ValueError(f"Unknown stop reason: {stop_reason}") 101 | 102 | -------------------------------------------------------------------------------- /modules/converse-client-server-stdio-demo-local/converse_tools.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, List, Callable 2 | import inspect 3 | import json 4 | 5 | class ConverseToolManager: 6 | def __init__(self): 7 | self._tools = {} 8 | self._name_mapping = {} # Maps sanitized names to original names 9 | 10 | def _sanitize_name(self, name: str) -> str: 11 | """Convert hyphenated names to underscore format""" 12 | return name.replace('-', '_') 13 | 14 | def register_tool(self, name: str, func: Callable, description: str, input_schema: Dict): 15 | """ 16 | Register a new tool with the system, sanitizing the name for Bedrock compatibility 17 | """ 18 | sanitized_name = self._sanitize_name(name) 19 | print(f"Registering tool - Original name: {name}, Sanitized name: {sanitized_name}") 20 | self._name_mapping[sanitized_name] = name 21 | self._tools[sanitized_name] = { 22 | 'function': func, 23 | 'description': description, 24 | 'input_schema': input_schema, 25 | 'original_name': name 26 | } 27 | 28 | def get_tools(self) -> Dict[str, List[Dict]]: 29 | """ 30 | Generate the tools specification using sanitized names 31 | """ 32 | tool_specs = [] 33 | for sanitized_name, tool in self._tools.items(): 34 | # Ensure input schema has the correct structure 35 | input_schema = tool['input_schema'] 36 | if 'json' not in input_schema: 37 | input_schema = {'json': input_schema} 38 | if 'type' not in input_schema['json']: 39 | input_schema['json']['type'] = 'object' 40 | if 'properties' not in input_schema['json']: 41 | input_schema['json']['properties'] = {} 42 | if 'required' not in input_schema['json']: 43 | input_schema['json']['required'] = [] 44 | 45 | tool_specs.append({ 46 | 'toolSpec': { 47 | 'name': sanitized_name, # Use sanitized name for Bedrock 48 | 'description': tool['description'], 49 | 'inputSchema': input_schema 50 | } 51 | }) 52 | 53 | return {'tools': tool_specs} 54 | 55 | async def execute_tool(self, payload: Dict[str, Any]) -> Dict[str, Any]: 56 | """ 57 | Execute a tool based on the agent's request, handling name translation 58 | """ 59 | tool_use_id = payload['toolUseId'] 60 | sanitized_name = payload['name'] 61 | tool_input = payload['input'] 62 | 63 | print(f"Executing tool - Requested name: {sanitized_name}") 64 | print(f"Available tools: {list(self._tools.keys())}") 65 | 66 | if sanitized_name not in self._tools: 67 | raise ValueError(f"Unknown tool: {sanitized_name}") 68 | try: 69 | tool_func = self._tools[sanitized_name]['function'] 70 | # Use original name when calling the actual function 71 | original_name = self._tools[sanitized_name]['original_name'] 72 | result = await tool_func(original_name, tool_input) 73 | return { 74 | 'toolUseId': tool_use_id, 75 | 'content': [{ 76 | 'text': str(result) 77 | }], 78 | 'status': 'success' 79 | } 80 | except Exception as e: 81 | return { 82 | 'toolUseId': tool_use_id, 83 | 'content': [{ 84 | 'text': f"Error executing tool: {str(e)}" 85 | }], 86 | 'status': 'error' 87 | } 88 | 89 | def clear_tools(self): 90 | """Clear all registered tools""" 91 | self._tools.clear() 92 | -------------------------------------------------------------------------------- /modules/converse-client-server-stdio-demo-local/mcp_client.py: -------------------------------------------------------------------------------- 1 | from mcp import ClientSession, StdioServerParameters 2 | from mcp.client.stdio import stdio_client 3 | from typing import Any, List 4 | import asyncio 5 | import json 6 | 7 | class MCPClient: 8 | def __init__(self, server_params: StdioServerParameters): 9 | self.server_params = server_params 10 | self.session = None 11 | self._client = None 12 | self._resource_update_callbacks = [] 13 | self._notification_task = None 14 | 15 | async def __aenter__(self): 16 | """Async context manager entry""" 17 | await self.connect() 18 | return self 19 | 20 | async def __aexit__(self, exc_type, exc_val, exc_tb): 21 | """Async context manager exit""" 22 | if self._notification_task: 23 | self._notification_task.cancel() 24 | try: 25 | await self._notification_task 26 | except asyncio.CancelledError: 27 | pass 28 | if self.session: 29 | await self.session.__aexit__(exc_type, exc_val, exc_tb) 30 | if self._client: 31 | await self._client.__aexit__(exc_type, exc_val, exc_tb) 32 | 33 | async def _handle_incoming_messages(self): 34 | """Process incoming messages from the session""" 35 | async for message in self.session.incoming_messages: 36 | if isinstance(message, Exception): 37 | print(f"Error in message handling: {message}") 38 | continue 39 | 40 | if hasattr(message, 'method') and message.method == "resource/updated": 41 | uri = message.params.uri 42 | for callback in self._resource_update_callbacks: 43 | try: 44 | await callback(uri) 45 | except Exception as e: 46 | print(f"Error in resource update callback: {e}") 47 | 48 | async def connect(self): 49 | """Establishes connection to MCP server""" 50 | self._client = stdio_client(self.server_params) 51 | self.read, self.write = await self._client.__aenter__() 52 | session = ClientSession(self.read, self.write) 53 | self.session = await session.__aenter__() 54 | await self.session.initialize() 55 | 56 | # Start message handling task 57 | self._notification_task = asyncio.create_task(self._handle_incoming_messages()) 58 | 59 | # Tools 60 | 61 | async def get_available_tools(self) -> List[Any]: 62 | """List available tools""" 63 | if not self.session: 64 | raise RuntimeError("Not connected to MCP server") 65 | 66 | response = await self.session.list_tools() 67 | print("Raw tools data:", response) # Debug print 68 | 69 | # Extract the actual tools from the response 70 | tools = response.tools if hasattr(response, 'tools') else [] 71 | 72 | # Convert tools to list of dictionaries with expected attributes 73 | formatted_tools = [ 74 | { 75 | 'name': tool.name, 76 | 'description': str(tool.description) if tool.description is not None else "No description available", 77 | 'inputSchema': { 78 | 'json': { 79 | 'type': 'object', 80 | 'properties': tool.inputSchema.get('properties', {}) if tool.inputSchema else {}, 81 | 'required': tool.inputSchema.get('required', []) if tool.inputSchema else [] 82 | } 83 | } 84 | } 85 | for tool in tools 86 | ] 87 | print("Formatted tools:", json.dumps(formatted_tools, indent=2)) # Debug print 88 | return formatted_tools 89 | 90 | async def call_tool(self, tool_name: str, arguments: dict) -> Any: 91 | """Call a tool with given arguments""" 92 | if not self.session: 93 | raise RuntimeError("Not connected to MCP server") 94 | 95 | result = await self.session.call_tool(tool_name, arguments=arguments) 96 | return result 97 | 98 | # Resources 99 | 100 | async def get_available_resources(self) -> List[Any]: 101 | """List available resources""" 102 | if not self.session: 103 | raise RuntimeError("Not connected to MCP server") 104 | 105 | resources = await self.session.list_resources() 106 | _, resources_list = resources 107 | _, resources_list = resources_list 108 | return resources_list 109 | 110 | async def get_resource(self, uri: str) -> Any: 111 | """Get a resource""" 112 | if not self.session: 113 | raise RuntimeError("Not connected to MCP server") 114 | 115 | resource = await self.session.read_resource(uri) 116 | return resource 117 | 118 | def on_resource_update(self, callback): 119 | """Register a callback to be called when resources are updated""" 120 | self._resource_update_callbacks.append(callback) -------------------------------------------------------------------------------- /modules/converse-client-server-stdio-demo-local/mcp_server.py: -------------------------------------------------------------------------------- 1 | from mcp.server.fastmcp import FastMCP 2 | from typing import Dict, Any 3 | import logging 4 | import sys 5 | 6 | # Configure logging to write to both file and stderr 7 | logging.basicConfig( 8 | level=logging.INFO, 9 | format='%(asctime)s - %(levelname)s - %(message)s', 10 | handlers=[ 11 | logging.FileHandler('mcp_server.log'), 12 | logging.StreamHandler(sys.stderr) 13 | ] 14 | ) 15 | logger = logging.getLogger(__name__) 16 | 17 | # Create a FastMCP instance 18 | mcp = FastMCP("Demo Server") 19 | 20 | @mcp.tool() 21 | def calculator(operation: str, x: float, y: float) -> Dict[str, Any]: 22 | """A simple calculator that can add, subtract, multiply, and divide""" 23 | logger.info(f"Calculator called with operation={operation}, x={x}, y={y}") 24 | 25 | result = None 26 | if operation == "add": 27 | result = x + y 28 | elif operation == "subtract": 29 | result = x - y 30 | elif operation == "multiply": 31 | result = x * y 32 | elif operation == "divide": 33 | if y == 0: 34 | logger.error("Division by zero attempted") 35 | raise ValueError("Cannot divide by zero") 36 | result = x / y 37 | 38 | logger.info(f"Calculator result: {result}") 39 | return {"result": result} 40 | 41 | @mcp.tool() 42 | def weather(location: str) -> Dict[str, Any]: 43 | """Get the current weather for a location""" 44 | logger.info(f"Weather tool called for location: {location}") 45 | 46 | # This is a mock implementation 47 | response = { 48 | "temperature": 72, 49 | "condition": "sunny", 50 | "location": location 51 | } 52 | logger.info(f"Weather response: {response}") 53 | return response 54 | 55 | if __name__ == "__main__": 56 | logger.info("Starting MCP Demo Server...") 57 | try: 58 | # Start the server 59 | mcp.run() 60 | except Exception as e: 61 | logger.error(f"Server error: {e}", exc_info=True) 62 | raise -------------------------------------------------------------------------------- /modules/converse-client-server-stdio-demo-local/requirements.txt: -------------------------------------------------------------------------------- 1 | fastmcp==0.4.1 2 | boto3==1.37.19 3 | botocore==1.37.19 -------------------------------------------------------------------------------- /modules/java-mcp-bedrock-agent/.gitattributes: -------------------------------------------------------------------------------- 1 | /gradlew text eol=lf 2 | *.bat text eol=crlf 3 | *.jar binary 4 | -------------------------------------------------------------------------------- /modules/java-mcp-bedrock-agent/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/**/build/ 6 | !**/src/test/**/build/ 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | bin/ 17 | !**/src/main/**/bin/ 18 | !**/src/test/**/bin/ 19 | 20 | ### IntelliJ IDEA ### 21 | .idea 22 | *.iws 23 | *.iml 24 | *.ipr 25 | out/ 26 | !**/src/main/**/out/ 27 | !**/src/test/**/out/ 28 | 29 | ### NetBeans ### 30 | /nbproject/private/ 31 | /nbbuild/ 32 | /dist/ 33 | /nbdist/ 34 | /.nb-gradle/ 35 | 36 | ### VS Code ### 37 | .vscode/ 38 | 39 | ### Kotlin ### 40 | .kotlin 41 | 42 | target/ 43 | -------------------------------------------------------------------------------- /modules/java-mcp-bedrock-agent/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | wrapperVersion=3.3.2 18 | distributionType=only-script 19 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip 20 | -------------------------------------------------------------------------------- /modules/java-mcp-bedrock-agent/README.md: -------------------------------------------------------------------------------- 1 | # Sample: MCP Agent with Bedrock 2 | 3 | > A multi-turn agent using Amazon Bedrock Converse and the MCP Java SDK 4 | 5 | ```mermaid 6 | sequenceDiagram 7 | participant User 8 | participant Application 9 | participant McpClient 10 | participant BedrockRuntimeClient 11 | participant McpServer 12 | participant BedrockService 13 | 14 | Note over Application: Application starts with a query 15 | 16 | Application->>McpClient: initialize() 17 | McpClient->>McpServer: HTTP request for initialization 18 | McpServer-->>McpClient: Return server info 19 | 20 | Application->>McpClient: listTools() 21 | McpClient->>McpServer: HTTP request for tools 22 | McpServer-->>McpClient: Return available tools 23 | 24 | Application->>Application: mcpToolsToConverseTools(tools) 25 | Note over Application: Convert MCP tools to Bedrock Converse tools format 26 | 27 | Application->>BedrockRuntimeClient: create client 28 | 29 | loop Conversation Loop 30 | Application->>BedrockRuntimeClient: converse(request) 31 | BedrockRuntimeClient->>BedrockService: Send converse request 32 | BedrockService-->>BedrockRuntimeClient: Return response 33 | BedrockRuntimeClient-->>Application: Return response 34 | 35 | Application->>User: Display text response 36 | 37 | alt stopReason == TOOL_USE 38 | Note over Application: Model wants to use a tool 39 | 40 | Application->>Application: Extract tool use details 41 | Application->>McpClient: callTool(request) 42 | McpClient->>McpServer: HTTP request to call tool 43 | McpServer-->>McpClient: Return tool result 44 | McpClient-->>Application: Return tool result 45 | 46 | Application->>Application: Create tool result message 47 | Application->>Application: Add to messages array 48 | Note over Application: Continue conversation loop 49 | else stopReason == END_TURN 50 | Note over Application: Conversation complete 51 | Application->>Application: Break loop 52 | end 53 | end 54 | ``` 55 | 56 | ## Setup 57 | 58 | 1. Setup Bedrock in the AWS Console, [request access to Nova Micro](https://us-east-1.console.aws.amazon.com/bedrock/home?region=us-east-1#/modelaccess) 59 | 1. [Setup auth for local development](https://docs.aws.amazon.com/cli/v1/userguide/cli-chap-authentication.html) 60 | 61 | ## Run The Agent 62 | 63 | ``` 64 | ./mvnw compile exec:java 65 | ``` 66 | 67 | Resources: 68 | - https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/java_bedrock-runtime_code_examples.html 69 | - https://docs.aws.amazon.com/bedrock/latest/userguide/tool-use-inference-call.html 70 | -------------------------------------------------------------------------------- /modules/java-mcp-bedrock-agent/mvnw.cmd: -------------------------------------------------------------------------------- 1 | <# : batch portion 2 | @REM ---------------------------------------------------------------------------- 3 | @REM Licensed to the Apache Software Foundation (ASF) under one 4 | @REM or more contributor license agreements. See the NOTICE file 5 | @REM distributed with this work for additional information 6 | @REM regarding copyright ownership. The ASF licenses this file 7 | @REM to you under the Apache License, Version 2.0 (the 8 | @REM "License"); you may not use this file except in compliance 9 | @REM with the License. You may obtain a copy of the License at 10 | @REM 11 | @REM http://www.apache.org/licenses/LICENSE-2.0 12 | @REM 13 | @REM Unless required by applicable law or agreed to in writing, 14 | @REM software distributed under the License is distributed on an 15 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | @REM KIND, either express or implied. See the License for the 17 | @REM specific language governing permissions and limitations 18 | @REM under the License. 19 | @REM ---------------------------------------------------------------------------- 20 | 21 | @REM ---------------------------------------------------------------------------- 22 | @REM Apache Maven Wrapper startup batch script, version 3.3.2 23 | @REM 24 | @REM Optional ENV vars 25 | @REM MVNW_REPOURL - repo url base for downloading maven distribution 26 | @REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 27 | @REM MVNW_VERBOSE - true: enable verbose log; others: silence the output 28 | @REM ---------------------------------------------------------------------------- 29 | 30 | @IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) 31 | @SET __MVNW_CMD__= 32 | @SET __MVNW_ERROR__= 33 | @SET __MVNW_PSMODULEP_SAVE=%PSModulePath% 34 | @SET PSModulePath= 35 | @FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( 36 | IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) 37 | ) 38 | @SET PSModulePath=%__MVNW_PSMODULEP_SAVE% 39 | @SET __MVNW_PSMODULEP_SAVE= 40 | @SET __MVNW_ARG0_NAME__= 41 | @SET MVNW_USERNAME= 42 | @SET MVNW_PASSWORD= 43 | @IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) 44 | @echo Cannot start maven from wrapper >&2 && exit /b 1 45 | @GOTO :EOF 46 | : end batch / begin powershell #> 47 | 48 | $ErrorActionPreference = "Stop" 49 | if ($env:MVNW_VERBOSE -eq "true") { 50 | $VerbosePreference = "Continue" 51 | } 52 | 53 | # calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties 54 | $distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl 55 | if (!$distributionUrl) { 56 | Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" 57 | } 58 | 59 | switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { 60 | "maven-mvnd-*" { 61 | $USE_MVND = $true 62 | $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" 63 | $MVN_CMD = "mvnd.cmd" 64 | break 65 | } 66 | default { 67 | $USE_MVND = $false 68 | $MVN_CMD = $script -replace '^mvnw','mvn' 69 | break 70 | } 71 | } 72 | 73 | # apply MVNW_REPOURL and calculate MAVEN_HOME 74 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 75 | if ($env:MVNW_REPOURL) { 76 | $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } 77 | $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" 78 | } 79 | $distributionUrlName = $distributionUrl -replace '^.*/','' 80 | $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' 81 | $MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" 82 | if ($env:MAVEN_USER_HOME) { 83 | $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" 84 | } 85 | $MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' 86 | $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" 87 | 88 | if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { 89 | Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" 90 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 91 | exit $? 92 | } 93 | 94 | if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { 95 | Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" 96 | } 97 | 98 | # prepare tmp dir 99 | $TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile 100 | $TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" 101 | $TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null 102 | trap { 103 | if ($TMP_DOWNLOAD_DIR.Exists) { 104 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 105 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 106 | } 107 | } 108 | 109 | New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null 110 | 111 | # Download and Install Apache Maven 112 | Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 113 | Write-Verbose "Downloading from: $distributionUrl" 114 | Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 115 | 116 | $webclient = New-Object System.Net.WebClient 117 | if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { 118 | $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) 119 | } 120 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 121 | $webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null 122 | 123 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 124 | $distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum 125 | if ($distributionSha256Sum) { 126 | if ($USE_MVND) { 127 | Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." 128 | } 129 | Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash 130 | if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { 131 | Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." 132 | } 133 | } 134 | 135 | # unzip and move 136 | Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null 137 | Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null 138 | try { 139 | Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null 140 | } catch { 141 | if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { 142 | Write-Error "fail to move MAVEN_HOME" 143 | } 144 | } finally { 145 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 146 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 147 | } 148 | 149 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 150 | -------------------------------------------------------------------------------- /modules/java-mcp-bedrock-agent/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.example 8 | java-mcp-bedrock-agent 9 | 1.0.0-SNAPSHOT 10 | 11 | 12 | UTF-8 13 | 14 | 15 | 16 | 17 | software.amazon.awssdk 18 | bedrockruntime 19 | 2.31.39 20 | 21 | 22 | 23 | io.modelcontextprotocol.sdk 24 | mcp 25 | 0.10.0 26 | 27 | 28 | 29 | org.slf4j 30 | slf4j-simple 31 | 2.0.12 32 | 33 | 34 | 35 | 36 | 37 | 38 | org.apache.maven.plugins 39 | maven-compiler-plugin 40 | 3.13.0 41 | 42 | 21 43 | 21 44 | 45 | 46 | 47 | org.codehaus.mojo 48 | exec-maven-plugin 49 | 3.5.0 50 | 51 | javamcpbedrockagent.Application 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /modules/spring-ai-agent-ecs/.gitattributes: -------------------------------------------------------------------------------- 1 | /gradlew text eol=lf 2 | *.bat text eol=crlf 3 | *.jar binary 4 | -------------------------------------------------------------------------------- /modules/spring-ai-agent-ecs/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/**/build/ 6 | !**/src/test/**/build/ 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | bin/ 17 | !**/src/main/**/bin/ 18 | !**/src/test/**/bin/ 19 | 20 | ### IntelliJ IDEA ### 21 | .idea 22 | *.iws 23 | *.iml 24 | *.ipr 25 | out/ 26 | !**/src/main/**/out/ 27 | !**/src/test/**/out/ 28 | 29 | ### NetBeans ### 30 | /nbproject/private/ 31 | /nbbuild/ 32 | /dist/ 33 | /nbdist/ 34 | /.nb-gradle/ 35 | 36 | ### VS Code ### 37 | .vscode/ 38 | 39 | ### Kotlin ### 40 | .kotlin 41 | 42 | target/ 43 | -------------------------------------------------------------------------------- /modules/spring-ai-agent-ecs/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | wrapperVersion=3.3.2 18 | distributionType=only-script 19 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip 20 | -------------------------------------------------------------------------------- /modules/spring-ai-agent-ecs/README.md: -------------------------------------------------------------------------------- 1 | # Sample: MCP Agent with Spring AI and Bedrock 2 | 3 | Provides a sample Spring AI MCP Server that runs on ECS; which is used by a Spring AI Agent using Bedrock; which also runs on ECS and is exposed publicly via a Load Balancer. 4 | 5 | ```mermaid 6 | flowchart LR 7 | subgraph aws[AWS] 8 | alb[Application Load Balancer] 9 | 10 | subgraph vpc[VPC] 11 | server[MCP Server\nECS Service] 12 | client[MCP Client / Bedrock Agent\nECS Service] 13 | end 14 | 15 | subgraph services[AWS Services] 16 | bedrock[Bedrock] 17 | end 18 | end 19 | 20 | internet((Internet)) 21 | 22 | %% Connections 23 | internet <--> alb 24 | alb --> client 25 | client <--> bedrock 26 | client <--> server 27 | 28 | %% Styling 29 | style aws fill:#f5f5f5,stroke:#232F3E,stroke-width:2px 30 | style vpc fill:#E8F4FA,stroke:#147EBA,stroke-width:2px 31 | style services fill:#E8F4FA,stroke:#147EBA,stroke-width:2px 32 | 33 | style alb fill:#FF9900,color:#fff,stroke:#FF9900 34 | style server fill:#2196f3,color:#fff,stroke:#2196f3 35 | style client fill:#2196f3,color:#fff,stroke:#2196f3 36 | style bedrock fill:#FF9900,color:#fff,stroke:#FF9900 37 | style internet fill:#fff,stroke:#666,stroke-width:2px 38 | 39 | %% Link styling 40 | linkStyle default stroke:#666,stroke-width:2px 41 | ``` 42 | 43 | ## Setup 44 | 45 | 1. Setup Bedrock in the AWS Console, [request access to Nova Pro](https://us-east-1.console.aws.amazon.com/bedrock/home?region=us-east-1#/modelaccess) 46 | 1. [Setup auth for local development](https://docs.aws.amazon.com/cli/v1/userguide/cli-chap-authentication.html) 47 | 48 | ## Run Locally 49 | 50 | Start the MCP Server: 51 | ``` 52 | ./mvnw -pl server spring-boot:run 53 | ``` 54 | 55 | Start the MCP Client / Agent: 56 | ``` 57 | ./mvnw -pl client spring-boot:run 58 | ``` 59 | 60 | Make a request to the server REST endpoint: 61 | 62 | In IntelliJ, open the `client.http` file and run the request. 63 | 64 | Or via `curl`: 65 | ``` 66 | curl -X POST --location "http://localhost:8080/inquire" \ 67 | -H "Content-Type: application/json" \ 68 | -d '{"question": "Get employees that have skills related to Java, but not Java"}' 69 | ``` 70 | 71 | ## Run on AWS 72 | 73 | Prereqs: 74 | - [Create an ECR Repo named `mcp-agent-spring-ai-server` and one named `mcp-agent-spring-ai-client`](https://us-east-1.console.aws.amazon.com/ecr/private-registry/repositories/create?region=us-east-1) 75 | - [Auth `docker` to ECR](https://docs.aws.amazon.com/AmazonECR/latest/userguide/registry_auth.html) 76 | - [Install Rain](https://github.com/aws-cloudformation/rain) 77 | 78 | Build and push the MCP Server & MCP Client to ECR: 79 | ``` 80 | export ECR_REPO=.dkr.ecr.us-east-1.amazonaws.com 81 | 82 | ./mvnw -pl server spring-boot:build-image -Dspring-boot.build-image.imageName=$ECR_REPO/mcp-agent-spring-ai-server 83 | docker push $ECR_REPO/mcp-agent-spring-ai-server:latest 84 | 85 | ./mvnw -pl client spring-boot:build-image -Dspring-boot.build-image.imageName=$ECR_REPO/mcp-agent-spring-ai-client 86 | docker push $ECR_REPO/mcp-agent-spring-ai-client:latest 87 | ``` 88 | 89 | Deploy the Agent: 90 | ``` 91 | rain deploy infra.cfn mcp-agent-spring-ai 92 | ``` 93 | 94 | End-to-end Test with `curl`: 95 | ``` 96 | curl -X POST --location "http://YOUR_LB_HOST/inquire" \ 97 | -H "Content-Type: application/json" \ 98 | -d '{"question": "Get employees that have skills related to Java, but not Java"}' 99 | ``` 100 | -------------------------------------------------------------------------------- /modules/spring-ai-agent-ecs/client.http: -------------------------------------------------------------------------------- 1 | POST http://localhost:8080/inquire 2 | Content-Type: application/json 3 | 4 | {"question": "Get employees that have skills related to Java, but not Java"} 5 | 6 | ### 7 | 8 | POST http://localhost:8080/inquire 9 | Content-Type: application/json 10 | 11 | {"question": "what skills do our employees have?"} 12 | 13 | ### 14 | 15 | POST http://localhost:8080/inquire 16 | Content-Type: application/json 17 | 18 | {"question": "list our employees with React skills"} 19 | -------------------------------------------------------------------------------- /modules/spring-ai-agent-ecs/client/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | com.example 9 | spring-ai-agent-ecs 10 | 1.0.0-SNAPSHOT 11 | 12 | 13 | client 14 | 15 | 16 | 17 | org.springframework.boot 18 | spring-boot-starter-web 19 | 20 | 21 | 22 | org.springframework.ai 23 | spring-ai-starter-model-bedrock-converse 24 | 25 | 26 | 27 | org.springframework.ai 28 | spring-ai-starter-mcp-client 29 | 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-starter-actuator 34 | runtime 35 | 36 | 37 | 38 | 39 | ${project.basedir}/src/main/kotlin 40 | ${project.basedir}/src/test/kotlin 41 | 42 | 43 | -------------------------------------------------------------------------------- /modules/spring-ai-agent-ecs/client/src/main/kotlin/mcpagentspringai/Application.kt: -------------------------------------------------------------------------------- 1 | package mcpagentspringai 2 | 3 | import io.modelcontextprotocol.client.McpSyncClient 4 | import org.springframework.ai.chat.client.ChatClient 5 | import org.springframework.ai.mcp.SyncMcpToolCallbackProvider 6 | import org.springframework.boot.autoconfigure.SpringBootApplication 7 | import org.springframework.boot.runApplication 8 | import org.springframework.context.annotation.Bean 9 | import org.springframework.web.bind.annotation.PostMapping 10 | import org.springframework.web.bind.annotation.RequestBody 11 | import org.springframework.web.bind.annotation.RestController 12 | 13 | 14 | @SpringBootApplication 15 | class Application { 16 | @Bean 17 | fun chatClient(mcpSyncClients: List, builder: ChatClient.Builder): ChatClient = 18 | builder 19 | .defaultToolCallbacks(SyncMcpToolCallbackProvider(mcpSyncClients)) 20 | .build() 21 | } 22 | 23 | data class Prompt(val question: String) 24 | 25 | @RestController 26 | class ConversationalController(val chatClient: ChatClient) { 27 | 28 | @PostMapping("/inquire") 29 | fun inquire(@RequestBody prompt: Prompt): String = 30 | chatClient 31 | .prompt() 32 | .user(prompt.question) 33 | .call() 34 | .content() ?: "Please try again later." 35 | } 36 | 37 | 38 | fun main(args: Array) { 39 | runApplication(*args) 40 | } 41 | -------------------------------------------------------------------------------- /modules/spring-ai-agent-ecs/client/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.ai.bedrock.converse.chat.options.model=amazon.nova-pro-v1:0 2 | spring.ai.bedrock.converse.chat.enabled=true 3 | spring.ai.mcp.client.sse.connections.sample1.url=${mcp-service.url:http://localhost:8081} 4 | -------------------------------------------------------------------------------- /modules/spring-ai-agent-ecs/mvnw.cmd: -------------------------------------------------------------------------------- 1 | <# : batch portion 2 | @REM ---------------------------------------------------------------------------- 3 | @REM Licensed to the Apache Software Foundation (ASF) under one 4 | @REM or more contributor license agreements. See the NOTICE file 5 | @REM distributed with this work for additional information 6 | @REM regarding copyright ownership. The ASF licenses this file 7 | @REM to you under the Apache License, Version 2.0 (the 8 | @REM "License"); you may not use this file except in compliance 9 | @REM with the License. You may obtain a copy of the License at 10 | @REM 11 | @REM http://www.apache.org/licenses/LICENSE-2.0 12 | @REM 13 | @REM Unless required by applicable law or agreed to in writing, 14 | @REM software distributed under the License is distributed on an 15 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | @REM KIND, either express or implied. See the License for the 17 | @REM specific language governing permissions and limitations 18 | @REM under the License. 19 | @REM ---------------------------------------------------------------------------- 20 | 21 | @REM ---------------------------------------------------------------------------- 22 | @REM Apache Maven Wrapper startup batch script, version 3.3.2 23 | @REM 24 | @REM Optional ENV vars 25 | @REM MVNW_REPOURL - repo url base for downloading maven distribution 26 | @REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 27 | @REM MVNW_VERBOSE - true: enable verbose log; others: silence the output 28 | @REM ---------------------------------------------------------------------------- 29 | 30 | @IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) 31 | @SET __MVNW_CMD__= 32 | @SET __MVNW_ERROR__= 33 | @SET __MVNW_PSMODULEP_SAVE=%PSModulePath% 34 | @SET PSModulePath= 35 | @FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( 36 | IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) 37 | ) 38 | @SET PSModulePath=%__MVNW_PSMODULEP_SAVE% 39 | @SET __MVNW_PSMODULEP_SAVE= 40 | @SET __MVNW_ARG0_NAME__= 41 | @SET MVNW_USERNAME= 42 | @SET MVNW_PASSWORD= 43 | @IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) 44 | @echo Cannot start maven from wrapper >&2 && exit /b 1 45 | @GOTO :EOF 46 | : end batch / begin powershell #> 47 | 48 | $ErrorActionPreference = "Stop" 49 | if ($env:MVNW_VERBOSE -eq "true") { 50 | $VerbosePreference = "Continue" 51 | } 52 | 53 | # calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties 54 | $distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl 55 | if (!$distributionUrl) { 56 | Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" 57 | } 58 | 59 | switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { 60 | "maven-mvnd-*" { 61 | $USE_MVND = $true 62 | $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" 63 | $MVN_CMD = "mvnd.cmd" 64 | break 65 | } 66 | default { 67 | $USE_MVND = $false 68 | $MVN_CMD = $script -replace '^mvnw','mvn' 69 | break 70 | } 71 | } 72 | 73 | # apply MVNW_REPOURL and calculate MAVEN_HOME 74 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 75 | if ($env:MVNW_REPOURL) { 76 | $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } 77 | $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" 78 | } 79 | $distributionUrlName = $distributionUrl -replace '^.*/','' 80 | $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' 81 | $MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" 82 | if ($env:MAVEN_USER_HOME) { 83 | $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" 84 | } 85 | $MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' 86 | $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" 87 | 88 | if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { 89 | Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" 90 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 91 | exit $? 92 | } 93 | 94 | if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { 95 | Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" 96 | } 97 | 98 | # prepare tmp dir 99 | $TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile 100 | $TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" 101 | $TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null 102 | trap { 103 | if ($TMP_DOWNLOAD_DIR.Exists) { 104 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 105 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 106 | } 107 | } 108 | 109 | New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null 110 | 111 | # Download and Install Apache Maven 112 | Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 113 | Write-Verbose "Downloading from: $distributionUrl" 114 | Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 115 | 116 | $webclient = New-Object System.Net.WebClient 117 | if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { 118 | $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) 119 | } 120 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 121 | $webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null 122 | 123 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 124 | $distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum 125 | if ($distributionSha256Sum) { 126 | if ($USE_MVND) { 127 | Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." 128 | } 129 | Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash 130 | if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { 131 | Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." 132 | } 133 | } 134 | 135 | # unzip and move 136 | Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null 137 | Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null 138 | try { 139 | Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null 140 | } catch { 141 | if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { 142 | Write-Error "fail to move MAVEN_HOME" 143 | } 144 | } finally { 145 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 146 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 147 | } 148 | 149 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 150 | -------------------------------------------------------------------------------- /modules/spring-ai-agent-ecs/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | org.springframework.boot 9 | spring-boot-starter-parent 10 | 3.5.0 11 | 12 | 13 | 14 | com.example 15 | spring-ai-agent-ecs 16 | 1.0.0-SNAPSHOT 17 | pom 18 | 19 | 20 | client 21 | server 22 | 23 | 24 | 25 | 21 26 | 2.1.21 27 | 1.0.0 28 | UTF-8 29 | 30 | 31 | 32 | 33 | 34 | org.springframework.ai 35 | spring-ai-bom 36 | ${spring-ai.version} 37 | pom 38 | import 39 | 40 | 41 | 42 | 43 | 44 | 45 | org.jetbrains.kotlin 46 | kotlin-reflect 47 | ${kotlin.version} 48 | runtime 49 | 50 | 51 | 52 | org.jetbrains.kotlin 53 | kotlin-stdlib 54 | ${kotlin.version} 55 | 56 | 57 | 58 | 59 | 60 | 61 | org.springframework.boot 62 | spring-boot-maven-plugin 63 | 64 | 65 | org.jetbrains.kotlin 66 | kotlin-maven-plugin 67 | ${kotlin.version} 68 | 69 | ${java.version} 70 | 71 | spring 72 | 73 | 74 | 75 | 76 | org.jetbrains.kotlin 77 | kotlin-maven-allopen 78 | ${kotlin.version} 79 | 80 | 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /modules/spring-ai-agent-ecs/server/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | com.example 9 | spring-ai-agent-ecs 10 | 1.0.0-SNAPSHOT 11 | 12 | 13 | server 14 | 15 | 16 | 17 | org.springframework.ai 18 | spring-ai-starter-mcp-server-webflux 19 | 20 | 21 | 22 | org.springframework.boot 23 | spring-boot-starter-actuator 24 | runtime 25 | 26 | 27 | 28 | 29 | ${project.basedir}/src/main/kotlin 30 | ${project.basedir}/src/test/kotlin 31 | 32 | 33 | -------------------------------------------------------------------------------- /modules/spring-ai-agent-ecs/server/src/main/kotlin/mcpagentspringai/Application.kt: -------------------------------------------------------------------------------- 1 | package mcpagentspringai 2 | 3 | import org.springframework.ai.tool.annotation.Tool 4 | import org.springframework.ai.tool.annotation.ToolParam 5 | import org.springframework.ai.tool.method.MethodToolCallbackProvider 6 | import org.springframework.boot.autoconfigure.SpringBootApplication 7 | import org.springframework.boot.runApplication 8 | import org.springframework.context.annotation.Bean 9 | import org.springframework.stereotype.Service 10 | 11 | @SpringBootApplication 12 | class Application { 13 | @Bean 14 | fun mcpTools(myTools: MyTools): MethodToolCallbackProvider = 15 | MethodToolCallbackProvider.builder().toolObjects(myTools).build() 16 | } 17 | 18 | data class Employee(val name: String, val skills: List) 19 | 20 | @Service 21 | class MyTools { 22 | 23 | @Tool(description = "employees have skills. this returns all possible skills our employees have") 24 | fun getSkills(): Set = 25 | SampleData.employees.flatMap { it.skills }.toSet() 26 | 27 | @Tool(description = "get the employees that have a specific skill") 28 | fun getEmployeesWithSkill(@ToolParam(description = "skill") skill: String): List = 29 | SampleData.employees.filter { employee -> 30 | employee.skills.any { it.equals(skill, ignoreCase = true) } 31 | } 32 | 33 | } 34 | 35 | fun main(args: Array) { 36 | SampleData.employees.forEach { println(it) } 37 | runApplication(*args) 38 | } 39 | -------------------------------------------------------------------------------- /modules/spring-ai-agent-ecs/server/src/main/kotlin/mcpagentspringai/SampleData.kt: -------------------------------------------------------------------------------- 1 | package mcpagentspringai 2 | 3 | import kotlin.random.Random 4 | 5 | object SampleData { 6 | 7 | private val firstNames = listOf("James", "Mary", "John", "Patricia", "Robert", "Jennifer", "Michael", "Linda", "William", "Elizabeth") 8 | private val lastNames = listOf("Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis", "Rodriguez", "Martinez") 9 | 10 | val skills = listOf( 11 | "Kotlin", "Java", "Python", "JavaScript", "TypeScript", 12 | "React", "Angular", "Spring Boot", "AWS", "Docker", 13 | "Kubernetes", "SQL", "MongoDB", "Git", "CI/CD", 14 | "Machine Learning", "DevOps", "Node.js", "REST API", "GraphQL" 15 | ) 16 | 17 | val employees = List(100) { 18 | Employee( 19 | name = firstNames.random() + " " + lastNames.random(), 20 | skills = List(Random.nextInt(2, 6)) { skills.random() }.distinct() 21 | ) 22 | }.distinctBy { it.name } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /modules/spring-ai-agent-ecs/server/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | server.port=8081 2 | -------------------------------------------------------------------------------- /modules/spring-ai-java-bedrock-mcp-rag/.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | target/ 3 | pom.xml.tag 4 | pom.xml.releaseBackup 5 | pom.xml.versionsBackup 6 | pom.xml.next 7 | release.properties 8 | dependency-reduced-pom.xml 9 | buildNumber.properties 10 | .mvn/timing.properties 11 | # https://github.com/takari/maven-wrapper#usage-without-binary-jar 12 | .mvn/wrapper/maven-wrapper.jar 13 | 14 | # Eclipse m2e generated files 15 | # Eclipse Core 16 | .project 17 | # JDT-specific (Eclipse Java Development Tools) 18 | .classpath 19 | -------------------------------------------------------------------------------- /modules/spring-ai-java-bedrock-mcp-rag/README.md: -------------------------------------------------------------------------------- 1 | # Sample: Spring AI with Bedrock and MCP 2 | 3 | A Spring Boot application that provides an AI-powered dog adoption service using: 4 | - Amazon Bedrock for AI/ML capabilities 5 | - Spring AI for conversation management 6 | - PostgreSQL with pgvector for vector storage 7 | - Two services: 8 | - Adoptions service: Handles dog adoption inquiries 9 | - Scheduling service: MCP Server that manages adoption appointments 10 | 11 | ## Architecture 12 | 13 | ```mermaid 14 | sequenceDiagram 15 | actor User 16 | participant Controller as ConversationalController 17 | participant Memory as ChatMemory 18 | participant RAG as QuestionAnswerAdvisor 19 | participant Vector as VectorStore 20 | participant Chat as ChatClient 21 | participant MCP as MCPSyncClient 22 | participant AI as Bedrock AI 23 | 24 | User->>Controller: POST /{id}/inquire 25 | 26 | alt New conversation 27 | Controller->>Memory: computeIfAbsent(id) 28 | Memory-->>Controller: Create new PromptChatMemoryAdvisor 29 | end 30 | 31 | par RAG Process 32 | Controller->>RAG: Process question 33 | RAG->>Vector: Search relevant context 34 | Vector-->>RAG: Return matching embeddings 35 | RAG-->>Controller: Return augmented prompt 36 | and Memory Management 37 | Controller->>Memory: Get conversation history 38 | Memory-->>Controller: Return chat context 39 | end 40 | 41 | Controller->>Chat: prompt().user(question) 42 | 43 | Chat->>MCP: Synchronous tool callback 44 | MCP-->>Chat: Return tool results 45 | 46 | Chat->>AI: Send augmented prompt + context 47 | AI-->>Chat: Generate response 48 | 49 | Chat-->>Controller: Return content 50 | Controller->>Memory: Store conversation 51 | Controller-->>User: Return response 52 | 53 | Note over RAG,Vector: Retrieval Augmented Generation 54 | Note over Memory: Maintains conversation state 55 | Note over MCP: Handles scheduled operations 56 | ``` 57 | 58 | ## Setup 59 | 60 | To run locally you will need: 61 | - JDK 23 or higher 62 | - Docker 63 | 64 | 1. Setup Bedrock in the AWS Console, [request access to Nova Lite and Cohere Embed Multilingual](https://us-east-1.console.aws.amazon.com/bedrock/home?region=us-east-1#/modelaccess) 65 | 1. [Setup auth for local development](https://docs.aws.amazon.com/cli/v1/userguide/cli-chap-authentication.html) 66 | 67 | Build the Scheduling MCP Server as a Docker container: 68 | ``` 69 | cd scheduling && ./mvnw spring-boot:build-image && cd .. 70 | ``` 71 | Alternatively, for faster MCP server startup, create a GraalVM Native Image container: 72 | ``` 73 | cd scheduling && ./mvnw -Pnative spring-boot:build-image && cd .. 74 | ``` 75 | 76 | ## Running 77 | 78 | This sample includes tests and a "test" main application which will start the dependency services (postgres with pgvector and the scheduling MCP server) in Docker with Testcontainers. 79 | 80 | First make sure you are in the `adoptions` directory: 81 | ``` 82 | cd adoptions 83 | ``` 84 | 85 | Run the tests: 86 | ``` 87 | ./mvnw test 88 | ``` 89 | 90 | Run the "adoptions" server: 91 | ``` 92 | ./mvnw spring-boot:test-run 93 | ``` 94 | 95 | With the server started you can now make requests to the server. 96 | In IntelliJ, open the `resources/client.http` file and run the two requests. 97 | Or via `curl`: 98 | ``` 99 | curl -X POST --location "http://localhost:8080/2/inquire" \ 100 | -H "Content-Type: application/x-www-form-urlencoded" \ 101 | -d 'question=Do+you+have+any+neurotic+dogs%3F' 102 | ``` 103 | -------------------------------------------------------------------------------- /modules/spring-ai-java-bedrock-mcp-rag/adoptions/.gitattributes: -------------------------------------------------------------------------------- 1 | /mvnw text eol=lf 2 | *.cmd text eol=crlf 3 | -------------------------------------------------------------------------------- /modules/spring-ai-java-bedrock-mcp-rag/adoptions/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | -------------------------------------------------------------------------------- /modules/spring-ai-java-bedrock-mcp-rag/adoptions/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | wrapperVersion=3.3.2 18 | distributionType=only-script 19 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip 20 | -------------------------------------------------------------------------------- /modules/spring-ai-java-bedrock-mcp-rag/adoptions/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 3.5.0 9 | 10 | 11 | com.example 12 | adoptions 13 | 0.0.1-SNAPSHOT 14 | adoptions 15 | Demo project for Spring Boot 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 23 31 | 1.0.0 32 | 33 | 34 | 35 | org.springframework.ai 36 | spring-ai-starter-mcp-client 37 | 38 | 39 | org.springframework.ai 40 | spring-ai-starter-model-bedrock-converse 41 | 42 | 43 | org.springframework.ai 44 | spring-ai-starter-model-bedrock 45 | 46 | 47 | org.springframework.boot 48 | spring-boot-starter-actuator 49 | 50 | 51 | org.springframework.boot 52 | spring-boot-starter-data-jdbc 53 | 54 | 55 | org.springframework.boot 56 | spring-boot-starter-web 57 | 58 | 59 | org.springframework.ai 60 | spring-ai-advisors-vector-store 61 | 62 | 63 | org.springframework.ai 64 | spring-ai-starter-vector-store-pgvector 65 | 66 | 67 | org.postgresql 68 | postgresql 69 | runtime 70 | 71 | 72 | 73 | org.springframework.boot 74 | spring-boot-starter-test 75 | test 76 | 77 | 78 | org.testcontainers 79 | junit-jupiter 80 | test 81 | 82 | 83 | org.springframework.boot 84 | spring-boot-testcontainers 85 | test 86 | 87 | 88 | org.testcontainers 89 | postgresql 90 | test 91 | 92 | 93 | 94 | org.springframework.boot 95 | spring-boot-devtools 96 | test 97 | true 98 | 99 | 100 | 101 | 102 | 103 | org.springframework.ai 104 | spring-ai-bom 105 | ${spring-ai.version} 106 | pom 107 | import 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | org.graalvm.buildtools 116 | native-maven-plugin 117 | 118 | 119 | org.springframework.boot 120 | spring-boot-maven-plugin 121 | 122 | 123 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /modules/spring-ai-java-bedrock-mcp-rag/adoptions/src/main/java/com/example/adoptions/AdoptionsApplication.java: -------------------------------------------------------------------------------- 1 | package com.example.adoptions; 2 | 3 | import io.modelcontextprotocol.client.McpSyncClient; 4 | import org.springframework.ai.chat.client.ChatClient; 5 | import org.springframework.ai.chat.client.advisor.vectorstore.QuestionAnswerAdvisor; 6 | import org.springframework.ai.chat.client.advisor.vectorstore.VectorStoreChatMemoryAdvisor; 7 | import org.springframework.ai.chat.memory.ChatMemory; 8 | import org.springframework.ai.mcp.SyncMcpToolCallbackProvider; 9 | import org.springframework.ai.vectorstore.VectorStore; 10 | import org.springframework.boot.SpringApplication; 11 | import org.springframework.boot.autoconfigure.SpringBootApplication; 12 | import org.springframework.context.annotation.Bean; 13 | import org.springframework.context.annotation.Configuration; 14 | import org.springframework.data.annotation.Id; 15 | import org.springframework.data.repository.ListCrudRepository; 16 | import org.springframework.stereotype.Controller; 17 | import org.springframework.web.bind.annotation.PathVariable; 18 | import org.springframework.web.bind.annotation.PostMapping; 19 | import org.springframework.web.bind.annotation.RequestParam; 20 | import org.springframework.web.bind.annotation.ResponseBody; 21 | 22 | import java.util.List; 23 | 24 | /** 25 | * @author James Ward 26 | * @author Josh Long 27 | */ 28 | @SpringBootApplication 29 | public class AdoptionsApplication { 30 | public static void main(String[] args) { 31 | SpringApplication.run(AdoptionsApplication.class, args); 32 | } 33 | } 34 | 35 | @Configuration 36 | class ConversationalConfiguration { 37 | 38 | @Bean 39 | ChatClient chatClient( 40 | List mcpSyncClients, 41 | ChatClient.Builder builder, 42 | VectorStore vectorStore) { 43 | 44 | var system = """ 45 | You are an AI powered assistant to help people adopt a dog from the adoption\s 46 | agency named Pooch Palace with locations in Atlanta, Antwerp, Seoul, Tokyo, Singapore, Paris,\s 47 | Mumbai, New Delhi, Barcelona, San Francisco, and London. Information about the dogs available\s 48 | will be presented below. If there is no information, then return a polite response suggesting we\s 49 | don't have any dogs available. 50 | 51 | If the response involves a timestamp, be sure to convert it to something human-readable. 52 | 53 | Do _not_ include any indication of what you're thinking. Nothing should be sent to the client between tags. 54 | Just give the answer. 55 | """; 56 | 57 | return builder 58 | .defaultSystem(system) 59 | .defaultToolCallbacks(new SyncMcpToolCallbackProvider(mcpSyncClients)) 60 | .defaultAdvisors(VectorStoreChatMemoryAdvisor.builder(vectorStore).build()) 61 | .build(); 62 | } 63 | 64 | } 65 | 66 | interface DogRepository extends ListCrudRepository { 67 | } 68 | 69 | record Dog(@Id int id, String name, String owner, String description) { 70 | } 71 | 72 | 73 | @Controller 74 | @ResponseBody 75 | class ConversationalController { 76 | 77 | private final ChatClient chatClient; 78 | private final QuestionAnswerAdvisor questionAnswerAdvisor; 79 | 80 | ConversationalController(VectorStore vectorStore, ChatClient chatClient) { 81 | this.chatClient = chatClient; 82 | this.questionAnswerAdvisor = new QuestionAnswerAdvisor(vectorStore); 83 | } 84 | 85 | @PostMapping("/{id}/inquire") 86 | String inquire(@PathVariable String id, @RequestParam String question) { 87 | return chatClient 88 | .prompt() 89 | .user(question) 90 | .advisors(questionAnswerAdvisor) 91 | .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, id)) 92 | .call() 93 | .content(); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /modules/spring-ai-java-bedrock-mcp-rag/adoptions/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.application.name=service 2 | # 3 | # amazing amazon bedrock 4 | # nb: you'll also need to specify spring.ai.bedrock.aws.access-key and spring.ai.bedrock.aws.secret-key 5 | spring.ai.bedrock.converse.chat.options.model=amazon.nova-lite-v1:0 6 | spring.ai.bedrock.converse.chat.enabled=true 7 | spring.ai.model.embedding=bedrock-cohere 8 | # 9 | # matches the embedding model's dimensions 10 | spring.ai.vectorstore.pgvector.dimensions=1024 11 | spring.ai.vectorstore.pgvector.initialize-schema=true 12 | 13 | spring.ai.mcp.client.sse.connections.scheduling.url=${scheduling-service.url} 14 | -------------------------------------------------------------------------------- /modules/spring-ai-java-bedrock-mcp-rag/adoptions/src/test/java/com/example/adoptions/DogTest.java: -------------------------------------------------------------------------------- 1 | package com.example.adoptions; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.ai.vectorstore.VectorStore; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.test.context.SpringBootTest; 7 | 8 | import static org.junit.jupiter.api.Assertions.assertEquals; 9 | 10 | @SpringBootTest(properties = "spring.sql.init.mode=always") 11 | public class DogTest { 12 | 13 | @Autowired 14 | private DogRepository dogRepository; 15 | 16 | @Autowired 17 | private VectorStore vectorStore; 18 | 19 | @Test 20 | void dogsDB() { 21 | var dogs = dogRepository.findAll(); 22 | assertEquals(10, dogs.size()); 23 | } 24 | 25 | @Test 26 | void dogsVector() { 27 | var doguments = vectorStore.similaritySearch("prancer"); 28 | assertEquals(4, doguments.size()); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /modules/spring-ai-java-bedrock-mcp-rag/adoptions/src/test/java/com/example/adoptions/ServerTest.java: -------------------------------------------------------------------------------- 1 | package com.example.adoptions; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | import org.springframework.boot.test.web.server.LocalServerPort; 6 | import org.springframework.http.ResponseEntity; 7 | import org.springframework.web.client.RestClient; 8 | 9 | import java.time.Duration; 10 | import java.time.Instant; 11 | import java.time.ZoneId; 12 | import java.time.ZonedDateTime; 13 | import java.time.format.DateTimeFormatter; 14 | import java.util.Locale; 15 | 16 | import static org.assertj.core.api.AssertionsForClassTypes.assertThat; 17 | 18 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, properties = "spring.sql.init.mode=always") 19 | public class ServerTest { 20 | 21 | @LocalServerPort 22 | private int port; 23 | 24 | @Test 25 | void adoptDog() { 26 | var prancer = inquire("Do you have any neurotic dogs?"); 27 | assertThat(prancer.getStatusCode().is2xxSuccessful()).isTrue(); 28 | assertThat(prancer.getBody()).contains("Prancer"); 29 | 30 | var schedule = inquire("fantastic. when could i schedule an appointment to adopt Prancer, from the London location?"); 31 | assertThat(schedule.getStatusCode().is2xxSuccessful()).isTrue(); 32 | var utcNow = ZonedDateTime.now(ZoneId.of("UTC")); 33 | var utcPlusThreeDays = utcNow.plusDays(3); 34 | var formatter = DateTimeFormatter.ofPattern("MMMM d", Locale.ENGLISH); 35 | String formattedDate = utcPlusThreeDays.format(formatter); 36 | 37 | assertThat(schedule.getBody()).contains(formattedDate); 38 | } 39 | 40 | private ResponseEntity inquire(String question) { 41 | var uri = "http://localhost:" + this.port + "/jwjl/inquire"; 42 | var rc = RestClient 43 | .builder() 44 | .baseUrl(uri) 45 | .build(); 46 | return rc 47 | .post() 48 | .uri(builder -> builder.queryParam("question", question).build()) 49 | .retrieve() 50 | .toEntity(String.class); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /modules/spring-ai-java-bedrock-mcp-rag/adoptions/src/test/java/com/example/adoptions/TestAdoptionsApplication.java: -------------------------------------------------------------------------------- 1 | package com.example.adoptions; 2 | 3 | import org.slf4j.LoggerFactory; 4 | import org.springframework.ai.document.Document; 5 | import org.springframework.ai.vectorstore.VectorStore; 6 | import org.springframework.boot.ApplicationRunner; 7 | import org.springframework.boot.SpringApplication; 8 | import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; 9 | import org.springframework.boot.devtools.restart.RestartScope; 10 | import org.springframework.boot.testcontainers.service.connection.ServiceConnection; 11 | import org.springframework.context.annotation.Bean; 12 | import org.springframework.context.annotation.Configuration; 13 | import org.springframework.test.context.DynamicPropertyRegistrar; 14 | import org.testcontainers.containers.GenericContainer; 15 | import org.testcontainers.containers.PostgreSQLContainer; 16 | import org.testcontainers.containers.output.Slf4jLogConsumer; 17 | import org.testcontainers.containers.wait.strategy.Wait; 18 | import org.testcontainers.junit.jupiter.Testcontainers; 19 | import org.testcontainers.utility.DockerImageName; 20 | 21 | import java.util.List; 22 | import java.util.Map; 23 | 24 | public class TestAdoptionsApplication { 25 | 26 | public static void main(String[] args) { 27 | System.setProperty("spring.sql.init.mode", "always"); 28 | SpringApplication.run(AdoptionsApplication.class, args); 29 | } 30 | } 31 | 32 | @Configuration 33 | @Testcontainers 34 | class TestContainersConfiguration { 35 | 36 | @Bean 37 | @ServiceConnection 38 | @RestartScope 39 | PostgreSQLContainer postgreSQLContainer() { 40 | var image = DockerImageName.parse("pgvector/pgvector:pg16") 41 | .asCompatibleSubstituteFor("postgres"); 42 | return new PostgreSQLContainer<>(image); 43 | } 44 | 45 | } 46 | 47 | @Configuration 48 | @ConditionalOnExpression("'${scheduling-service.url:}'.empty") // only start the scheduling container if the url isn't set 49 | @Testcontainers 50 | class SchedulingServiceConfiguration { 51 | 52 | @Bean 53 | @RestartScope 54 | GenericContainer schedulingService() { 55 | return new GenericContainer<>(DockerImageName.parse("scheduling")) 56 | .withExposedPorts(8081) 57 | .waitingFor(Wait.forHttp("/sse")) 58 | .withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger(getClass()))); 59 | } 60 | 61 | @Bean 62 | DynamicPropertyRegistrar adoptionServiceProperties(GenericContainer schedulingService) { 63 | return (properties) -> 64 | properties.add("scheduling-service.url", () -> "http://localhost:" + schedulingService.getFirstMappedPort()); 65 | } 66 | 67 | } 68 | 69 | @Configuration 70 | class DogDataInitializerConfiguration { 71 | 72 | @Bean 73 | ApplicationRunner initializerRunner(VectorStore vectorStore, 74 | DogRepository dogRepository) { 75 | return _ -> { 76 | if (dogRepository.count() == 0) { 77 | System.out.println("initializing vector store"); 78 | var map = Map.of( 79 | "Jasper", "A grey Shih Tzu known for being protective.", 80 | "Toby", "A grey Doberman known for being playful.", 81 | "Nala", "A spotted German Shepherd known for being loyal.", 82 | "Penny", "A white Great Dane known for being protective.", 83 | "Bella", "A golden Poodle known for being calm.", 84 | "Willow", "A brindle Great Dane known for being calm.", 85 | "Daisy", "A spotted Poodle known for being affectionate.", 86 | "Mia", "A grey Great Dane known for being loyal.", 87 | "Molly", "A golden Chihuahua known for being curious.", 88 | "Prancer", "A demonic, neurotic, man hating, animal hating, children hating dogs that look like gremlins." 89 | ); 90 | map.forEach((name, description) -> { 91 | var dog = dogRepository.save(new Dog(0, name, null, description)); 92 | var dogument = new Document("id: %s, name: %s, description: %s".formatted(dog.id(), dog.name(), dog.description())); 93 | vectorStore.add(List.of(dogument)); 94 | }); 95 | System.out.println("finished initializing vector store"); 96 | } 97 | 98 | }; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /modules/spring-ai-java-bedrock-mcp-rag/adoptions/src/test/resources/schema.sql: -------------------------------------------------------------------------------- 1 | CREATE SEQUENCE IF NOT EXISTS dog_id_seq 2 | AS integer 3 | START WITH 1 4 | INCREMENT BY 1 5 | NO MINVALUE 6 | NO MAXVALUE CACHE 1; 7 | 8 | CREATE TABLE IF NOT EXISTS dog 9 | ( 10 | id integer DEFAULT nextval('dog_id_seq') PRIMARY KEY, 11 | name text NOT NULL, 12 | owner text, 13 | description text NOT NULL 14 | ); 15 | -------------------------------------------------------------------------------- /modules/spring-ai-java-bedrock-mcp-rag/resources/client.http: -------------------------------------------------------------------------------- 1 | ### 2 | POST localhost:8080/2/inquire 3 | Content-Type: application/x-www-form-urlencoded 4 | 5 | question=Do you have any neurotic dogs? 6 | 7 | ### 8 | 9 | POST localhost:8080/2/inquire 10 | Content-Type: application/x-www-form-urlencoded 11 | 12 | question=fantastic. how soon can i schedule an appointment to adopt Prancer? 13 | -------------------------------------------------------------------------------- /modules/spring-ai-java-bedrock-mcp-rag/scheduling/.gitattributes: -------------------------------------------------------------------------------- 1 | /mvnw text eol=lf 2 | *.cmd text eol=crlf 3 | -------------------------------------------------------------------------------- /modules/spring-ai-java-bedrock-mcp-rag/scheduling/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | -------------------------------------------------------------------------------- /modules/spring-ai-java-bedrock-mcp-rag/scheduling/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | wrapperVersion=3.3.2 18 | distributionType=only-script 19 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip 20 | -------------------------------------------------------------------------------- /modules/spring-ai-java-bedrock-mcp-rag/scheduling/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 3.5.0 9 | 10 | 11 | com.example 12 | scheduling 13 | 0.0.1-SNAPSHOT 14 | scheduling 15 | Demo project for Spring Boot 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 23 31 | 1.0.0 32 | 33 | 34 | 35 | org.springframework.ai 36 | spring-ai-starter-mcp-server-webflux 37 | 38 | 39 | 40 | 41 | 42 | org.springframework.ai 43 | spring-ai-bom 44 | ${spring-ai.version} 45 | pom 46 | import 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | org.springframework.boot 55 | spring-boot-maven-plugin 56 | 57 | 58 | 59 | ${project.artifactId}:latest 60 | 61 | 62 | 24 63 | 64 | 65 | 66 | 67 | 68 | org.graalvm.buildtools 69 | native-maven-plugin 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /modules/spring-ai-java-bedrock-mcp-rag/scheduling/src/main/java/com/example/adoptions/SchedulingApplication.java: -------------------------------------------------------------------------------- 1 | package com.example.adoptions; 2 | 3 | import org.springframework.ai.tool.ToolCallbackProvider; 4 | import org.springframework.ai.tool.annotation.Tool; 5 | import org.springframework.ai.tool.annotation.ToolParam; 6 | import org.springframework.ai.tool.method.MethodToolCallbackProvider; 7 | import org.springframework.boot.SpringApplication; 8 | import org.springframework.boot.autoconfigure.SpringBootApplication; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.stereotype.Service; 11 | 12 | import java.time.Instant; 13 | import java.time.temporal.ChronoUnit; 14 | 15 | @SpringBootApplication 16 | public class SchedulingApplication { 17 | 18 | public static void main(String[] args) { 19 | SpringApplication.run(SchedulingApplication.class, args); 20 | } 21 | 22 | @Bean 23 | ToolCallbackProvider serviceToolCallbackProvider( 24 | DogAdoptionAppointmentScheduler scheduler) { 25 | return MethodToolCallbackProvider.builder() 26 | .toolObjects(scheduler) 27 | .build(); 28 | } 29 | 30 | } 31 | 32 | 33 | @Service 34 | class DogAdoptionAppointmentScheduler { 35 | 36 | @Tool(description = "schedule an appointment to adopt a dog" + 37 | " at the Pooch Palace dog adoption agency") 38 | String scheduleDogAdoptionAppointment( 39 | @ToolParam(description = "the id of the dog") String id, 40 | @ToolParam(description = "the name of the dog") String name) { 41 | var instant = Instant.now().plus(3, ChronoUnit.DAYS); 42 | System.out.println("confirming the appointment: " + instant + " for dog " + id + " named " + name); 43 | return instant.toString(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /modules/spring-ai-java-bedrock-mcp-rag/scheduling/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.application.name=service 2 | server.port=8081 3 | -------------------------------------------------------------------------------- /modules/spring-ai-mcp-inter-agent-ecs/.gitattributes: -------------------------------------------------------------------------------- 1 | /gradlew text eol=lf 2 | *.bat text eol=crlf 3 | *.jar binary 4 | -------------------------------------------------------------------------------- /modules/spring-ai-mcp-inter-agent-ecs/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/**/build/ 6 | !**/src/test/**/build/ 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | bin/ 17 | !**/src/main/**/bin/ 18 | !**/src/test/**/bin/ 19 | 20 | ### IntelliJ IDEA ### 21 | .idea 22 | *.iws 23 | *.iml 24 | *.ipr 25 | out/ 26 | !**/src/main/**/out/ 27 | !**/src/test/**/out/ 28 | 29 | ### NetBeans ### 30 | /nbproject/private/ 31 | /nbbuild/ 32 | /dist/ 33 | /nbdist/ 34 | /.nb-gradle/ 35 | 36 | ### VS Code ### 37 | .vscode/ 38 | 39 | ### Kotlin ### 40 | .kotlin 41 | 42 | target/ 43 | -------------------------------------------------------------------------------- /modules/spring-ai-mcp-inter-agent-ecs/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | wrapperVersion=3.3.2 18 | distributionType=only-script 19 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip 20 | -------------------------------------------------------------------------------- /modules/spring-ai-mcp-inter-agent-ecs/README.md: -------------------------------------------------------------------------------- 1 | # Sample: MCP Inter-Agent with Spring AI and Bedrock 2 | 3 | MCP-based agents (Bedrock + MCP client) can call other MCP-based agents by exposing them as MCP servers. 4 | This example uses a hierarchy of agents with where the outer agent calls (using MCP) an inner agent that does routing and prompt modification, which then calls another MCP server. 5 | 6 | ```mermaid 7 | sequenceDiagram 8 | participant User 9 | participant REST as REST API Endpoint 10 | participant Agent1 as Agent1
(with MCP Client) 11 | participant Agent2 as Agent2
(with MCP Client & Server) 12 | participant Bedrock as Amazon Bedrock
(Nova Pro) 13 | participant MCP_Server as MCP Server 14 | 15 | User->>REST: POST /inquire
(Question about employees) 16 | REST->>Agent1: Forward question 17 | 18 | Agent1->>Bedrock: Initial query 19 | Bedrock-->>Agent1: Needs additional data 20 | 21 | Agent1->>Agent2: Request employee data
(MCP protocol) 22 | Agent2->>Bedrock: Modified query 23 | Bedrock-->>Agent2: Needs additional data 24 | Agent2->>MCP_Server: Request employee data
(MCP protocol) 25 | MCP_Server-->>Agent2: Return employee data 26 | Agent2-->>Agent1: Return processed answer 27 | 28 | Agent1-->>REST: Return processed answer 29 | REST-->>User: JSON response 30 | ``` 31 | 32 | ## Setup 33 | 34 | 1. Setup Bedrock in the AWS Console, [request access to Nova Pro](https://us-east-1.console.aws.amazon.com/bedrock/home?region=us-east-1#/modelaccess) 35 | 1. [Setup auth for local development](https://docs.aws.amazon.com/cli/v1/userguide/cli-chap-authentication.html) 36 | 37 | ## Run Locally 38 | 39 | Start the MCP Server: 40 | ``` 41 | ./mvnw -pl server spring-boot:run 42 | ``` 43 | 44 | Start the MCP Server: 45 | ``` 46 | ./mvnw -pl client-server spring-boot:run 47 | ``` 48 | 49 | Start the MCP Client / Agent: 50 | ``` 51 | ./mvnw -pl client spring-boot:run 52 | ``` 53 | 54 | Make a request to the server REST endpoint: 55 | 56 | In IntelliJ, open the `client.http` file and run the request. 57 | 58 | Or via `curl`: 59 | ``` 60 | curl -X POST --location "http://localhost:8080/inquire" \ 61 | -H "Content-Type: application/json" \ 62 | -d '{"question": "Get employees that have skills related to Java, but not Java"}' 63 | ``` 64 | 65 | ## Run on AWS 66 | 67 | Prereqs: 68 | - [Create an ECR Repos](https://us-east-1.console.aws.amazon.com/ecr/private-registry/repositories/create?region=us-east-1) 69 | - `spring-ai-mcp-inter-agent-ecs-server` 70 | - `spring-ai-mcp-inter-agent-ecs-client-server` 71 | - `spring-ai-mcp-inter-agent-ecs-client` 72 | - [Auth `docker` to ECR](https://docs.aws.amazon.com/AmazonECR/latest/userguide/registry_auth.html) 73 | - i.e. `aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin $ECR_REPO` 74 | - [Install Rain](https://github.com/aws-cloudformation/rain) 75 | 76 | Build and push the MCP Server & MCP Client to ECR: 77 | ``` 78 | export ECR_REPO=.dkr.ecr.us-east-1.amazonaws.com 79 | 80 | ./mvnw -pl server spring-boot:build-image -Dspring-boot.build-image.imageName=$ECR_REPO/spring-ai-mcp-inter-agent-ecs-server 81 | docker push $ECR_REPO/spring-ai-mcp-inter-agent-ecs-server:latest 82 | 83 | ./mvnw -pl client-server spring-boot:build-image -Dspring-boot.build-image.imageName=$ECR_REPO/spring-ai-mcp-inter-agent-ecs-client-server 84 | docker push $ECR_REPO/spring-ai-mcp-inter-agent-ecs-client-server:latest 85 | 86 | ./mvnw -pl client spring-boot:build-image -Dspring-boot.build-image.imageName=$ECR_REPO/spring-ai-mcp-inter-agent-ecs-client 87 | docker push $ECR_REPO/spring-ai-mcp-inter-agent-ecs-client:latest 88 | ``` 89 | 90 | Deploy the Agent: 91 | ``` 92 | rain deploy infra.cfn spring-ai-mcp-inter-agent-ecs 93 | ``` 94 | 95 | End-to-end Test with `curl`: 96 | ``` 97 | curl -X POST --location "http://YOUR_LB_HOST/inquire" \ 98 | -H "Content-Type: application/json" \ 99 | -d '{"question": "Get employees that have skills related to Java, but not Java"}' 100 | ``` 101 | -------------------------------------------------------------------------------- /modules/spring-ai-mcp-inter-agent-ecs/client-server/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | com.example 9 | spring-ai-mcp-inter-agent-ecs 10 | 1.0.0-SNAPSHOT 11 | 12 | 13 | client-server 14 | 15 | 16 | 17 | org.springframework.ai 18 | spring-ai-starter-model-bedrock-converse 19 | 20 | 21 | 22 | org.springframework.ai 23 | spring-ai-starter-mcp-client-webflux 24 | 25 | 26 | 27 | org.springframework.ai 28 | spring-ai-starter-mcp-server-webflux 29 | 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-starter-actuator 34 | runtime 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /modules/spring-ai-mcp-inter-agent-ecs/client-server/src/main/java/mcpagentspringai/Application.java: -------------------------------------------------------------------------------- 1 | package mcpagentspringai; 2 | 3 | import io.modelcontextprotocol.client.McpSyncClient; 4 | import io.modelcontextprotocol.server.McpServerFeatures; 5 | import org.springframework.ai.chat.client.ChatClient; 6 | import org.springframework.ai.mcp.McpToolUtils; 7 | import org.springframework.ai.mcp.SyncMcpToolCallbackProvider; 8 | import org.springframework.ai.tool.annotation.Tool; 9 | import org.springframework.ai.tool.annotation.ToolParam; 10 | import org.springframework.ai.tool.method.MethodToolCallbackProvider; 11 | import org.springframework.boot.SpringApplication; 12 | import org.springframework.boot.autoconfigure.SpringBootApplication; 13 | import org.springframework.context.annotation.Bean; 14 | import org.springframework.context.annotation.Configuration; 15 | import org.springframework.stereotype.Service; 16 | 17 | import java.util.List; 18 | 19 | @SpringBootApplication 20 | public class Application { 21 | public static void main(String[] args) { 22 | SpringApplication.run(Application.class, args); 23 | } 24 | } 25 | 26 | @Configuration 27 | class AgentConfiguration { 28 | 29 | // Expose the tool that uses the chat client as an MCP server 30 | @Bean 31 | public List myToolSpecs(MyTools myTools) { 32 | var toolCallbacks = List.of(MethodToolCallbackProvider.builder().toolObjects(myTools).build().getToolCallbacks()); 33 | return McpToolUtils.toSyncToolSpecification(toolCallbacks); 34 | } 35 | 36 | // Bedrock Converse chat client with employee database MCP client 37 | @Bean 38 | ChatClient chatClient(List mcpSyncClients, ChatClient.Builder builder) { 39 | return builder 40 | .defaultToolCallbacks(new SyncMcpToolCallbackProvider(mcpSyncClients)) 41 | .defaultSystem("abbreviate employee first names with first letter and a period") 42 | .build(); 43 | } 44 | 45 | } 46 | 47 | @Service 48 | class EmployeeQueries { 49 | private final ChatClient chatClient; 50 | 51 | EmployeeQueries(ChatClient chatClient) { 52 | this.chatClient = chatClient; 53 | } 54 | 55 | String query(String question) { 56 | return chatClient 57 | .prompt() 58 | .user(question) 59 | .call() 60 | .content(); 61 | } 62 | 63 | } 64 | 65 | @Service 66 | class MyTools { 67 | 68 | private final EmployeeQueries employeeQueries; 69 | 70 | MyTools(EmployeeQueries employeeQueries) { 71 | this.employeeQueries = employeeQueries; 72 | } 73 | 74 | @Tool(description = "answers questions related to our employees") 75 | String inquire(@ToolParam(description = "the query about the employees", required = true) String question) { 76 | return employeeQueries.query(question); 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /modules/spring-ai-mcp-inter-agent-ecs/client-server/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.ai.bedrock.converse.chat.options.model=amazon.nova-pro-v1:0 2 | spring.ai.bedrock.converse.chat.options.max-tokens=10000 3 | spring.ai.mcp.client.sse.connections.employeedb.url=${mcp-service.url:http://localhost:8082} 4 | spring.ai.mcp.client.toolcallback.enabled=false 5 | server.port=8081 6 | -------------------------------------------------------------------------------- /modules/spring-ai-mcp-inter-agent-ecs/client-server/src/test/java/mcpagentspringai/TestApplication.java: -------------------------------------------------------------------------------- 1 | package mcpagentspringai; 2 | 3 | import org.springframework.boot.ApplicationRunner; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.context.annotation.Bean; 7 | 8 | @SpringBootApplication 9 | public class TestApplication { 10 | 11 | @Bean 12 | ApplicationRunner query(EmployeeQueries employeeQueries) { 13 | return args -> 14 | System.out.println(employeeQueries.query("Get employees that have skills related to Java")); 15 | // System.out.println(employeeQueries.query("What skills do our employees have?")); 16 | } 17 | 18 | public static void main(String[] args) { 19 | SpringApplication.run(Application.class, args); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /modules/spring-ai-mcp-inter-agent-ecs/client-server/src/test/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.ai.bedrock.converse.chat.options.model=amazon.nova-pro-v1:0 2 | spring.ai.bedrock.converse.chat.options.max-tokens=10000 3 | spring.ai.mcp.client.sse.connections.employeedb.url=${mcp-service.url:http://localhost:8082} 4 | spring.ai.mcp.server.enabled=false 5 | spring.main.web-application-type=none 6 | -------------------------------------------------------------------------------- /modules/spring-ai-mcp-inter-agent-ecs/client.http: -------------------------------------------------------------------------------- 1 | POST http://localhost:8080/inquire 2 | Content-Type: application/json 3 | 4 | {"question": "Get employees that have skills related to Java"} 5 | 6 | ### 7 | 8 | POST http://localhost:8080/inquire 9 | Content-Type: application/json 10 | 11 | {"question": "what skills do our employees have?"} 12 | 13 | ### 14 | 15 | POST http://localhost:8080/inquire 16 | Content-Type: application/json 17 | 18 | {"question": "list our employees with React skills"} 19 | -------------------------------------------------------------------------------- /modules/spring-ai-mcp-inter-agent-ecs/client/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | com.example 9 | spring-ai-mcp-inter-agent-ecs 10 | 1.0.0-SNAPSHOT 11 | 12 | 13 | client 14 | 15 | 16 | 17 | org.springframework.boot 18 | spring-boot-starter-web 19 | 20 | 21 | 22 | org.springframework.ai 23 | spring-ai-starter-model-bedrock-converse 24 | 25 | 26 | 27 | org.springframework.ai 28 | spring-ai-starter-mcp-client 29 | 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-starter-actuator 34 | runtime 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /modules/spring-ai-mcp-inter-agent-ecs/client/src/main/java/mcpagentspringai/Application.java: -------------------------------------------------------------------------------- 1 | package mcpagentspringai; 2 | 3 | import io.modelcontextprotocol.client.McpSyncClient; 4 | import org.springframework.ai.chat.client.ChatClient; 5 | import org.springframework.ai.mcp.SyncMcpToolCallbackProvider; 6 | import org.springframework.boot.SpringApplication; 7 | import org.springframework.boot.autoconfigure.SpringBootApplication; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | import org.springframework.web.bind.annotation.*; 11 | 12 | import java.util.List; 13 | 14 | @SpringBootApplication 15 | public class Application { 16 | public static void main(String[] args) { 17 | SpringApplication.run(Application.class, args); 18 | } 19 | } 20 | 21 | @Configuration 22 | class ConversationalConfiguration { 23 | @Bean 24 | ChatClient chatClient(List mcpSyncClients, ChatClient.Builder builder) { 25 | return builder 26 | .defaultToolCallbacks(new SyncMcpToolCallbackProvider(mcpSyncClients)) 27 | .build(); 28 | } 29 | } 30 | 31 | record Prompt(String question) { } 32 | 33 | @RestController 34 | class ConversationalController { 35 | 36 | private final ChatClient chatClient; 37 | 38 | ConversationalController(ChatClient chatClient) { 39 | this.chatClient = chatClient; 40 | } 41 | 42 | @PostMapping("/inquire") 43 | String inquire(@RequestBody Prompt prompt) { 44 | return chatClient 45 | .prompt() 46 | .user(prompt.question()) 47 | .call() 48 | .content(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /modules/spring-ai-mcp-inter-agent-ecs/client/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.ai.bedrock.converse.chat.options.model=amazon.nova-pro-v1:0 2 | spring.ai.bedrock.converse.chat.options.max-tokens=10000 3 | spring.ai.mcp.client.sse.connections.employee_root.url=${mcp-service.url:http://localhost:8081} 4 | -------------------------------------------------------------------------------- /modules/spring-ai-mcp-inter-agent-ecs/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | org.springframework.boot 9 | spring-boot-starter-parent 10 | 3.5.0 11 | 12 | 13 | 14 | com.example 15 | spring-ai-mcp-inter-agent-ecs 16 | 1.0.0-SNAPSHOT 17 | pom 18 | 19 | 20 | client 21 | client-server 22 | server 23 | 24 | 25 | 26 | 21 27 | 2.1.21 28 | 1.0.0 29 | UTF-8 30 | 31 | 32 | 33 | 34 | 35 | org.springframework.ai 36 | spring-ai-bom 37 | ${spring-ai.version} 38 | pom 39 | import 40 | 41 | 42 | 43 | 44 | 45 | 46 | org.jetbrains.kotlin 47 | kotlin-reflect 48 | ${kotlin.version} 49 | runtime 50 | 51 | 52 | 53 | org.jetbrains.kotlin 54 | kotlin-stdlib 55 | ${kotlin.version} 56 | 57 | 58 | 59 | 60 | 61 | 62 | org.springframework.boot 63 | spring-boot-maven-plugin 64 | 65 | 66 | 67 | org.jetbrains.kotlin 68 | kotlin-maven-plugin 69 | ${kotlin.version} 70 | 71 | ${java.version} 72 | 73 | spring 74 | 75 | 76 | 77 | 78 | org.jetbrains.kotlin 79 | kotlin-maven-allopen 80 | ${kotlin.version} 81 | 82 | 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /modules/spring-ai-mcp-inter-agent-ecs/server/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | com.example 9 | spring-ai-mcp-inter-agent-ecs 10 | 1.0.0-SNAPSHOT 11 | 12 | 13 | server 14 | 15 | 16 | 17 | org.springframework.ai 18 | spring-ai-starter-mcp-server-webflux 19 | 20 | 21 | 22 | org.springframework.boot 23 | spring-boot-starter-actuator 24 | runtime 25 | 26 | 27 | 28 | 29 | ${project.basedir}/src/main/kotlin 30 | ${project.basedir}/src/test/kotlin 31 | 32 | 33 | -------------------------------------------------------------------------------- /modules/spring-ai-mcp-inter-agent-ecs/server/src/main/kotlin/mcpagentspringai/Application.kt: -------------------------------------------------------------------------------- 1 | package mcpagentspringai 2 | 3 | import org.springframework.ai.tool.annotation.Tool 4 | import org.springframework.ai.tool.annotation.ToolParam 5 | import org.springframework.ai.tool.method.MethodToolCallbackProvider 6 | import org.springframework.boot.autoconfigure.SpringBootApplication 7 | import org.springframework.boot.runApplication 8 | import org.springframework.context.annotation.Bean 9 | import org.springframework.stereotype.Service 10 | 11 | @SpringBootApplication 12 | class Application { 13 | @Bean 14 | fun mcpTools(myTools: MyTools): MethodToolCallbackProvider = 15 | MethodToolCallbackProvider.builder().toolObjects(myTools).build() 16 | } 17 | 18 | data class Employee(val name: String, val skills: List) 19 | 20 | @Service 21 | class MyTools { 22 | 23 | @Tool(description = "the list of all possible employee skills") 24 | fun getSkills(): Set = run { 25 | println("getSkills") 26 | SampleData.employees.flatMap { it.skills }.toSet() 27 | } 28 | 29 | @Tool(description = "the employees that have a specific skill") 30 | fun getEmployeesWithSkill(@ToolParam(description = "skill") skill: String): List = run { 31 | println("getEmployeesWithSkill $skill") 32 | SampleData.employees.filter { employee -> 33 | employee.skills.any { it.equals(skill, ignoreCase = true) } 34 | } 35 | } 36 | 37 | } 38 | 39 | fun main(args: Array) { 40 | SampleData.employees.forEach { println(it) } 41 | runApplication(*args) 42 | } 43 | -------------------------------------------------------------------------------- /modules/spring-ai-mcp-inter-agent-ecs/server/src/main/kotlin/mcpagentspringai/SampleData.kt: -------------------------------------------------------------------------------- 1 | package mcpagentspringai 2 | 3 | import kotlin.random.Random 4 | 5 | object SampleData { 6 | 7 | private val firstNames = listOf("James", "Mary", "John", "Patricia", "Robert", "Jennifer", "Michael", "Linda", "William", "Elizabeth") 8 | private val lastNames = listOf("Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis", "Rodriguez", "Martinez") 9 | 10 | val skills = listOf( 11 | "Kotlin", "Java", "Python", "JavaScript", "TypeScript", 12 | "React", "Angular", "Spring Boot", "AWS", "Docker", 13 | "Kubernetes", "SQL", "MongoDB", "Git", "CI/CD", 14 | "Machine Learning", "DevOps", "Node.js", "REST API", "GraphQL" 15 | ) 16 | 17 | val employees = List(100) { 18 | Employee( 19 | name = firstNames.random() + " " + lastNames.random(), 20 | skills = List(Random.nextInt(2, 6)) { skills.random() }.distinct() 21 | ) 22 | }.distinctBy { it.name } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /modules/spring-ai-mcp-inter-agent-ecs/server/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | server.port=8082 2 | -------------------------------------------------------------------------------- /modules/spring-ai-mcp-server-ecs/.gitattributes: -------------------------------------------------------------------------------- 1 | /gradlew text eol=lf 2 | *.bat text eol=crlf 3 | *.jar binary 4 | -------------------------------------------------------------------------------- /modules/spring-ai-mcp-server-ecs/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/**/build/ 6 | !**/src/test/**/build/ 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | bin/ 17 | !**/src/main/**/bin/ 18 | !**/src/test/**/bin/ 19 | 20 | ### IntelliJ IDEA ### 21 | .idea 22 | *.iws 23 | *.iml 24 | *.ipr 25 | out/ 26 | !**/src/main/**/out/ 27 | !**/src/test/**/out/ 28 | 29 | ### NetBeans ### 30 | /nbproject/private/ 31 | /nbbuild/ 32 | /dist/ 33 | /nbdist/ 34 | /.nb-gradle/ 35 | 36 | ### VS Code ### 37 | .vscode/ 38 | 39 | ### Kotlin ### 40 | .kotlin 41 | 42 | target/ 43 | -------------------------------------------------------------------------------- /modules/spring-ai-mcp-server-ecs/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | wrapperVersion=3.3.2 18 | distributionType=only-script 19 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip 20 | -------------------------------------------------------------------------------- /modules/spring-ai-mcp-server-ecs/README.md: -------------------------------------------------------------------------------- 1 | # Hello Spring MCP Server 2 | 3 | ## Run Locally 4 | 5 | 1. Run the application: 6 | ``` 7 | ./mvnw spring-boot:run 8 | ``` 9 | 10 | ## Deploy on ECS 11 | 12 | Prereqs: 13 | - [Create ECR Repo](https://us-east-1.console.aws.amazon.com/ecr/private-registry/repositories/create?region=us-east-1) 14 | - [Auth `docker` to ECR](https://docs.aws.amazon.com/AmazonECR/latest/userguide/registry_auth.html) 15 | - [Install rain](https://github.com/aws-cloudformation/rain) 16 | 17 | ``` 18 | export ECR_REPO=.dkr.ecr.us-east-1.amazonaws.com/ 19 | 20 | ./mvnw spring-boot:build-image -Dspring-boot.build-image.imageName=$ECR_REPO 21 | 22 | docker push $ECR_REPO:latest 23 | 24 | rain deploy \ 25 | --params=ContainerImage=$ECR_REPO:latest,ContainerPort=8080,ServiceName=hello-spring-mcp-server \ 26 | infra.cfn \ 27 | hello-spring-mcp-server 28 | ``` 29 | -------------------------------------------------------------------------------- /modules/spring-ai-mcp-server-ecs/infra.cfn: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: ECS Service with ALB 3 | 4 | Parameters: 5 | ContainerImage: 6 | Description: Docker image to run 7 | Type: String 8 | 9 | ContainerPort: 10 | Description: Port the container listens on 11 | Type: Number 12 | Default: 8080 13 | 14 | ServiceName: 15 | Description: Name of the ECS service 16 | Type: String 17 | Default: web-service 18 | 19 | Resources: 20 | VPC: 21 | Type: AWS::EC2::VPC 22 | Properties: 23 | CidrBlock: 10.0.0.0/16 24 | EnableDnsHostnames: true 25 | EnableDnsSupport: true 26 | Tags: 27 | - Key: Name 28 | Value: !Sub ${AWS::StackName}-vpc 29 | 30 | InternetGateway: 31 | Type: AWS::EC2::InternetGateway 32 | 33 | AttachGateway: 34 | Type: AWS::EC2::VPCGatewayAttachment 35 | Properties: 36 | VpcId: !Ref VPC 37 | InternetGatewayId: !Ref InternetGateway 38 | 39 | PublicSubnet1: 40 | Type: AWS::EC2::Subnet 41 | Properties: 42 | VpcId: !Ref VPC 43 | CidrBlock: 10.0.1.0/24 44 | AvailabilityZone: !Select [0, !GetAZs ""] 45 | MapPublicIpOnLaunch: true 46 | Tags: 47 | - Key: Name 48 | Value: !Sub ${AWS::StackName}-public-subnet-1 49 | 50 | PublicSubnet2: 51 | Type: AWS::EC2::Subnet 52 | Properties: 53 | VpcId: !Ref VPC 54 | CidrBlock: 10.0.2.0/24 55 | AvailabilityZone: !Select [1, !GetAZs ""] 56 | MapPublicIpOnLaunch: true 57 | Tags: 58 | - Key: Name 59 | Value: !Sub ${AWS::StackName}-public-subnet-2 60 | 61 | PublicRouteTable: 62 | Type: AWS::EC2::RouteTable 63 | Properties: 64 | VpcId: !Ref VPC 65 | Tags: 66 | - Key: Name 67 | Value: !Sub ${AWS::StackName}-public-rt 68 | 69 | PublicRoute: 70 | Type: AWS::EC2::Route 71 | DependsOn: AttachGateway 72 | Properties: 73 | RouteTableId: !Ref PublicRouteTable 74 | DestinationCidrBlock: 0.0.0.0/0 75 | GatewayId: !Ref InternetGateway 76 | 77 | PublicSubnet1RouteTableAssociation: 78 | Type: AWS::EC2::SubnetRouteTableAssociation 79 | Properties: 80 | SubnetId: !Ref PublicSubnet1 81 | RouteTableId: !Ref PublicRouteTable 82 | 83 | PublicSubnet2RouteTableAssociation: 84 | Type: AWS::EC2::SubnetRouteTableAssociation 85 | Properties: 86 | SubnetId: !Ref PublicSubnet2 87 | RouteTableId: !Ref PublicRouteTable 88 | 89 | ECSCluster: 90 | Type: AWS::ECS::Cluster 91 | Properties: 92 | ClusterName: !Sub ${AWS::StackName}-cluster 93 | 94 | TaskDefinition: 95 | Type: AWS::ECS::TaskDefinition 96 | Properties: 97 | Family: !Ref ServiceName 98 | Cpu: "256" 99 | Memory: "512" 100 | NetworkMode: awsvpc 101 | RequiresCompatibilities: 102 | - FARGATE 103 | ExecutionRoleArn: !GetAtt ExecutionRole.Arn 104 | TaskRoleArn: !GetAtt TaskRole.Arn 105 | ContainerDefinitions: 106 | - Name: !Ref ServiceName 107 | Image: !Ref ContainerImage 108 | PortMappings: 109 | - ContainerPort: !Ref ContainerPort 110 | LogConfiguration: 111 | LogDriver: awslogs 112 | Options: 113 | awslogs-group: !Ref LogGroup 114 | awslogs-region: !Ref AWS::Region 115 | awslogs-stream-prefix: ecs 116 | 117 | LogGroup: 118 | DeletionPolicy: Delete 119 | UpdateReplacePolicy: Delete 120 | Type: AWS::Logs::LogGroup 121 | Properties: 122 | LogGroupName: !Sub /ecs/${AWS::StackName} 123 | RetentionInDays: 1 124 | 125 | ExecutionRole: 126 | Type: AWS::IAM::Role 127 | Properties: 128 | AssumeRolePolicyDocument: 129 | Version: "2012-10-17" 130 | Statement: 131 | - Effect: Allow 132 | Principal: 133 | Service: ecs-tasks.amazonaws.com 134 | Action: sts:AssumeRole 135 | ManagedPolicyArns: 136 | - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy 137 | 138 | TaskRole: 139 | Type: AWS::IAM::Role 140 | Properties: 141 | AssumeRolePolicyDocument: 142 | Version: "2012-10-17" 143 | Statement: 144 | - Effect: Allow 145 | Principal: 146 | Service: ecs-tasks.amazonaws.com 147 | Action: sts:AssumeRole 148 | 149 | Service: 150 | Type: AWS::ECS::Service 151 | DependsOn: LoadBalancerListener 152 | Properties: 153 | ServiceName: !Ref ServiceName 154 | Cluster: !Ref ECSCluster 155 | TaskDefinition: !Ref TaskDefinition 156 | DesiredCount: 1 157 | LaunchType: FARGATE 158 | HealthCheckGracePeriodSeconds: "60" 159 | NetworkConfiguration: 160 | AwsvpcConfiguration: 161 | SecurityGroups: 162 | - !Ref ContainerSecurityGroup 163 | Subnets: 164 | - !Ref PublicSubnet1 165 | - !Ref PublicSubnet2 166 | AssignPublicIp: ENABLED 167 | LoadBalancers: 168 | - ContainerName: !Ref ServiceName 169 | ContainerPort: !Ref ContainerPort 170 | LoadBalancerName: !Ref AWS::NoValue 171 | TargetGroupArn: !Ref TargetGroup 172 | 173 | ContainerSecurityGroup: 174 | Type: AWS::EC2::SecurityGroup 175 | Properties: 176 | GroupDescription: Security group for container 177 | VpcId: !Ref VPC 178 | SecurityGroupIngress: 179 | - FromPort: !Ref ContainerPort 180 | ToPort: !Ref ContainerPort 181 | IpProtocol: tcp 182 | SourceSecurityGroupId: !Ref LoadBalancerSecurityGroup 183 | 184 | LoadBalancerSecurityGroup: 185 | Type: AWS::EC2::SecurityGroup 186 | Properties: 187 | GroupDescription: Security group for ALB 188 | VpcId: !Ref VPC 189 | SecurityGroupIngress: 190 | - FromPort: 80 191 | ToPort: !Ref ContainerPort 192 | IpProtocol: tcp 193 | CidrIp: 0.0.0.0/0 194 | 195 | LoadBalancer: 196 | Type: AWS::ElasticLoadBalancingV2::LoadBalancer 197 | Properties: 198 | Scheme: internet-facing 199 | SecurityGroups: 200 | - !GetAtt LoadBalancerSecurityGroup.GroupId 201 | Subnets: 202 | - !Ref PublicSubnet1 203 | - !Ref PublicSubnet2 204 | Type: application 205 | 206 | TargetGroup: 207 | Type: AWS::ElasticLoadBalancingV2::TargetGroup 208 | Properties: 209 | HealthCheckPath: /actuator/health 210 | HealthCheckProtocol: HTTP 211 | Port: !Ref ContainerPort 212 | Protocol: HTTP 213 | TargetType: ip 214 | VpcId: !Ref VPC 215 | 216 | LoadBalancerListener: 217 | Type: AWS::ElasticLoadBalancingV2::Listener 218 | Properties: 219 | LoadBalancerArn: !Ref LoadBalancer 220 | Port: 80 221 | Protocol: HTTP 222 | DefaultActions: 223 | - Type: forward 224 | TargetGroupArn: !Ref TargetGroup 225 | 226 | Outputs: 227 | LoadBalancerDNS: 228 | Description: DNS name of the load balancer 229 | Value: !GetAtt LoadBalancer.DNSName 230 | -------------------------------------------------------------------------------- /modules/spring-ai-mcp-server-ecs/mvnw.cmd: -------------------------------------------------------------------------------- 1 | <# : batch portion 2 | @REM ---------------------------------------------------------------------------- 3 | @REM Licensed to the Apache Software Foundation (ASF) under one 4 | @REM or more contributor license agreements. See the NOTICE file 5 | @REM distributed with this work for additional information 6 | @REM regarding copyright ownership. The ASF licenses this file 7 | @REM to you under the Apache License, Version 2.0 (the 8 | @REM "License"); you may not use this file except in compliance 9 | @REM with the License. You may obtain a copy of the License at 10 | @REM 11 | @REM http://www.apache.org/licenses/LICENSE-2.0 12 | @REM 13 | @REM Unless required by applicable law or agreed to in writing, 14 | @REM software distributed under the License is distributed on an 15 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | @REM KIND, either express or implied. See the License for the 17 | @REM specific language governing permissions and limitations 18 | @REM under the License. 19 | @REM ---------------------------------------------------------------------------- 20 | 21 | @REM ---------------------------------------------------------------------------- 22 | @REM Apache Maven Wrapper startup batch script, version 3.3.2 23 | @REM 24 | @REM Optional ENV vars 25 | @REM MVNW_REPOURL - repo url base for downloading maven distribution 26 | @REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 27 | @REM MVNW_VERBOSE - true: enable verbose log; others: silence the output 28 | @REM ---------------------------------------------------------------------------- 29 | 30 | @IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) 31 | @SET __MVNW_CMD__= 32 | @SET __MVNW_ERROR__= 33 | @SET __MVNW_PSMODULEP_SAVE=%PSModulePath% 34 | @SET PSModulePath= 35 | @FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( 36 | IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) 37 | ) 38 | @SET PSModulePath=%__MVNW_PSMODULEP_SAVE% 39 | @SET __MVNW_PSMODULEP_SAVE= 40 | @SET __MVNW_ARG0_NAME__= 41 | @SET MVNW_USERNAME= 42 | @SET MVNW_PASSWORD= 43 | @IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) 44 | @echo Cannot start maven from wrapper >&2 && exit /b 1 45 | @GOTO :EOF 46 | : end batch / begin powershell #> 47 | 48 | $ErrorActionPreference = "Stop" 49 | if ($env:MVNW_VERBOSE -eq "true") { 50 | $VerbosePreference = "Continue" 51 | } 52 | 53 | # calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties 54 | $distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl 55 | if (!$distributionUrl) { 56 | Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" 57 | } 58 | 59 | switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { 60 | "maven-mvnd-*" { 61 | $USE_MVND = $true 62 | $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" 63 | $MVN_CMD = "mvnd.cmd" 64 | break 65 | } 66 | default { 67 | $USE_MVND = $false 68 | $MVN_CMD = $script -replace '^mvnw','mvn' 69 | break 70 | } 71 | } 72 | 73 | # apply MVNW_REPOURL and calculate MAVEN_HOME 74 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 75 | if ($env:MVNW_REPOURL) { 76 | $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } 77 | $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" 78 | } 79 | $distributionUrlName = $distributionUrl -replace '^.*/','' 80 | $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' 81 | $MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" 82 | if ($env:MAVEN_USER_HOME) { 83 | $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" 84 | } 85 | $MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' 86 | $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" 87 | 88 | if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { 89 | Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" 90 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 91 | exit $? 92 | } 93 | 94 | if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { 95 | Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" 96 | } 97 | 98 | # prepare tmp dir 99 | $TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile 100 | $TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" 101 | $TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null 102 | trap { 103 | if ($TMP_DOWNLOAD_DIR.Exists) { 104 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 105 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 106 | } 107 | } 108 | 109 | New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null 110 | 111 | # Download and Install Apache Maven 112 | Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 113 | Write-Verbose "Downloading from: $distributionUrl" 114 | Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 115 | 116 | $webclient = New-Object System.Net.WebClient 117 | if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { 118 | $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) 119 | } 120 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 121 | $webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null 122 | 123 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 124 | $distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum 125 | if ($distributionSha256Sum) { 126 | if ($USE_MVND) { 127 | Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." 128 | } 129 | Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash 130 | if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { 131 | Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." 132 | } 133 | } 134 | 135 | # unzip and move 136 | Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null 137 | Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null 138 | try { 139 | Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null 140 | } catch { 141 | if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { 142 | Write-Error "fail to move MAVEN_HOME" 143 | } 144 | } finally { 145 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 146 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 147 | } 148 | 149 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 150 | -------------------------------------------------------------------------------- /modules/spring-ai-mcp-server-ecs/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | org.springframework.boot 9 | spring-boot-starter-parent 10 | 3.5.0 11 | 12 | 13 | 14 | com.example 15 | spring-ai-mcp-server-ecs 16 | 1.0.0-SNAPSHOT 17 | 18 | 19 | 21 20 | 2.1.21 21 | 1.0.0 22 | UTF-8 23 | 24 | 25 | 26 | 27 | 28 | org.springframework.ai 29 | spring-ai-bom 30 | ${spring-ai.version} 31 | pom 32 | import 33 | 34 | 35 | 36 | 37 | 38 | 39 | org.jetbrains.kotlin 40 | kotlin-reflect 41 | ${kotlin.version} 42 | runtime 43 | 44 | 45 | 46 | org.jetbrains.kotlin 47 | kotlin-stdlib 48 | ${kotlin.version} 49 | 50 | 51 | 52 | org.springframework.ai 53 | spring-ai-starter-mcp-server-webflux 54 | 55 | 56 | 57 | org.springframework.boot 58 | spring-boot-starter-actuator 59 | runtime 60 | 61 | 62 | 63 | 64 | ${project.basedir}/src/main/kotlin 65 | ${project.basedir}/src/test/kotlin 66 | 67 | 68 | 69 | org.springframework.boot 70 | spring-boot-maven-plugin 71 | 72 | 73 | org.jetbrains.kotlin 74 | kotlin-maven-plugin 75 | ${kotlin.version} 76 | 77 | ${java.version} 78 | 79 | spring 80 | 81 | 82 | 83 | 84 | org.jetbrains.kotlin 85 | kotlin-maven-allopen 86 | ${kotlin.version} 87 | 88 | 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /modules/spring-ai-mcp-server-ecs/src/main/kotlin/springaimcpserverecs/Application.kt: -------------------------------------------------------------------------------- 1 | package springaimcpserverecs 2 | 3 | import org.springframework.ai.tool.annotation.Tool 4 | import org.springframework.ai.tool.annotation.ToolParam 5 | import org.springframework.ai.tool.method.MethodToolCallbackProvider 6 | import org.springframework.boot.autoconfigure.SpringBootApplication 7 | import org.springframework.boot.runApplication 8 | import org.springframework.context.annotation.Bean 9 | import org.springframework.stereotype.Service 10 | 11 | @SpringBootApplication 12 | class Application { 13 | @Bean 14 | fun myTools(helloService: HelloService): MethodToolCallbackProvider = 15 | MethodToolCallbackProvider.builder().toolObjects(helloService).build() 16 | } 17 | 18 | @Service 19 | class HelloService { 20 | 21 | @Tool(description = "says hello to someone") 22 | fun sayHello(@ToolParam(description = "name of person") name: String): String = 23 | "hello, $name" 24 | 25 | } 26 | 27 | fun main(args: Array) { 28 | runApplication(*args) 29 | } 30 | --------------------------------------------------------------------------------