├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── app.py ├── converse_agent.py ├── converse_tools.py ├── mcp_client.py └── requirements.txt /.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 | *.db 39 | *.sqlite3 40 | *.log 41 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Amazon Bedrock Converse API MCP Demo 2 | 3 | This is a demo of Anthropic's open source MCP used with Amazon Bedrock Converse API. This combination allows for the MCP to be used with any of the many models supported by the Converse API. 4 | 5 | ## Prerequisites 6 | 7 | - Python 3.8+ 8 | - AWS account with Bedrock access 9 | - AWS credentials configured locally 10 | - SQLite database (Follow the instructions in the [MCP Quick Start Guide](https://modelcontextprotocol.io/quickstart) to set this up.) 11 | 12 | ## Installation 13 | 14 | 1. Clone the repository: 15 | ```bash 16 | git clone 17 | cd 18 | ``` 19 | 20 | 2. Create and activate a virtual environment: 21 | ```bash 22 | python -m venv venv 23 | source venv/bin/activate # On Windows: venv\Scripts\activate 24 | ``` 25 | 26 | 3. Install required packages: 27 | ```bash 28 | pip install -r requirements.txt 29 | ``` 30 | 31 | ## Configuration 32 | 33 | 1. Ensure AWS credentials are properly configured in `~/.aws/credentials` or via environment variables: 34 | ```bash 35 | export AWS_ACCESS_KEY_ID=your_access_key 36 | export AWS_SECRET_ACCESS_KEY=your_secret_key 37 | export AWS_DEFAULT_REGION=us-west-2 38 | ``` 39 | 40 | 2. The default configuration uses: 41 | - Model: anthropic.claude-3-5-sonnet-20241022-v2:0 42 | - Region: us-west-2 43 | - SQLite database path: ~/test.db 44 | 45 | ## Project Structure 46 | 47 | - `app.py`: Main application entry point and interactive loop 48 | - `converse_agent.py`: Core agent implementation with Bedrock integration 49 | - `converse_tools.py`: Tool management and execution system 50 | - `mcp_client.py`: MCP (Model Control Protocol) client implementation 51 | 52 | ## Usage 53 | 54 | 1. Start the application: 55 | ```bash 56 | python app.py 57 | ``` 58 | 59 | 2. Enter prompts when prompted. The agent will: 60 | - Process your input 61 | - Execute any necessary tools 62 | - Provide responses 63 | - Maintain conversation context 64 | 65 | 3. Exit the application by typing 'quit', 'exit', 'q', or using Ctrl+C 66 | 67 | ## Key Components 68 | 69 | ### ConverseAgent 70 | The main agent class that: 71 | - Manages conversation flow 72 | - Integrates with Bedrock 73 | - Handles tool execution 74 | - Processes responses 75 | 76 | Reference: 77 | ```python:converse_agent.py 78 | startLine: 3 79 | endLine: 109 80 | ``` 81 | 82 | ### ConverseToolManager 83 | Manages tool registration and execution: 84 | - Tool registration with schemas 85 | - Name sanitization 86 | - Tool execution handling 87 | 88 | Reference: 89 | ```python:converse_tools.py 90 | startLine: 5 91 | endLine: 76 92 | ``` 93 | 94 | ### MCPClient 95 | Handles communication with the MCP server: 96 | - Tool discovery 97 | - Tool execution 98 | - Server connection management 99 | 100 | Reference: 101 | ```python:mcp_client.py 102 | startLine: 6 103 | endLine: 48 104 | ``` 105 | 106 | ## Security 107 | 108 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. 109 | 110 | ## License 111 | 112 | This library is licensed under the MIT-0 License. See the LICENSE file. -------------------------------------------------------------------------------- /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 | 7 | async def main(): 8 | """ 9 | Main function that sets up and runs an interactive AI agent with tool integration. 10 | The agent can process user prompts and utilize registered tools to perform tasks. 11 | """ 12 | # Initialize model configuration 13 | model_id = "anthropic.claude-3-5-sonnet-20241022-v2:0" 14 | # model_id = "mistral.mistral-large-2407-v1:0" 15 | # model_id = "us.meta.llama3-2-90b-instruct-v1:0" 16 | 17 | # Set up the agent and tool manager 18 | agent = ConverseAgent(model_id) 19 | agent.tools = ConverseToolManager() 20 | 21 | # Define the agent's behavior through system prompt 22 | agent.system_prompt = """You are a helpful assistant that can use tools to help you answer 23 | questions and perform tasks.""" 24 | 25 | # Create server parameters for SQLite configuration 26 | server_params = StdioServerParameters( 27 | command="uvx", 28 | args=["mcp-server-sqlite", "--db-path", "~/test.db"], 29 | env=None 30 | ) 31 | 32 | # Initialize MCP client with server parameters 33 | async with MCPClient(server_params) as mcp_client: 34 | 35 | # Fetch available tools from the MCP client 36 | tools = await mcp_client.get_available_tools() 37 | 38 | # Register each available tool with the agent 39 | for tool in tools: 40 | agent.tools.register_tool( 41 | name=tool.name, 42 | func=mcp_client.call_tool, 43 | description=tool.description, 44 | input_schema={'json': tool.inputSchema} 45 | ) 46 | 47 | # Start interactive prompt loop 48 | while True: 49 | try: 50 | # Get user input and check for exit commands 51 | user_prompt = input("\nEnter your prompt (or 'quit' to exit): ") 52 | if user_prompt.lower() in ['quit', 'exit', 'q']: 53 | break 54 | 55 | # Process the prompt and display the response 56 | response = await agent.invoke_with_prompt(user_prompt) 57 | print("\nResponse:", response) 58 | 59 | except KeyboardInterrupt: 60 | print("\nExiting...") 61 | break 62 | except Exception as e: 63 | print(f"\nError occurred: {e}") 64 | 65 | if __name__ == "__main__": 66 | # Run the async main function 67 | asyncio.run(main()) -------------------------------------------------------------------------------- /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 | 23 | print(f"User: {json.dumps(content, indent=2)}") 24 | 25 | self.messages.append( 26 | { 27 | "role": "user", 28 | "content": content 29 | } 30 | ) 31 | response = self._get_converse_response() 32 | 33 | print(f"Agent: {json.dumps(response, indent=2)}") 34 | 35 | return await self._handle_response(response) 36 | 37 | def _get_converse_response(self): 38 | """ 39 | https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/bedrock-runtime/client/converse.html 40 | """ 41 | 42 | # print(f"Invoking with messages: {json.dumps(self.messages, indent=2)}") 43 | 44 | response = self.client.converse( 45 | modelId=self.model_id, 46 | messages=self.messages, 47 | system=[ 48 | { 49 | "text": self.system_prompt 50 | } 51 | ], 52 | inferenceConfig={ 53 | "maxTokens": 8192, 54 | "temperature": 0.7, 55 | }, 56 | toolConfig=self.tools.get_tools() 57 | ) 58 | return(response) 59 | 60 | async def _handle_response(self, response): 61 | # Add the response to the conversation history 62 | self.messages.append(response['output']['message']) 63 | 64 | # Do we need to do anything else? 65 | stop_reason = response['stopReason'] 66 | 67 | if stop_reason in ['end_turn', 'stop_sequence']: 68 | # Safely extract the text from the nested response structure 69 | try: 70 | message = response.get('output', {}).get('message', {}) 71 | content = message.get('content', []) 72 | text = content[0].get('text', '') 73 | if hasattr(self, 'response_output_tags') and len(self.response_output_tags) == 2: 74 | pattern = f"(?s).*{re.escape(self.response_output_tags[0])}(.*?){re.escape(self.response_output_tags[1])}" 75 | match = re.search(pattern, text) 76 | if match: 77 | return match.group(1) 78 | return text 79 | except (KeyError, IndexError): 80 | return '' 81 | 82 | elif stop_reason == 'tool_use': 83 | try: 84 | # Extract tool use details from response 85 | tool_response = [] 86 | for content_item in response['output']['message']['content']: 87 | if 'toolUse' in content_item: 88 | tool_request = { 89 | "toolUseId": content_item['toolUse']['toolUseId'], 90 | "name": content_item['toolUse']['name'], 91 | "input": content_item['toolUse']['input'] 92 | } 93 | 94 | tool_result = await self.tools.execute_tool(tool_request) 95 | tool_response.append({'toolResult': tool_result}) 96 | 97 | return await self.invoke(tool_response) 98 | 99 | except KeyError as e: 100 | raise ValueError(f"Missing required tool use field: {e}") 101 | except Exception as e: 102 | raise ValueError(f"Failed to execute tool: {e}") 103 | 104 | elif stop_reason == 'max_tokens': 105 | # Hit token limit (this is one way to handle it.) 106 | await self.invoke_with_prompt('Please continue.') 107 | 108 | else: 109 | raise ValueError(f"Unknown stop reason: {stop_reason}") 110 | 111 | -------------------------------------------------------------------------------- /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 | self._name_mapping[sanitized_name] = name 20 | self._tools[sanitized_name] = { 21 | 'function': func, 22 | 'description': description, 23 | 'input_schema': input_schema, 24 | 'original_name': name 25 | } 26 | 27 | def get_tools(self) -> Dict[str, List[Dict]]: 28 | """ 29 | Generate the tools specification using sanitized names 30 | """ 31 | tool_specs = [] 32 | for sanitized_name, tool in self._tools.items(): 33 | tool_specs.append({ 34 | 'toolSpec': { 35 | 'name': sanitized_name, # Use sanitized name for Bedrock 36 | 'description': tool['description'], 37 | 'inputSchema': tool['input_schema'] 38 | } 39 | }) 40 | 41 | return {'tools': tool_specs} 42 | 43 | async def execute_tool(self, payload: Dict[str, Any]) -> Dict[str, Any]: 44 | """ 45 | Execute a tool based on the agent's request, handling name translation 46 | """ 47 | tool_use_id = payload['toolUseId'] 48 | sanitized_name = payload['name'] 49 | tool_input = payload['input'] 50 | 51 | if sanitized_name not in self._tools: 52 | raise ValueError(f"Unknown tool: {sanitized_name}") 53 | try: 54 | tool_func = self._tools[sanitized_name]['function'] 55 | # Use original name when calling the actual function 56 | original_name = self._tools[sanitized_name]['original_name'] 57 | result = await tool_func(original_name, tool_input) 58 | return { 59 | 'toolUseId': tool_use_id, 60 | 'content': [{ 61 | 'text': str(result) 62 | }], 63 | 'status': 'success' 64 | } 65 | except Exception as e: 66 | return { 67 | 'toolUseId': tool_use_id, 68 | 'content': [{ 69 | 'text': f"Error executing tool: {str(e)}" 70 | }], 71 | 'status': 'error' 72 | } 73 | 74 | def clear_tools(self): 75 | """Clear all registered tools""" 76 | self._tools.clear() 77 | -------------------------------------------------------------------------------- /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 | 6 | class MCPClient: 7 | def __init__(self, server_params: StdioServerParameters): 8 | self.server_params = server_params 9 | self.session = None 10 | self._client = None 11 | 12 | async def __aenter__(self): 13 | """Async context manager entry""" 14 | await self.connect() 15 | return self 16 | 17 | async def __aexit__(self, exc_type, exc_val, exc_tb): 18 | """Async context manager exit""" 19 | if self.session: 20 | await self.session.__aexit__(exc_type, exc_val, exc_tb) 21 | if self._client: 22 | await self._client.__aexit__(exc_type, exc_val, exc_tb) 23 | 24 | async def connect(self): 25 | """Establishes connection to MCP server""" 26 | self._client = stdio_client(self.server_params) 27 | self.read, self.write = await self._client.__aenter__() 28 | session = ClientSession(self.read, self.write) 29 | self.session = await session.__aenter__() 30 | await self.session.initialize() 31 | 32 | async def get_available_tools(self) -> List[Any]: 33 | """List available tools""" 34 | if not self.session: 35 | raise RuntimeError("Not connected to MCP server") 36 | 37 | tools = await self.session.list_tools() 38 | _, tools_list = tools 39 | _, tools_list = tools_list 40 | return tools_list 41 | 42 | async def call_tool(self, tool_name: str, arguments: dict) -> Any: 43 | """Call a tool with given arguments""" 44 | if not self.session: 45 | raise RuntimeError("Not connected to MCP server") 46 | 47 | result = await self.session.call_tool(tool_name, arguments=arguments) 48 | return result 49 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | boto3>=1.35.69 2 | mcp>=1.0.0 3 | uvicorn>=0.32.0 --------------------------------------------------------------------------------