diff --git a/01-tutorials/01-AgentCore-runtime/06-mcp-stdio-server/.bedrock_agentcore.yaml b/01-tutorials/01-AgentCore-runtime/06-mcp-stdio-server/.bedrock_agentcore.yaml new file mode 100644 index 00000000..736b2237 --- /dev/null +++ b/01-tutorials/01-AgentCore-runtime/06-mcp-stdio-server/.bedrock_agentcore.yaml @@ -0,0 +1,34 @@ +default_agent: mcp_stdio +agents: + mcp_stdio: + name: mcp_stdio + entrypoint: main.py + platform: linux/arm64 + container_runtime: docker + aws: + execution_role: arn:aws:iam::416075262792:role/AmazonBedrockAgentCoreSDKRuntime-us-east-1-a1350239bb + execution_role_auto_create: false + account: '416075262792' + region: us-east-1 + ecr_repository: 416075262792.dkr.ecr.us-east-1.amazonaws.com/bedrock-agentcore-mcp_stdio + ecr_auto_create: false + network_configuration: + network_mode: PUBLIC + protocol_configuration: + server_protocol: MCP + observability: + enabled: true + bedrock_agentcore: + agent_id: mcp_stdio-gTGNJNErHb + agent_arn: arn:aws:bedrock-agentcore:us-east-1:416075262792:runtime/mcp_stdio-gTGNJNErHb + agent_session_id: null + codebuild: + project_name: null + execution_role: null + source_bucket: null + authorizer_configuration: + customJWTAuthorizer: + allowedClients: + - 3egn0ph7mmsg19c5cvraoroo2o + discoveryUrl: https://cognito-idp.eu-west-1.amazonaws.com/eu-west-1_B2tQC2cD4/.well-known/openid-configuration + oauth_configuration: null diff --git a/01-tutorials/01-AgentCore-runtime/06-mcp-stdio-server/Dockerfile b/01-tutorials/01-AgentCore-runtime/06-mcp-stdio-server/Dockerfile new file mode 100644 index 00000000..a3cbb420 --- /dev/null +++ b/01-tutorials/01-AgentCore-runtime/06-mcp-stdio-server/Dockerfile @@ -0,0 +1,10 @@ +FROM python:3.13-alpine +COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ +WORKDIR /app +COPY pyproject.toml . +RUN uv sync +COPY main.py . + +EXPOSE 8000 + +CMD ["uv", "run", "main.py"] \ No newline at end of file diff --git a/01-tutorials/01-AgentCore-runtime/06-mcp-stdio-server/main.py b/01-tutorials/01-AgentCore-runtime/06-mcp-stdio-server/main.py new file mode 100644 index 00000000..0a8c4ed1 --- /dev/null +++ b/01-tutorials/01-AgentCore-runtime/06-mcp-stdio-server/main.py @@ -0,0 +1,23 @@ +from fastmcp import FastMCP +from fastmcp.client.transports import StdioTransport +from fastmcp.server.proxy import ProxyClient +from starlette.responses import JSONResponse + +# Create a proxy directly from a config dictionary +transport = StdioTransport( + command="uv", + args=["run", "awslabs.aws-documentation-mcp-server"], +) + +# Create a proxy to the configured server (auto-creates ProxyClient) +proxy = FastMCP.as_proxy(ProxyClient(transport), name="Proxy", stateless_http=True) + + +@proxy.custom_route("/ping", ["GET"]) +def ping(req): + return JSONResponse({"status": "healthy"}) + + +# Run the proxy with stdio transport for local access +if __name__ == "__main__": + proxy.run(transport="streamable-http", host="0.0.0.0", port=8000) diff --git a/01-tutorials/01-AgentCore-runtime/06-mcp-stdio-server/mcp_client.ipynb b/01-tutorials/01-AgentCore-runtime/06-mcp-stdio-server/mcp_client.ipynb new file mode 100644 index 00000000..a614985e --- /dev/null +++ b/01-tutorials/01-AgentCore-runtime/06-mcp-stdio-server/mcp_client.ipynb @@ -0,0 +1,594 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "5c0122e65c053f38", + "metadata": {}, + "source": [ + "# Testing MCP Client with Typescript MCP Server on Amazon Bedrock AgentCore Runtime\n", + "\n", + "## Overview\n", + "\n", + "In this tutorial we will learn how to host an existing STDIO MCP server (Model Context Protocol) server using the Amazon Bedrock AgentCore runtime environment.\n", + "\n", + "### Tutorial Details\n", + "\n", + "| Information | Details |\n", + "|:--------------------|:----------------------------------------------------------|\n", + "| Tutorial type | Hosting existing STDIO MCP server |\n", + "| Tool type | MCP server |\n", + "| Tutorial components | Hosting STDIO MCP server on AgentCore Runtime |\n", + "| Tutorial vertical | Cross-vertical |\n", + "| Example complexity | Easy |\n", + "\n", + "\n", + "### Tutorial Overview\n", + "\n", + "1. The AgentCore Runtime authentication will use Amazon Cognito to provide JWT tokens for accessing our deployed MCP server.\n", + "\n", + "2. The mcp server is an existing MCP server using STDIO. We will use https://awslabs.github.io/mcp/servers/aws-documentation-mcp-server for this example\n", + "\n", + "3. The mcp client is written in python.\n", + " _Note the mcp client can be written in any language._" + ] + }, + { + "cell_type": "markdown", + "id": "3a676f58ecf52b42", + "metadata": {}, + "source": [ + "## Prerequisites\n", + "\n", + "To execute this tutorial you will need:\n", + "- Python 3.10+ (MCP client)\n", + "- Docker (for containerization) \n", + "- Amazon ECR (Elastic Container Registry) for storing Docker images \n", + "- AWS account with access to Bedrock AgentCore \n", + "- MCP (Model Context Protocol) library\n", + "- (Optional) MCP Inspector (`npx @modelcontextprotocol/inspector`)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "initial_id", + "metadata": { + "jupyter": { + "is_executing": true + } + }, + "outputs": [], + "source": [ + "#!uv add -r requirements.txt --active" + ] + }, + { + "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", + "* **Prompts**: Prompts allow servers to provide structured messages and instructions for interacting with LLM\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", + "The MCP protocol can use multiple transports, the most common one being STDIO. In this tutorial we show how to take an existing STDIO server and expose it via HTTP in AgentCore Runtime so that it can be made available to users without requiring installation on local machines. It also makes the tool available to Agents without requiring installation on the local agent environment." + ] + }, + { + "cell_type": "markdown", + "id": "step4_cognito_setup", + "metadata": {}, + "source": [ + "## Step 1: Setting up Amazon Cognito for Authentication\n", + "\n", + "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 create_agentcore_role, setup_cognito_user_pool" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "setup_cognito", + "metadata": {}, + "outputs": [], + "source": [ + "print(\"Setting up Amazon Cognito user pool...\")\n", + "cognito_config = setup_cognito_user_pool()\n", + "print(\"Cognito setup completed โœ“\")\n", + "print(f\"User Pool ID: {cognito_config.get('user_pool_id', 'N/A')}\")\n", + "print(f\"Client ID: {cognito_config.get('client_id', 'N/A')}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dffd282c", + "metadata": {}, + "outputs": [], + "source": [ + "CLIENT_ID = cognito_config.get('client_id', None)\n", + "DISCOVERY_URL = cognito_config.get('discovery_url', None)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0f398a9c", + "metadata": {}, + "outputs": [], + "source": [ + "import boto3\n", + "\n", + "sess = boto3.Session(region_name='us-east-1')\n", + "client = sess.client('sts')\n", + "ACCOUNT = client.get_caller_identity()['Account']" + ] + }, + { + "cell_type": "markdown", + "id": "9d0015ac", + "metadata": {}, + "source": [ + "## Step 2: Running the proxy MCP server\n", + "\n", + "Let's build a docker container that contain our STDIO MCP server and the proxy that exposes it as a streamable HTTP server.\n", + "We use [FastMCP proxy](https://gofastmcp.com/servers/proxy) functionality to expose the local STDIO server as HTTP." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "85ddca34", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile main.py\n", + "from fastmcp import FastMCP\n", + "from fastmcp.client.transports import StdioTransport\n", + "from fastmcp.server.proxy import ProxyClient\n", + "from starlette.responses import JSONResponse\n", + "\n", + "# Create a proxy directly from a config dictionary\n", + "transport = StdioTransport(\n", + " command=\"uv\",\n", + " args=[\"run\", \"awslabs.aws-documentation-mcp-server\"],\n", + ")\n", + "\n", + "# Create a proxy to the configured server (auto-creates ProxyClient)\n", + "proxy = FastMCP.as_proxy(ProxyClient(transport), name=\"Proxy\", stateless_http=True)\n", + "\n", + "\n", + "@proxy.custom_route(\"/ping\", [\"GET\"])\n", + "def ping(req):\n", + " return JSONResponse({\"status\": \"healthy\"})\n", + "\n", + "\n", + "# Run the proxy with stdio transport for local access\n", + "if __name__ == \"__main__\":\n", + " proxy.run(transport=\"streamable-http\", host=\"0.0.0.0\", port=8000)\n" + ] + }, + { + "cell_type": "markdown", + "id": "138c4fe8", + "metadata": {}, + "source": [ + "In a terminal run:\n", + "\n", + "```bash\n", + "docker build -t mcp-stdio-proxy .\n", + "```\n", + "\n", + "Once build we can run it and test it with the MCP Inspector (`npx @modelcontextprotocol/inspector`):\n", + "\n", + "```bash\n", + "docker run --rm -p 8000:8000 mcp-stdio-proxy\n", + "```\n" + ] + }, + { + "cell_type": "markdown", + "id": "step6_configure_deployment", + "metadata": {}, + "source": [ + "## Step 3: MCP Server Deployment to AgentCore\n", + "\n", + "We leverage the starter toolkit to deploy this project, by manually creating a .bedrock-agentcore.yaml file." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e2830561", + "metadata": {}, + "outputs": [], + "source": [ + "config = f\"\"\"\n", + "default_agent: mcp_stdio\n", + "agents:\n", + " mcp_stdio:\n", + " name: mcp_stdio\n", + " entrypoint: main.py\n", + " platform: linux/arm64\n", + " container_runtime: docker\n", + " aws:\n", + " execution_role: \n", + " execution_role_auto_create: true\n", + " account: \"{ACCOUNT}\"\n", + " region: us-east-1\n", + " ecr_repository:\n", + " ecr_auto_create: true\n", + " network_configuration:\n", + " network_mode: PUBLIC\n", + " protocol_configuration:\n", + " server_protocol: MCP\n", + " observability:\n", + " enabled: true\n", + " bedrock_agentcore:\n", + " agent_id: \n", + " agent_arn: \n", + " agent_session_id: null\n", + " codebuild:\n", + " project_name: null\n", + " execution_role: null\n", + " source_bucket: null\n", + " memory:\n", + " mode: null\n", + " authorizer_configuration: \n", + " customJWTAuthorizer: \n", + " allowedClients:\n", + " - {CLIENT_ID}\n", + " discoveryUrl: {DISCOVERY_URL}\n", + " request_header_configuration: null\n", + " oauth_configuration: null\n", + "\"\"\"\n", + "\n", + "with open(\".bedrock_agentcore.yaml\", \"w\") as f:\n", + " f.write(config)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "75ff71b0", + "metadata": {}, + "outputs": [], + "source": [ + "from bedrock_agentcore_starter_toolkit.operations.runtime.launch import launch_bedrock_agentcore" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "93903d71", + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "\n", + "result = launch_bedrock_agentcore(config_path=Path('.bedrock_agentcore.yaml'), use_codebuild=False, auto_update_on_conflict=True)" + ] + }, + { + "cell_type": "markdown", + "id": "step10_remote_client", + "metadata": {}, + "source": [ + "## Step 4: Creating 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": "d66e6613", + "metadata": {}, + "outputs": [], + "source": [ + "agent_arn = result.agent_arn\n", + " \n", + "encoded_arn = agent_arn.replace(':', '%3A').replace('/', '%2F')\n", + "mcp_url = f\"https://bedrock-agentcore.{sess.region_name}.amazonaws.com/runtimes/{encoded_arn}/invocations?qualifier=DEFAULT\"\n", + "print(mcp_url)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5d763f40", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "from utils import reauthenticate_user\n", + "bearer_token = reauthenticate_user(cognito_config['client_id'])\n", + "print(bearer_token)" + ] + }, + { + "cell_type": "markdown", + "id": "0ad5cd4a", + "metadata": {}, + "source": [ + "You can use the above information to test the client using MCP Inspector" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "create_remote_client", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "import sys\n", + "import traceback\n", + "\n", + "\n", + "from mcp import ClientSession\n", + "from mcp.client.streamable_http import streamablehttp_client\n", + "\n", + "import json\n", + "\n", + "async def list_tools():\n", + " agent_arn = result.agent_arn\n", + " bearer_token = reauthenticate_user(cognito_config['client_id'])\n", + " print(bearer_token)\n", + " encoded_arn = agent_arn.replace(':', '%3A').replace('/', '%2F')\n", + " mcp_url = f\"https://bedrock-agentcore.{sess.region_name}.amazonaws.com/runtimes/{encoded_arn}/invocations?qualifier=DEFAULT\"\n", + " headers = {\n", + " \"Authorization\": f\"Bearer {bearer_token}\",\n", + " \"Content-Type\": \"application/json\"\n", + " }\n", + " \n", + " print(f\"\\nConnecting to: {mcp_url}\")\n", + " print(\"Headers configured โœ“\")\n", + "\n", + " try:\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", + " 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", + " print(traceback.format_exc(e))\n", + " sys.exit(1)" + ] + }, + { + "cell_type": "markdown", + "id": "step11_test_remote", + "metadata": {}, + "source": [ + "## Step 7: 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", + "await list_tools()" + ] + }, + { + "cell_type": "markdown", + "id": "step12_invoke_tools", + "metadata": {}, + "source": [ + "## Step 8: 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": [ + "import sys\n", + "\n", + "from mcp import ClientSession\n", + "from mcp.client.streamable_http import streamablehttp_client\n", + "\n", + "async def call_tools():\n", + " agent_arn = result.agent_arn\n", + " bearer_token = reauthenticate_user(cognito_config['client_id'])\n", + " print(bearer_token)\n", + " encoded_arn = agent_arn.replace(':', '%3A').replace('/', '%2F')\n", + " mcp_url = f\"https://bedrock-agentcore.{sess.region_name}.amazonaws.com/runtimes/{encoded_arn}/invocations?qualifier=DEFAULT\"\n", + " headers = {\n", + " \"Authorization\": f\"Bearer {bearer_token}\",\n", + " \"Content-Type\": \"application/json\"\n", + " }\n", + " \n", + " print(f\"\\nConnecting to: {mcp_url}\")\n", + " print(\"Headers configured โœ“\")\n", + " try:\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", + " 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(\"\\nTesting read_documentation\")\n", + " add_result = await session.call_tool(\n", + " name=\"read_documentation\",\n", + " arguments={\"max_length\": 5000, \"url\": \"https://docs.aws.amazon.com/lambda/latest/dg/lambda-invocation.html\"}\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(\"\\nTesting search\")\n", + " add_result = await session.call_tool(\n", + " name=\"search\",\n", + " arguments={\"limit\": 10, \"search_phrase\": \"agentcore runtime\"}\n", + " )\n", + " print(f\" Result: {add_result.content[0].text}\")\n", + " except Exception as e:\n", + " print(f\" Error: {e}\")\n", + " \n", + " \n", + " print(\"\\nโœ… MCP tool testing completed!\")\n", + " \n", + " except Exception as e:\n", + " print(f\"โŒ Error connecting to MCP server: {e}\")\n", + " sys.exit(1)\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", + "await call_tools()" + ] + }, + { + "cell_type": "markdown", + "id": "next_steps", + "metadata": {}, + "source": [ + "## Next Steps\n", + "\n", + "Now that you have successfully deployed an STDIO MCP server to AgentCore Runtime, you can:\n", + "\n", + "1. **Try another STDIO MCP server**: Try to host other STDIO MCP servers and transform them in company wide accessible tools\n", + "2. **Configure other OAuth2 providers**: Implement custom JWT authorizers" + ] + }, + { + "cell_type": "markdown", + "id": "congratulations", + "metadata": {}, + "source": [ + "# ๐ŸŽ‰ Congratulations!\n", + "\n", + "You have successfully:\n", + "\n", + "โœ… **Hosted an existing STDIO MCP server** using FastMCP proxy \n", + "โœ… **Set up authentication** with Amazon Cognito \n", + "โœ… **Deployed to AWS** using AgentCore Runtime \n", + "โœ… **Tested MCP tools 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!" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "amazon-bedrock-agentcore-samples", + "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.12.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/01-tutorials/01-AgentCore-runtime/06-mcp-stdio-server/pyproject.toml b/01-tutorials/01-AgentCore-runtime/06-mcp-stdio-server/pyproject.toml new file mode 100644 index 00000000..dd4ade2a --- /dev/null +++ b/01-tutorials/01-AgentCore-runtime/06-mcp-stdio-server/pyproject.toml @@ -0,0 +1,10 @@ +[project] +name = "06-mcp-stdio-server" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "awslabs-aws-documentation-mcp-server>=1.1.9", + "fastmcp>=2.12.5", +]