diff --git a/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap b/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap index 89ef1df5..1e91f804 100644 --- a/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap +++ b/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap @@ -395,6 +395,11 @@ exports[`Assets Directory Snapshots > File listing > should match the expected f "cdk/tsconfig.json", "container/python/Dockerfile", "container/python/dockerignore.template", + "mcp/python-fastmcp-lambda/README.md", + "mcp/python-fastmcp-lambda/handler.py", + "mcp/python-fastmcp-lambda/pyproject.toml", + "mcp/python-fastmcp-lambda/run.sh", + "mcp/python-fastmcp-lambda/server.py", "mcp/python-lambda/README.md", "mcp/python-lambda/handler.py", "mcp/python-lambda/pyproject.toml", @@ -631,6 +636,188 @@ if __name__ == "__main__": " `; +exports[`Assets Directory Snapshots > MCP assets > mcp/mcp/python-fastmcp-lambda/README.md should match snapshot 1`] = ` +"# {{ Name }} + +FastMCP server running on AWS Lambda with a function URL, generated by the AgentCore CLI. + +Demonstrates HTTP tool patterns with proper error handling and retry logic. + +## How It Works + +This server uses [FastMCP](https://github.com/jlowin/fastmcp) to define MCP tools and +[Mangum](https://github.com/jordanh/mangum) to adapt the ASGI app for AWS Lambda. +The Lambda function URL provides the HTTP endpoint that the AgentCore gateway connects to. + +## Available Tools + +| Tool | Description | +| ----------------- | ------------------------------------------------------ | +| \`lookup_ip\` | Look up geolocation and network info for an IP address | +| \`get_random_user\` | Generate a random user profile for testing | +| \`fetch_post\` | Fetch a post by ID from JSONPlaceholder API | + +## Deployment + +\`\`\`bash +agentcore deploy +\`\`\` + +The CDK stack creates the Lambda function, function URL, and wires it to the gateway target. +" +`; + +exports[`Assets Directory Snapshots > MCP assets > mcp/mcp/python-fastmcp-lambda/handler.py should match snapshot 1`] = ` +"""" +FastMCP Server for AWS Lambda with Function URL. + +This template shows: +- FastMCP server running on Lambda via Lambda Web Adapter + uvicorn +- HTTP tool patterns with proper error handling +- Retry logic and response validation + +Deploy with: agentcore deploy +""" + +import logging +from typing import Any + +import httpx +from mcp.server.fastmcp import FastMCP + +logging.basicConfig(level=logging.INFO, format="%(levelname)s - %(message)s") +logger = logging.getLogger(__name__) + +mcp = FastMCP("{{ Name }}", stateless_http=True, host="0.0.0.0") + +HTTP_TIMEOUT = 10.0 +MAX_RETRIES = 2 + + +async def fetch_json(url: str, headers: dict[str, str] | None = None) -> dict[str, Any] | None: + """Make an HTTP GET request with retry logic.""" + async with httpx.AsyncClient() as client: + for attempt in range(MAX_RETRIES): + try: + response = await client.get(url, headers=headers, timeout=HTTP_TIMEOUT) + response.raise_for_status() + return response.json() + except httpx.TimeoutException: + logger.warning(f"Timeout on attempt {attempt + 1} for {url}") + except httpx.HTTPStatusError as e: + logger.error(f"HTTP {e.response.status_code} for {url}") + return None + except httpx.RequestError as e: + logger.error(f"Request failed: {e}") + return None + return None + + +@mcp.tool() +async def lookup_ip(ip_address: str) -> str: + """Look up geolocation and network info for an IP address. + + Args: + ip_address: IPv4 or IPv6 address to look up + """ + data = await fetch_json(f"http://ip-api.com/json/{ip_address}") + + if not data: + return f"Failed to look up IP: {ip_address}" + + if data.get("status") == "fail": + return f"Lookup failed: {data.get('message', 'unknown error')}" + + return ( + f"IP: {data['query']}\\n" + f"Location: {data['city']}, {data['regionName']}, {data['country']}\\n" + f"ISP: {data['isp']}\\n" + f"Organization: {data['org']}\\n" + f"Timezone: {data['timezone']}" + ) + + +@mcp.tool() +async def get_random_user() -> str: + """Generate a random user profile for testing or mock data.""" + data = await fetch_json("https://randomuser.me/api/") + + if not data or "results" not in data: + return "Failed to generate random user." + + user = data["results"][0] + name = user["name"] + location = user["location"] + + return ( + f"Name: {name['first']} {name['last']}\\n" + f"Email: {user['email']}\\n" + f"Location: {location['city']}, {location['country']}\\n" + f"Phone: {user['phone']}" + ) + + +@mcp.tool() +async def fetch_post(post_id: int) -> str: + """Fetch a post by ID from JSONPlaceholder API. + + Args: + post_id: The post ID (1-100) + """ + if not 1 <= post_id <= 100: + return "Post ID must be between 1 and 100." + + data = await fetch_json(f"https://jsonplaceholder.typicode.com/posts/{post_id}") + + if not data: + return f"Failed to fetch post {post_id}." + + return ( + f"Post #{data['id']}\\n" + f"Title: {data['title']}\\n\\n" + f"{data['body']}" + ) +" +`; + +exports[`Assets Directory Snapshots > MCP assets > mcp/mcp/python-fastmcp-lambda/pyproject.toml should match snapshot 1`] = ` +"[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "{{ name }}" +version = "0.1.0" +description = "FastMCP Server on AWS Lambda" +readme = "README.md" +requires-python = ">=3.10" +dependencies = [ + "mcp[cli] >= 1.18.0", + "httpx >= 0.27.0", + "fastapi >= 0.115.0", + "uvicorn >= 0.34.0", +] + +[tool.hatch.build.targets.wheel] +packages = ["."] +" +`; + +exports[`Assets Directory Snapshots > MCP assets > mcp/mcp/python-fastmcp-lambda/run.sh should match snapshot 1`] = ` +"#!/bin/bash +exec python -m uvicorn --port=$PORT server:app +" +`; + +exports[`Assets Directory Snapshots > MCP assets > mcp/mcp/python-fastmcp-lambda/server.py should match snapshot 1`] = ` +"from fastapi import FastAPI +from handler import mcp + +app = FastAPI(lifespan=lambda app: mcp.session_manager.run()) +app.mount("/", mcp.streamable_http_app()) +" +`; + exports[`Assets Directory Snapshots > MCP assets > mcp/mcp/python-lambda/README.md should match snapshot 1`] = ` "# {{ Name }} diff --git a/src/assets/mcp/python-fastmcp-lambda/README.md b/src/assets/mcp/python-fastmcp-lambda/README.md new file mode 100644 index 00000000..b525d2af --- /dev/null +++ b/src/assets/mcp/python-fastmcp-lambda/README.md @@ -0,0 +1,27 @@ +# {{ Name }} + +FastMCP server running on AWS Lambda with a function URL, generated by the AgentCore CLI. + +Demonstrates HTTP tool patterns with proper error handling and retry logic. + +## How It Works + +This server uses [FastMCP](https://github.com/jlowin/fastmcp) to define MCP tools and +[Mangum](https://github.com/jordanh/mangum) to adapt the ASGI app for AWS Lambda. +The Lambda function URL provides the HTTP endpoint that the AgentCore gateway connects to. + +## Available Tools + +| Tool | Description | +| ----------------- | ------------------------------------------------------ | +| `lookup_ip` | Look up geolocation and network info for an IP address | +| `get_random_user` | Generate a random user profile for testing | +| `fetch_post` | Fetch a post by ID from JSONPlaceholder API | + +## Deployment + +```bash +agentcore deploy +``` + +The CDK stack creates the Lambda function, function URL, and wires it to the gateway target. diff --git a/src/assets/mcp/python-fastmcp-lambda/handler.py b/src/assets/mcp/python-fastmcp-lambda/handler.py new file mode 100644 index 00000000..5b40cd93 --- /dev/null +++ b/src/assets/mcp/python-fastmcp-lambda/handler.py @@ -0,0 +1,109 @@ +""" +FastMCP Server for AWS Lambda with Function URL. + +This template shows: +- FastMCP server running on Lambda via Lambda Web Adapter + uvicorn +- HTTP tool patterns with proper error handling +- Retry logic and response validation + +Deploy with: agentcore deploy +""" + +import logging +from typing import Any + +import httpx +from mcp.server.fastmcp import FastMCP + +logging.basicConfig(level=logging.INFO, format="%(levelname)s - %(message)s") +logger = logging.getLogger(__name__) + +mcp = FastMCP("{{ Name }}", stateless_http=True, host="0.0.0.0") + +HTTP_TIMEOUT = 10.0 +MAX_RETRIES = 2 + + +async def fetch_json(url: str, headers: dict[str, str] | None = None) -> dict[str, Any] | None: + """Make an HTTP GET request with retry logic.""" + async with httpx.AsyncClient() as client: + for attempt in range(MAX_RETRIES): + try: + response = await client.get(url, headers=headers, timeout=HTTP_TIMEOUT) + response.raise_for_status() + return response.json() + except httpx.TimeoutException: + logger.warning(f"Timeout on attempt {attempt + 1} for {url}") + except httpx.HTTPStatusError as e: + logger.error(f"HTTP {e.response.status_code} for {url}") + return None + except httpx.RequestError as e: + logger.error(f"Request failed: {e}") + return None + return None + + +@mcp.tool() +async def lookup_ip(ip_address: str) -> str: + """Look up geolocation and network info for an IP address. + + Args: + ip_address: IPv4 or IPv6 address to look up + """ + data = await fetch_json(f"http://ip-api.com/json/{ip_address}") + + if not data: + return f"Failed to look up IP: {ip_address}" + + if data.get("status") == "fail": + return f"Lookup failed: {data.get('message', 'unknown error')}" + + return ( + f"IP: {data['query']}\n" + f"Location: {data['city']}, {data['regionName']}, {data['country']}\n" + f"ISP: {data['isp']}\n" + f"Organization: {data['org']}\n" + f"Timezone: {data['timezone']}" + ) + + +@mcp.tool() +async def get_random_user() -> str: + """Generate a random user profile for testing or mock data.""" + data = await fetch_json("https://randomuser.me/api/") + + if not data or "results" not in data: + return "Failed to generate random user." + + user = data["results"][0] + name = user["name"] + location = user["location"] + + return ( + f"Name: {name['first']} {name['last']}\n" + f"Email: {user['email']}\n" + f"Location: {location['city']}, {location['country']}\n" + f"Phone: {user['phone']}" + ) + + +@mcp.tool() +async def fetch_post(post_id: int) -> str: + """Fetch a post by ID from JSONPlaceholder API. + + Args: + post_id: The post ID (1-100) + """ + if not 1 <= post_id <= 100: + return "Post ID must be between 1 and 100." + + data = await fetch_json(f"https://jsonplaceholder.typicode.com/posts/{post_id}") + + if not data: + return f"Failed to fetch post {post_id}." + + return ( + f"Post #{data['id']}\n" + f"Title: {data['title']}\n\n" + f"{data['body']}" + ) diff --git a/src/assets/mcp/python-fastmcp-lambda/pyproject.toml b/src/assets/mcp/python-fastmcp-lambda/pyproject.toml new file mode 100644 index 00000000..244012d2 --- /dev/null +++ b/src/assets/mcp/python-fastmcp-lambda/pyproject.toml @@ -0,0 +1,19 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "{{ name }}" +version = "0.1.0" +description = "FastMCP Server on AWS Lambda" +readme = "README.md" +requires-python = ">=3.10" +dependencies = [ + "mcp[cli] >= 1.18.0", + "httpx >= 0.27.0", + "fastapi >= 0.115.0", + "uvicorn >= 0.34.0", +] + +[tool.hatch.build.targets.wheel] +packages = ["."] diff --git a/src/assets/mcp/python-fastmcp-lambda/run.sh b/src/assets/mcp/python-fastmcp-lambda/run.sh new file mode 100755 index 00000000..bb9f89b1 --- /dev/null +++ b/src/assets/mcp/python-fastmcp-lambda/run.sh @@ -0,0 +1,2 @@ +#!/bin/bash +exec python -m uvicorn --port=$PORT server:app diff --git a/src/assets/mcp/python-fastmcp-lambda/server.py b/src/assets/mcp/python-fastmcp-lambda/server.py new file mode 100644 index 00000000..653a8a18 --- /dev/null +++ b/src/assets/mcp/python-fastmcp-lambda/server.py @@ -0,0 +1,5 @@ +from fastapi import FastAPI +from handler import mcp + +app = FastAPI(lifespan=lambda app: mcp.session_manager.run()) +app.mount("/", mcp.streamable_http_app()) diff --git a/src/cli/templates/GatewayTargetRenderer.ts b/src/cli/templates/GatewayTargetRenderer.ts index 12e25f4d..3505a16a 100644 --- a/src/cli/templates/GatewayTargetRenderer.ts +++ b/src/cli/templates/GatewayTargetRenderer.ts @@ -79,7 +79,7 @@ export async function renderGatewayTargetTemplate( } // Select template based on compute host - const templateSubdir = host === 'Lambda' ? 'python-lambda' : 'python'; + const templateSubdir = host === 'Lambda' ? 'python-fastmcp-lambda' : 'python'; const templateDir = getTemplatePath('mcp', templateSubdir); await copyAndRenderDir(templateDir, outputDir, { Name: toolName });