From f82cf6aba974ff1473978c9da505b43bd54b17c7 Mon Sep 17 00:00:00 2001 From: Eashan Kaushik <50113394+EashanKaushik@users.noreply.github.com> Date: Sat, 4 Oct 2025 14:36:48 -0400 Subject: [PATCH] iam auth with MCP Runtime --- .../hosting_mcp_server.ipynb | 188 ++-- .../hosting_mcp_server_iam_auth.ipynb | 882 ++++++++++++++++++ .../streamable_http_sigv4.py | 152 +++ 3 files changed, 1112 insertions(+), 110 deletions(-) create mode 100644 01-tutorials/01-AgentCore-runtime/02-hosting-MCP-server/hosting_mcp_server_iam_auth.ipynb create mode 100644 01-tutorials/01-AgentCore-runtime/02-hosting-MCP-server/streamable_http_sigv4.py diff --git a/01-tutorials/01-AgentCore-runtime/02-hosting-MCP-server/hosting_mcp_server.ipynb b/01-tutorials/01-AgentCore-runtime/02-hosting-MCP-server/hosting_mcp_server.ipynb index 0cb52f10..692085cd 100644 --- a/01-tutorials/01-AgentCore-runtime/02-hosting-MCP-server/hosting_mcp_server.ipynb +++ b/01-tutorials/01-AgentCore-runtime/02-hosting-MCP-server/hosting_mcp_server.ipynb @@ -5,7 +5,7 @@ "id": "5c0122e65c053f38", "metadata": {}, "source": [ - "# Hosting MCP Server on Amazon Bedrock AgentCore Runtime\n", + "# Hosting MCP Server on Amazon Bedrock AgentCore Runtime - OAuth Inbound Authentication\n", "\n", "## Overview\n", "\n", @@ -71,6 +71,51 @@ "!pip install --force-reinstall -U -r requirements.txt --quiet" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "d60bc625", + "metadata": {}, + "outputs": [], + "source": [ + "from bedrock_agentcore_starter_toolkit import Runtime\n", + "from bedrock_agentcore_starter_toolkit.operations.runtime import destroy_bedrock_agentcore\n", + "from boto3.session import Session\n", + "from pathlib import Path\n", + "import os\n", + "import sys\n", + "\n", + "# Get the current notebook's directory\n", + "current_dir = os.path.dirname(os.path.abspath('__file__' if '__file__' in globals() else '.'))\n", + "\n", + "utils_dir = os.path.join(current_dir, '..')\n", + "utils_dir = os.path.abspath(utils_dir)\n", + "\n", + "# Add to sys.path\n", + "sys.path.insert(0, utils_dir)\n", + "print(\"sys.path[0]:\", sys.path[0])\n", + "\n", + "from utils import setup_cognito_user_pool" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "76b89c61", + "metadata": {}, + "outputs": [], + "source": [ + "boto_session = Session()\n", + "region = boto_session.region_name\n", + "\n", + "ssm_client = boto_session.client('ssm', region_name=region)\n", + "secrets_client = boto_session.client('secretsmanager', region_name=region)\n", + "agentcore_control_client = boto_session.client(\"bedrock-agentcore-control\", region_name=region)\n", + "ssm_client = boto_session.client('ssm', region_name=region)\n", + "\n", + "tool_name = \"mcp_server_agentcore\"" + ] + }, { "cell_type": "markdown", "id": "ca924a7a2731e26f", @@ -232,29 +277,6 @@ "AgentCore Runtime requires authentication. We'll use Amazon Cognito to provide JWT tokens for accessing our deployed MCP server." ] }, - { - "cell_type": "code", - "execution_count": null, - "id": "import_utils", - "metadata": {}, - "outputs": [], - "source": [ - "import sys\n", - "import os\n", - "\n", - "# Get the current notebook's directory\n", - "current_dir = os.path.dirname(os.path.abspath('__file__' if '__file__' in globals() else '.'))\n", - "\n", - "utils_dir = os.path.join(current_dir, '..')\n", - "utils_dir = os.path.abspath(utils_dir)\n", - "\n", - "# Add to sys.path\n", - "sys.path.insert(0, utils_dir)\n", - "print(\"sys.path[0]:\", sys.path[0])\n", - "\n", - "from utils import setup_cognito_user_pool" - ] - }, { "cell_type": "code", "execution_count": null, @@ -292,12 +314,6 @@ "metadata": {}, "outputs": [], "source": [ - "from bedrock_agentcore_starter_toolkit import Runtime\n", - "from boto3.session import Session\n", - "import time\n", - "\n", - "boto_session = Session()\n", - "region = boto_session.region_name\n", "print(f\"Using AWS region: {region}\")\n", "\n", "required_files = ['mcp_server.py', 'requirements.txt']\n", @@ -326,7 +342,7 @@ " region=region,\n", " authorizer_configuration=auth_config,\n", " protocol=\"MCP\",\n", - " agent_name=\"mcp_server_agentcore\"\n", + " agent_name=tool_name\n", ")\n", "print(\"Configuration completed โœ“\")" ] @@ -360,43 +376,6 @@ "print(f\"Agent ID: {launch_result.agent_id}\")" ] }, - { - "cell_type": "markdown", - "id": "step8_check_status", - "metadata": {}, - "source": [ - "## Checking AgentCore Runtime Status\n", - "\n", - "Now that we've deployed the AgentCore Runtime, let's check for its deployment status and wait for it to be ready:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "afa6ac09-9adb-4846-9fc1-4d12aeb74853", - "metadata": {}, - "outputs": [], - "source": [ - "print(\"Checking AgentCore Runtime status...\")\n", - "status_response = agentcore_runtime.status()\n", - "status = status_response.endpoint['status']\n", - "print(f\"Initial status: {status}\")\n", - "\n", - "end_status = ['READY', 'CREATE_FAILED', 'DELETE_FAILED', 'UPDATE_FAILED']\n", - "while status not in end_status:\n", - " print(f\"Status: {status} - waiting...\")\n", - " time.sleep(10)\n", - " status_response = agentcore_runtime.status()\n", - " status = status_response.endpoint['status']\n", - "\n", - "if status == 'READY':\n", - " print(\"โœ“ AgentCore Runtime is READY!\")\n", - "else:\n", - " print(f\"โš  AgentCore Runtime status: {status}\")\n", - " \n", - "print(f\"Final status: {status}\")" - ] - }, { "cell_type": "markdown", "id": "step9_store_credentials", @@ -745,49 +724,38 @@ "metadata": {}, "outputs": [], "source": [ - "import boto3\n", + "# print(\"๐Ÿ—‘๏ธ Starting cleanup process...\")\n", "\n", - "print(\"๐Ÿ—‘๏ธ Starting cleanup process...\")\n", - "\n", - "agentcore_control_client = boto3.client('bedrock-agentcore-control', region_name=region)\n", - "ecr_client = boto3.client('ecr', region_name=region)\n", - "ssm_client = boto3.client('ssm', region_name=region)\n", - "secrets_client = boto3.client('secretsmanager', region_name=region)\n", - "\n", - "try:\n", - " print(\"Deleting AgentCore Runtime...\")\n", - " runtime_delete_response = agentcore_control_client.delete_agent_runtime(\n", - " agentRuntimeId=launch_result.agent_id,\n", - " )\n", - " print(\"โœ“ AgentCore Runtime deletion initiated\")\n", + "# try:\n", + "# ssm_client.delete_parameter(Name='/mcp_server/runtime/agent_arn')\n", + "# print(\"โœ“ Parameter Store parameter deleted\")\n", + "# except ssm_client.exceptions.ParameterNotFound:\n", + "# print(\"โ„น๏ธ Parameter Store parameter not found\")\n", "\n", - " print(\"Deleting ECR repository...\")\n", - " ecr_repo_name = launch_result.ecr_uri.split('/')[1]\n", - " ecr_client.delete_repository(\n", - " repositoryName=ecr_repo_name,\n", - " force=True\n", - " )\n", - " print(\"โœ“ ECR repository deleted\")\n", + "# try:\n", + "# secrets_client.delete_secret(\n", + "# SecretId='mcp_server/cognito/credentials',\n", + "# ForceDeleteWithoutRecovery=True\n", + "# )\n", + "# print(\"โœ“ Secrets Manager secret deleted\")\n", + "# except secrets_client.exceptions.ResourceNotFoundException:\n", + "# print(\"โ„น๏ธ Secrets Manager secret not found\")\n", "\n", - " try:\n", - " ssm_client.delete_parameter(Name='/mcp_server/runtime/agent_arn')\n", - " print(\"โœ“ Parameter Store parameter deleted\")\n", - " except ssm_client.exceptions.ParameterNotFound:\n", - " print(\"โ„น๏ธ Parameter Store parameter not found\")\n", - " try:\n", - " secrets_client.delete_secret(\n", - " SecretId='mcp_server/cognito/credentials',\n", - " ForceDeleteWithoutRecovery=True\n", - " )\n", - " print(\"โœ“ Secrets Manager secret deleted\")\n", - " except secrets_client.exceptions.ResourceNotFoundException:\n", - " print(\"โ„น๏ธ Secrets Manager secret not found\")\n", - "\n", - " print(\"\\nโœ… Cleanup completed successfully!\")\n", - " \n", - "except Exception as e:\n", - " print(f\"โŒ Error during cleanup: {e}\")\n", - " print(\"You may need to manually clean up some resources.\")" + "# print(\"\\nโœ… Cleanup completed successfully!\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b29754a7", + "metadata": {}, + "outputs": [], + "source": [ + "# destroy_bedrock_agentcore(\n", + "# config_path=Path(\".bedrock_agentcore.yaml\"),\n", + "# agent_name=tool_name,\n", + "# delete_ecr_repo=True\n", + "# )" ] }, { @@ -824,7 +792,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": ".venv", "language": "python", "name": "python3" }, @@ -838,7 +806,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.9" + "version": "3.13.2" } }, "nbformat": 4, diff --git a/01-tutorials/01-AgentCore-runtime/02-hosting-MCP-server/hosting_mcp_server_iam_auth.ipynb b/01-tutorials/01-AgentCore-runtime/02-hosting-MCP-server/hosting_mcp_server_iam_auth.ipynb new file mode 100644 index 00000000..655400e9 --- /dev/null +++ b/01-tutorials/01-AgentCore-runtime/02-hosting-MCP-server/hosting_mcp_server_iam_auth.ipynb @@ -0,0 +1,882 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "5c0122e65c053f38", + "metadata": {}, + "source": [ + "# Hosting MCP Server on Amazon Bedrock AgentCore Runtime - AWS IAM Inbound Authentication\n", + "\n", + "## Overview\n", + "\n", + "In this tutorial we will learn how to host MCP (Model Context Protocol) servers on Amazon Bedrock AgentCore Runtime. We will use the Amazon Bedrock AgentCore Python SDK to wrap MCP tools as an MCP server compatible with Amazon Bedrock AgentCore.\n", + "\n", + "The Amazon Bedrock AgentCore Python SDK handles the MCP server implementation details so you can focus on your tools' core functionality. It transforms your code into the AgentCore standardized MCP protocol contracts for direct communication.\n", + "\n", + "While the [MCP protocol](https://modelcontextprotocol.io/docs/getting-started/intro) specification traditionally requires OAuth tokens for authentication, AgentCore runtime allows the ability to configure AWS IAM credentials for inbound requests to their MCP servers, addressing a crucial enterprise requirement.\n", + "\n", + "### Tutorial Details\n", + "\n", + "| Information | Details |\n", + "|:--------------------|:----------------------------------------------------------|\n", + "| Tutorial type | Hosting Tools |\n", + "| Tool type | MCP server |\n", + "| Tutorial components | Hosting MCP server on AgentCore Runtime |\n", + "| Tutorial vertical | Cross-vertical |\n", + "| Example complexity | Easy |\n", + "| SDK used | Amazon BedrockAgentCore Python SDK and MCP |\n", + "\n", + "### Tutorial Architecture\n", + "\n", + "In this tutorial we will describe how to deploy an MCP server to AgentCore runtime.\n", + "\n", + "For demonstration purposes, we will use a simple MCP server with 3 tools: `add_numbers`, `multiply_numbers` and `greet_user`\n", + "\n", + "
\n", + " \n", + "
\n", + "\n", + "### Tutorial Key Features\n", + "\n", + "* Creating MCP servers with custom tools\n", + "* Testing MCP servers locally\n", + "* Hosting MCP servers on Amazon Bedrock AgentCore Runtime\n", + "* Invoking deployed MCP servers with authentication\n" + ] + }, + { + "cell_type": "markdown", + "id": "3a676f58ecf52b42", + "metadata": {}, + "source": [ + "## Prerequisites\n", + "\n", + "To execute this tutorial you will need:\n", + "* Python 3.10+\n", + "* AWS credentials configured\n", + "* Amazon Bedrock AgentCore SDK\n", + "* MCP (Model Context Protocol) library\n", + "* Running Docker daemon" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "initial_id", + "metadata": { + "jupyter": { + "is_executing": true + } + }, + "outputs": [], + "source": [ + "!pip install --force-reinstall -U -r requirements.txt --quiet" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6e35d127", + "metadata": {}, + "outputs": [], + "source": [ + "from bedrock_agentcore_starter_toolkit import Runtime\n", + "from bedrock_agentcore_starter_toolkit.operations.runtime import destroy_bedrock_agentcore\n", + "from boto3.session import Session\n", + "from pathlib import Path\n", + "import os" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "74841b8d", + "metadata": {}, + "outputs": [], + "source": [ + "boto_session = Session()\n", + "region = boto_session.region_name\n", + "\n", + "agentcore_control_client = boto_session.client(\"bedrock-agentcore-control\", region_name=region)\n", + "ssm_client = boto_session.client('ssm', region_name=region)\n", + "\n", + "tool_name = \"mcp_server_iam\"" + ] + }, + { + "cell_type": "markdown", + "id": "ca924a7a2731e26f", + "metadata": {}, + "source": [ + "## Understanding MCP (Model Context Protocol)\n", + "\n", + "MCP is a protocol that allows AI models to securely access external data and tools. Key concepts:\n", + "\n", + "* **Tools**: Functions that the AI can call to perform actions\n", + "* **Streamable HTTP**: Transport protocol used by AgentCore Runtime\n", + "* **Session Isolation**: Each client gets isolated sessions via `Mcp-Session-Id` header\n", + "* **Stateless Operation**: Servers must support stateless operation for scalability\n", + "\n", + "AgentCore Runtime expects MCP servers to be hosted on `0.0.0.0:8000/mcp` as the default path.\n", + "\n", + "### Project Structure\n", + "\n", + "Let's set up our project with the proper structure:\n", + "\n", + "```\n", + "mcp_server_project/\n", + "โ”œโ”€โ”€ mcp_server.py # Main MCP server code\n", + "โ”œโ”€โ”€ mcp_client.py # Local testing client\n", + "โ”œโ”€โ”€ mcp_client_remote.py # Remote testing client\n", + "โ”œโ”€โ”€ requirements.txt # Dependencies\n", + "โ””โ”€โ”€ __init__.py # Python package marker\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "step2_create_server", + "metadata": {}, + "source": [ + "## Creating MCP Server\n", + "\n", + "Now let's create our MCP server with three simple tools. The server uses FastMCP with `stateless_http=True` which is required for AgentCore Runtime compatibility." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b7d386ab54e85e63", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile mcp_server.py\n", + "from mcp.server.fastmcp import FastMCP\n", + "from starlette.responses import JSONResponse\n", + "\n", + "mcp = FastMCP(host=\"0.0.0.0\", stateless_http=True)\n", + "\n", + "@mcp.tool()\n", + "def add_numbers(a: int, b: int) -> int:\n", + " \"\"\"Add two numbers together\"\"\"\n", + " return a + b\n", + "\n", + "@mcp.tool()\n", + "def multiply_numbers(a: int, b: int) -> int:\n", + " \"\"\"Multiply two numbers together\"\"\"\n", + " return a * b\n", + "\n", + "@mcp.tool()\n", + "def greet_user(name: str) -> str:\n", + " \"\"\"Greet a user by name\"\"\"\n", + " return f\"Hello, {name}! Nice to meet you.\"\n", + "\n", + "if __name__ == \"__main__\":\n", + " mcp.run(transport=\"streamable-http\")" + ] + }, + { + "cell_type": "markdown", + "id": "understand_code", + "metadata": {}, + "source": [ + "### What This Code Does\n", + "\n", + "* **FastMCP**: Creates an MCP server that can host your tools\n", + "* **@mcp.tool()**: Decorator that turns your Python functions into MCP tools\n", + "* **stateless_http=True**: Required for AgentCore Runtime compatibility\n", + "* **Tools**: Three simple tools demonstrating different types of operations" + ] + }, + { + "cell_type": "markdown", + "id": "step3_local_client", + "metadata": {}, + "source": [ + "## Creating Local Testing Client\n", + "\n", + "Before deploying to AgentCore Runtime, let's create a client to test our MCP server locally:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1226d59e6b56c96", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile mcp_client.py\n", + "import asyncio\n", + "\n", + "from mcp import ClientSession\n", + "from mcp.client.streamable_http import streamablehttp_client\n", + "\n", + "async def main():\n", + " mcp_url = \"http://localhost:8000/mcp\"\n", + " headers = {}\n", + "\n", + " async with streamablehttp_client(mcp_url, headers, timeout=120, terminate_on_close=False) as (\n", + " read_stream,\n", + " write_stream,\n", + " _,\n", + " ):\n", + " async with ClientSession(read_stream, write_stream) as session:\n", + " await session.initialize()\n", + " tool_result = await session.list_tools()\n", + " print(\"Available tools:\")\n", + " for tool in tool_result.tools:\n", + " print(f\" - {tool.name}: {tool.description}\")\n", + "\n", + "if __name__ == \"__main__\":\n", + " asyncio.run(main())" + ] + }, + { + "cell_type": "markdown", + "id": "test_locally_instructions", + "metadata": {}, + "source": [ + " ### Testing Locally\n", + "\n", + "To test your MCP server locally:\n", + "\n", + "1. **Terminal 1**: Start the MCP server\n", + " ```bash\n", + " python mcp_server.py\n", + " ```\n", + " \n", + "2. **Terminal 2**: Run the test client\n", + " ```bash\n", + " python mcp_client.py\n", + " ```\n", + "\n", + "You should see your three tools listed in the output." + ] + }, + { + "cell_type": "markdown", + "id": "step6_configure_deployment", + "metadata": {}, + "source": [ + "## Configuring AgentCore Runtime Deployment\n", + "\n", + "Next we will use our starter toolkit to configure the AgentCore Runtime deployment with an entrypoint, the execution role we just created and a requirements file. We will also configure the starter kit to auto create the Amazon ECR repository on launch.\n", + "\n", + "During the configure step, your docker file will be generated based on your application code\n", + "\n", + "
\n", + " \n", + "
" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "configure_runtime", + "metadata": {}, + "outputs": [], + "source": [ + "print(f\"Using AWS region: {region}\")\n", + "\n", + "required_files = [\"mcp_server.py\", \"requirements.txt\"]\n", + "for file in required_files:\n", + " if not os.path.exists(file):\n", + " raise FileNotFoundError(f\"Required file {file} not found\")\n", + "print(\"All required files found โœ“\")\n", + "\n", + "agentcore_runtime = Runtime()\n", + "\n", + "print(\"Configuring AgentCore Runtime...\")\n", + "response = agentcore_runtime.configure(\n", + " entrypoint=\"mcp_server.py\",\n", + " auto_create_execution_role=True,\n", + " auto_create_ecr=True,\n", + " requirements_file=\"requirements.txt\",\n", + " region=region,\n", + " protocol=\"MCP\",\n", + " agent_name=tool_name,\n", + ")\n", + "print(\"Configuration completed โœ“\")" + ] + }, + { + "cell_type": "markdown", + "id": "step7_launch", + "metadata": {}, + "source": [ + "## Launching MCP Server to AgentCore Runtime\n", + "\n", + "Now that we've got a docker file, let's launch the MCP server to the AgentCore Runtime. This will create the Amazon ECR repository and the AgentCore Runtime\n", + "\n", + "
\n", + " \n", + "
" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "17a32ab8-7701-4900-8055-e24364bdf35c", + "metadata": {}, + "outputs": [], + "source": [ + "print(\"Launching MCP server to AgentCore Runtime...\")\n", + "print(\"This may take several minutes...\")\n", + "launch_result = agentcore_runtime.launch()\n", + "print(\"Launch completed โœ“\")\n", + "print(f\"Agent ARN: {launch_result.agent_arn}\")\n", + "print(f\"Agent ID: {launch_result.agent_id}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9b0aea8f", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "agent_arn_response = ssm_client.put_parameter(\n", + " Name='/mcp_server/runtime_iam/agent_arn',\n", + " Value=launch_result.agent_arn,\n", + " Type='String',\n", + " Description='Agent ARN for MCP server with inbound auth',\n", + " Overwrite=True\n", + ")\n", + "print(\"โœ“ Agent ARN stored in Parameter Store\")\n", + "\n", + "print(\"\\nConfiguration stored successfully!\")\n", + "print(f\"Agent ARN: {launch_result.agent_arn}\")" + ] + }, + { + "cell_type": "markdown", + "id": "step10_remote_client", + "metadata": {}, + "source": [ + "## Creating Remote Testing Client\n", + "\n", + "Now let's create a client to test our deployed MCP server. This client will retrieve the necessary credentials from AWS and connect to the deployed server:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "create_remote_client", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile mcp_client_remote.py \n", + "import asyncio\n", + "import sys\n", + "import logging\n", + "import boto3\n", + "from boto3.session import Session\n", + "from mcp import ClientSession\n", + "from mcp.client.streamable_http import streamablehttp_client\n", + "from streamable_http_sigv4 import streamablehttp_client_with_sigv4\n", + "\n", + "\n", + "logging.basicConfig(\n", + " level=logging.INFO, format=\"%(asctime)s - %(name)s - %(levelname)s - %(message)s\"\n", + ")\n", + "logger = logging.getLogger(__name__)\n", + "\n", + "\n", + "def create_streamable_http_transport_sigv4(\n", + " mcp_url: str, service_name: str, region: str\n", + "):\n", + " \"\"\"\n", + " Create a streamable HTTP transport with AWS SigV4 authentication.\n", + "\n", + " This function creates an MCP client transport that uses AWS Signature Version 4 (SigV4)\n", + " to authenticate requests. This is necessary because standard MCP clients don't natively\n", + " support AWS IAM authentication, and this bridges that gap.\n", + "\n", + " Args:\n", + " mcp_url (str): The URL of the MCP gateway endpoint\n", + " service_name (str): The AWS service name for SigV4 signing (typically \"bedrock-agentcore\")\n", + " region (str): The AWS region where the gateway is deployed\n", + "\n", + " Returns:\n", + " StreamableHTTPTransportWithSigV4: A transport instance configured for SigV4 auth\n", + "\n", + " Example:\n", + " >>> transport = create_streamable_http_transport_sigv4(\n", + " ... mcp_url=\".../mcp\",\n", + " ... service_name=\"bedrock-agentcore\",\n", + " ... region=\"us-west-2\"\n", + " ... )\n", + " \"\"\"\n", + " # Get AWS credentials from the current boto3 session\n", + " # These credentials will be used to sign requests with SigV4\n", + " session = boto3.Session()\n", + " credentials = session.get_credentials()\n", + "\n", + " # Create and return the custom transport with SigV4 signing capability\n", + " return streamablehttp_client_with_sigv4(\n", + " url=mcp_url,\n", + " credentials=credentials,\n", + " service=service_name,\n", + " region=region,\n", + " )\n", + "\n", + "\n", + "def get_full_tools_list(client):\n", + " \"\"\"\n", + " Retrieve the complete list of tools from an MCP client, handling pagination.\n", + "\n", + " MCP servers may return tools in paginated responses. This function handles the\n", + " pagination automatically and returns all available tools in a single list.\n", + "\n", + " Args:\n", + " client: An MCP client instance (from strands.tools.mcp.mcp_client.MCPClient)\n", + "\n", + " Returns:\n", + " list: A complete list of all tools available from the MCP server\n", + "\n", + " Example:\n", + " >>> mcp_client = MCPClient(lambda: create_transport())\n", + " >>> all_tools = get_full_tools_list(mcp_client)\n", + " >>> print(f\"Found {len(all_tools)} tools\")\n", + " \"\"\"\n", + " more_tools = True\n", + " tools = []\n", + " pagination_token = None\n", + "\n", + " # Loop until we've fetched all pages\n", + " while more_tools:\n", + " tmp_tools = client.list_tools_sync(pagination_token=pagination_token)\n", + "\n", + " tools.extend(tmp_tools)\n", + "\n", + " # Check if there are more pages to fetch\n", + " if tmp_tools.pagination_token is None:\n", + " # No more pages - we're done\n", + " more_tools = False\n", + " else:\n", + " # More pages exist - prepare to fetch the next one\n", + " more_tools = True\n", + " pagination_token = tmp_tools.pagination_token\n", + "\n", + " return tools\n", + "\n", + "\n", + "async def main():\n", + " boto_session = Session()\n", + " region = boto_session.region_name\n", + " print(f\"Using AWS region: {region}\")\n", + "\n", + " ssm_client = boto3.client(\"ssm\", region_name=region)\n", + "\n", + " agent_arn_response = ssm_client.get_parameter(\n", + " Name=\"/mcp_server/runtime_iam/agent_arn\"\n", + " )\n", + " agent_arn = agent_arn_response[\"Parameter\"][\"Value\"]\n", + " print(f\"Retrieved Agent ARN: {agent_arn}\")\n", + "\n", + " if not agent_arn:\n", + " print(\"โŒ Error: AGENT_ARN not found\")\n", + " sys.exit(1)\n", + "\n", + " encoded_arn = agent_arn.replace(\":\", \"%3A\").replace(\"/\", \"%2F\")\n", + " mcp_url = f\"https://bedrock-agentcore.{region}.amazonaws.com/runtimes/{encoded_arn}/invocations?qualifier=DEFAULT\"\n", + "\n", + " try:\n", + " async with create_streamable_http_transport_sigv4(\n", + " mcp_url=mcp_url, service_name=\"bedrock-agentcore\", region=region\n", + " ) as (\n", + " read_stream,\n", + " write_stream,\n", + " _,\n", + " ):\n", + " async with ClientSession(read_stream, write_stream) as session:\n", + " print(\"\\n๐Ÿ”„ Initializing MCP session...\")\n", + " await session.initialize()\n", + " print(\"โœ“ MCP session initialized\")\n", + "\n", + " print(\"\\n๐Ÿ”„ Listing available tools...\")\n", + " tool_result = await session.list_tools()\n", + "\n", + " print(\"\\n๐Ÿ“‹ Available MCP Tools:\")\n", + " print(\"=\" * 50)\n", + " for tool in tool_result.tools:\n", + " print(f\"๐Ÿ”ง {tool.name}\")\n", + " print(f\" Description: {tool.description}\")\n", + " if hasattr(tool, \"inputSchema\") and tool.inputSchema:\n", + " properties = tool.inputSchema.get(\"properties\", {})\n", + " if properties:\n", + " print(f\" Parameters: {list(properties.keys())}\")\n", + " print()\n", + "\n", + " print(f\"โœ… Successfully connected to MCP server!\")\n", + " print(f\"Found {len(tool_result.tools)} tools available.\")\n", + "\n", + " except Exception as e:\n", + " print(f\"โŒ Error connecting to MCP server: {e}\")\n", + " import traceback\n", + "\n", + " print(\"\\n๐Ÿ” Full error traceback:\")\n", + " traceback.print_exc()\n", + " sys.exit(1)\n", + "\n", + "\n", + "if __name__ == \"__main__\":\n", + " asyncio.run(main())\n" + ] + }, + { + "cell_type": "markdown", + "id": "step11_test_remote", + "metadata": {}, + "source": [ + "## Testing Your Deployed MCP Server\n", + "\n", + "Let's test our deployed MCP server using the remote client:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "test_remote_server", + "metadata": {}, + "outputs": [], + "source": [ + "print(\"Testing deployed MCP server...\")\n", + "print(\"=\" * 50)\n", + "!python mcp_client_remote.py" + ] + }, + { + "cell_type": "markdown", + "id": "step12_invoke_tools", + "metadata": {}, + "source": [ + "### Invoking MCP Tools Remotely\n", + "\n", + "Now let's create an enhanced client that not only lists tools but also invokes them to demonstrate the full MCP functionality:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "invoke_mcp_tools", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile invoke_mcp_tools.py\n", + "import asyncio\n", + "import sys\n", + "import os\n", + "import logging\n", + "import boto3\n", + "from boto3.session import Session\n", + "from mcp import ClientSession\n", + "from mcp.client.streamable_http import streamablehttp_client\n", + "from streamable_http_sigv4 import streamablehttp_client_with_sigv4\n", + "\n", + "logging.basicConfig(\n", + " level=logging.INFO, format=\"%(asctime)s - %(name)s - %(levelname)s - %(message)s\"\n", + ")\n", + "logger = logging.getLogger(__name__)\n", + "\n", + "\n", + "def create_streamable_http_transport_sigv4(\n", + " mcp_url: str, service_name: str, region: str\n", + "):\n", + " \"\"\"\n", + " Create a streamable HTTP transport with AWS SigV4 authentication.\n", + "\n", + " This function creates an MCP client transport that uses AWS Signature Version 4 (SigV4)\n", + " to authenticate requests. This is necessary because standard MCP clients don't natively\n", + " support AWS IAM authentication, and this bridges that gap.\n", + "\n", + " Args:\n", + " mcp_url (str): The URL of the MCP gateway endpoint\n", + " service_name (str): The AWS service name for SigV4 signing (typically \"bedrock-agentcore\")\n", + " region (str): The AWS region where the gateway is deployed\n", + "\n", + " Returns:\n", + " StreamableHTTPTransportWithSigV4: A transport instance configured for SigV4 auth\n", + "\n", + " Example:\n", + " >>> transport = create_streamable_http_transport_sigv4(\n", + " ... mcp_url=\".../mcp\",\n", + " ... service_name=\"bedrock-agentcore\",\n", + " ... region=\"us-west-2\"\n", + " ... )\n", + " \"\"\"\n", + " # Get AWS credentials from the current boto3 session\n", + " # These credentials will be used to sign requests with SigV4\n", + " session = boto3.Session()\n", + " credentials = session.get_credentials()\n", + "\n", + " # Create and return the custom transport with SigV4 signing capability\n", + " return streamablehttp_client_with_sigv4(\n", + " url=mcp_url,\n", + " credentials=credentials,\n", + " service=service_name,\n", + " region=region,\n", + " )\n", + "\n", + "\n", + "def get_full_tools_list(client):\n", + " \"\"\"\n", + " Retrieve the complete list of tools from an MCP client, handling pagination.\n", + "\n", + " MCP servers may return tools in paginated responses. This function handles the\n", + " pagination automatically and returns all available tools in a single list.\n", + "\n", + " Args:\n", + " client: An MCP client instance (from strands.tools.mcp.mcp_client.MCPClient)\n", + "\n", + " Returns:\n", + " list: A complete list of all tools available from the MCP server\n", + "\n", + " Example:\n", + " >>> mcp_client = MCPClient(lambda: create_transport())\n", + " >>> all_tools = get_full_tools_list(mcp_client)\n", + " >>> print(f\"Found {len(all_tools)} tools\")\n", + " \"\"\"\n", + " more_tools = True\n", + " tools = []\n", + " pagination_token = None\n", + "\n", + " # Loop until we've fetched all pages\n", + " while more_tools:\n", + " tmp_tools = client.list_tools_sync(pagination_token=pagination_token)\n", + "\n", + " tools.extend(tmp_tools)\n", + "\n", + " # Check if there are more pages to fetch\n", + " if tmp_tools.pagination_token is None:\n", + " # No more pages - we're done\n", + " more_tools = False\n", + " else:\n", + " # More pages exist - prepare to fetch the next one\n", + " more_tools = True\n", + " pagination_token = tmp_tools.pagination_token\n", + "\n", + " return tools\n", + "\n", + "\n", + "async def main():\n", + " boto_session = Session()\n", + " region = boto_session.region_name\n", + " print(f\"Using AWS region: {region}\")\n", + "\n", + " ssm_client = boto3.client(\"ssm\", region_name=region)\n", + "\n", + " agent_arn_response = ssm_client.get_parameter(\n", + " Name=\"/mcp_server/runtime_iam/agent_arn\"\n", + " )\n", + " agent_arn = agent_arn_response[\"Parameter\"][\"Value\"]\n", + " print(f\"Retrieved Agent ARN: {agent_arn}\")\n", + "\n", + " if not agent_arn:\n", + " print(\"โŒ Error: AGENT_ARN not found\")\n", + " sys.exit(1)\n", + "\n", + " encoded_arn = agent_arn.replace(\":\", \"%3A\").replace(\"/\", \"%2F\")\n", + " mcp_url = f\"https://bedrock-agentcore.{region}.amazonaws.com/runtimes/{encoded_arn}/invocations?qualifier=DEFAULT\"\n", + "\n", + " try:\n", + " async with create_streamable_http_transport_sigv4(\n", + " mcp_url=mcp_url, service_name=\"bedrock-agentcore\", region=region\n", + " ) as (\n", + " read_stream,\n", + " write_stream,\n", + " _,\n", + " ):\n", + " async with ClientSession(read_stream, write_stream) as session:\n", + " print(\"\\n๐Ÿ”„ Initializing MCP session...\")\n", + " await session.initialize()\n", + " print(\"โœ“ MCP session initialized\")\n", + "\n", + " print(\"\\n๐Ÿ”„ Listing available tools...\")\n", + " tool_result = await session.list_tools()\n", + "\n", + " print(\"\\n๐Ÿ“‹ Available MCP Tools:\")\n", + " print(\"=\" * 50)\n", + " for tool in tool_result.tools:\n", + " print(f\"๐Ÿ”ง {tool.name}: {tool.description}\")\n", + "\n", + " print(\"\\n๐Ÿงช Testing MCP Tools:\")\n", + " print(\"=\" * 50)\n", + "\n", + " try:\n", + " print(\"\\nโž• Testing add_numbers(5, 3)...\")\n", + " add_result = await session.call_tool(\n", + " name=\"add_numbers\", arguments={\"a\": 5, \"b\": 3}\n", + " )\n", + " print(f\" Result: {add_result.content[0].text}\")\n", + " except Exception as e:\n", + " print(f\" Error: {e}\")\n", + "\n", + " try:\n", + " print(\"\\nโœ–๏ธ Testing multiply_numbers(4, 7)...\")\n", + " multiply_result = await session.call_tool(\n", + " name=\"multiply_numbers\", arguments={\"a\": 4, \"b\": 7}\n", + " )\n", + " print(f\" Result: {multiply_result.content[0].text}\")\n", + " except Exception as e:\n", + " print(f\" Error: {e}\")\n", + "\n", + " try:\n", + " print(\"\\n๐Ÿ‘‹ Testing greet_user('Alice')...\")\n", + " greet_result = await session.call_tool(\n", + " name=\"greet_user\", arguments={\"name\": \"Alice\"}\n", + " )\n", + " print(f\" Result: {greet_result.content[0].text}\")\n", + " except Exception as e:\n", + " print(f\" Error: {e}\")\n", + "\n", + " print(\"\\nโœ… MCP tool testing completed!\")\n", + "\n", + " except Exception as e:\n", + " print(f\"โŒ Error connecting to MCP server: {e}\")\n", + " import traceback\n", + "\n", + " print(\"\\n๐Ÿ” Full error traceback:\")\n", + " traceback.print_exc()\n", + " sys.exit(1)\n", + "\n", + "\n", + "if __name__ == \"__main__\":\n", + " asyncio.run(main())\n" + ] + }, + { + "cell_type": "markdown", + "id": "test_tool_invocation", + "metadata": {}, + "source": [ + "## Test Tool Invocation\n", + "\n", + "Let's test our MCP tools by actually invoking them:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "run_tool_tests", + "metadata": {}, + "outputs": [], + "source": [ + "print(\"Testing MCP tool invocation...\")\n", + "print(\"=\" * 50)\n", + "!python invoke_mcp_tools.py" + ] + }, + { + "cell_type": "markdown", + "id": "next_steps", + "metadata": {}, + "source": [ + "## Next Steps\n", + "\n", + "Now that you have successfully deployed an MCP server to AgentCore Runtime, you can:\n", + "\n", + "1. **Add More Tools**: Extend your MCP server with additional tools\n", + "2. **Custom Authentication**: Implement AWS IAM inbound authentication\n", + "3. **Integration**: Integrate with other AgentCore services" + ] + }, + { + "cell_type": "markdown", + "id": "cleanup_section", + "metadata": {}, + "source": [ + "## Cleanup (Optional)\n", + "\n", + "If you want to clean up the resources created during this tutorial, run the following cells:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "71cc0fbb", + "metadata": {}, + "outputs": [], + "source": [ + "# try:\n", + "# ssm_client.delete_parameter(Name='/mcp_server/runtime_iam/agent_arn')\n", + "# print(\"โœ“ Parameter Store parameter deleted\")\n", + "# except ssm_client.exceptions.ParameterNotFound:\n", + "# print(\"โ„น๏ธ Parameter Store parameter not found\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cleanup_resources", + "metadata": {}, + "outputs": [], + "source": [ + "# destroy_bedrock_agentcore(\n", + "# config_path=Path(\".bedrock_agentcore.yaml\"),\n", + "# agent_name=tool_name,\n", + "# delete_ecr_repo=True\n", + "# )" + ] + }, + { + "cell_type": "markdown", + "id": "congratulations", + "metadata": {}, + "source": [ + "# ๐ŸŽ‰ Congratulations!\n", + "\n", + "You have successfully:\n", + "\n", + "โœ… **Created an MCP server** with custom tools \n", + "โœ… **Tested locally** using MCP client \n", + "โœ… **Set up authentication** with Amazon Cognito \n", + "โœ… **Deployed to AWS** using AgentCore Runtime \n", + "โœ… **Invoked remotely** with proper authentication \n", + "โœ… **Learned MCP concepts** and best practices \n", + "\n", + "Your MCP server is now running on Amazon Bedrock AgentCore Runtime and ready for production use!\n", + "\n", + "## Summary\n", + "\n", + "In this tutorial, you learned how to:\n", + "- Build MCP servers using FastMCP\n", + "- Configure stateless HTTP transport for AgentCore compatibility\n", + "- Set up AWS IAM inbound authentication\n", + "- Deploy and manage MCP servers on AWS\n", + "- Test both locally and remotely\n", + "- Use MCP clients for tool invocation\n", + "\n", + "The deployed MCP server can now be integrated into larger AI applications and workflows!" + ] + }, + { + "cell_type": "markdown", + "id": "287db1dd", + "metadata": {}, + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/01-tutorials/01-AgentCore-runtime/02-hosting-MCP-server/streamable_http_sigv4.py b/01-tutorials/01-AgentCore-runtime/02-hosting-MCP-server/streamable_http_sigv4.py new file mode 100644 index 00000000..8bcd2ee1 --- /dev/null +++ b/01-tutorials/01-AgentCore-runtime/02-hosting-MCP-server/streamable_http_sigv4.py @@ -0,0 +1,152 @@ +""" +StreamableHTTP Client Transport with AWS SigV4 Signing + +This module extends the MCP StreamableHTTPTransport to add AWS SigV4 request signing +for authentication with MCP servers that authenticate using AWS IAM. +""" + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager +from datetime import timedelta +from typing import Generator + +import httpx +from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream +from botocore.auth import SigV4Auth +from botocore.awsrequest import AWSRequest +from botocore.credentials import Credentials +from mcp.client.streamable_http import ( + GetSessionIdCallback, + StreamableHTTPTransport, + streamablehttp_client, +) +from mcp.shared._httpx_utils import McpHttpClientFactory, create_mcp_http_client +from mcp.shared.message import SessionMessage + + +class SigV4HTTPXAuth(httpx.Auth): + """HTTPX Auth class that signs requests with AWS SigV4.""" + + def __init__( + self, + credentials: Credentials, + service: str, + region: str, + ): + self.credentials = credentials + self.service = service + self.region = region + self.signer = SigV4Auth(credentials, service, region) + + def auth_flow( + self, request: httpx.Request + ) -> Generator[httpx.Request, httpx.Response, None]: + """Signs the request with SigV4 and adds the signature to the request headers.""" + + # Create an AWS request + headers = dict(request.headers) + # Header 'connection' = 'keep-alive' is not used in calculating the request + # signature on the server-side, and results in a signature mismatch if included + headers.pop("connection", None) # Remove if present, ignore if not + + aws_request = AWSRequest( + method=request.method, + url=str(request.url), + data=request.content, + headers=headers, + ) + + # Sign the request with SigV4 + self.signer.add_auth(aws_request) + + # Add the signature header to the original request + request.headers.update(dict(aws_request.headers)) + + yield request + + +class StreamableHTTPTransportWithSigV4(StreamableHTTPTransport): + """ + Streamable HTTP client transport with AWS SigV4 signing support. + + This transport enables communication with MCP servers that authenticate using AWS IAM, + such as servers behind a Lambda function URL or API Gateway. + """ + + def __init__( + self, + url: str, + credentials: Credentials, + service: str, + region: str, + headers: dict[str, str] | None = None, + timeout: float | timedelta = 30, + sse_read_timeout: float | timedelta = 60 * 5, + ) -> None: + """Initialize the StreamableHTTP transport with SigV4 signing. + + Args: + url: The endpoint URL. + credentials: AWS credentials for signing. + service: AWS service name (e.g., 'lambda'). + region: AWS region (e.g., 'us-east-1'). + headers: Optional headers to include in requests. + timeout: HTTP timeout for regular operations. + sse_read_timeout: Timeout for SSE read operations. + """ + # Initialize parent class with SigV4 auth handler + super().__init__( + url=url, + headers=headers, + timeout=timeout, + sse_read_timeout=sse_read_timeout, + auth=SigV4HTTPXAuth(credentials, service, region), + ) + + self.credentials = credentials + self.service = service + self.region = region + + +@asynccontextmanager +async def streamablehttp_client_with_sigv4( + url: str, + credentials: Credentials, + service: str, + region: str, + headers: dict[str, str] | None = None, + timeout: float | timedelta = 30, + sse_read_timeout: float | timedelta = 60 * 5, + terminate_on_close: bool = True, + httpx_client_factory: McpHttpClientFactory = create_mcp_http_client, +) -> AsyncGenerator[ + tuple[ + MemoryObjectReceiveStream[SessionMessage | Exception], + MemoryObjectSendStream[SessionMessage], + GetSessionIdCallback, + ], + None, +]: + """ + Client transport for Streamable HTTP with SigV4 auth. + + This transport enables communication with MCP servers that authenticate using AWS IAM, + such as servers behind a Lambda function URL or API Gateway. + + Yields: + Tuple containing: + - read_stream: Stream for reading messages from the server + - write_stream: Stream for sending messages to the server + - get_session_id_callback: Function to retrieve the current session ID + """ + + async with streamablehttp_client( + url=url, + headers=headers, + timeout=timeout, + sse_read_timeout=sse_read_timeout, + terminate_on_close=terminate_on_close, + httpx_client_factory=httpx_client_factory, + auth=SigV4HTTPXAuth(credentials, service, region), + ) as result: + yield result