Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions src/assets/templates/python-mcp/Dockerfile.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
FROM public.ecr.aws/docker/library/python:3.12-slim-trixie

RUN pip install --no-cache-dir uv

ARG UV_DEFAULT_INDEX
ARG UV_INDEX

WORKDIR /app

ENV UV_SYSTEM_PYTHON=1 \
UV_COMPILE_BYTECODE=1 \
UV_NO_PROGRESS=1 \
PYTHONUNBUFFERED=1 \
DOCKER_CONTAINER=1 \
UV_DEFAULT_INDEX=${UV_DEFAULT_INDEX} \
UV_INDEX=${UV_INDEX} \
PATH="/app/.venv/bin:$PATH"

RUN useradd -m -u 1000 bedrock_agentcore

COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev --no-install-project

COPY --chown=bedrock_agentcore:bedrock_agentcore . .
RUN uv sync --frozen --no-dev

USER bedrock_agentcore

# AgentCore Runtime service contract ports
# https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-service-contract.html
# 8080: HTTP Mode
# 8000: MCP Mode
# 9000: A2A Mode
EXPOSE 8080 8000 9000

{{#if enableOtel}}
CMD ["opentelemetry-instrument", "python", "-m", "{{entrypoint}}"]
{{else}}
CMD ["python", "-m", "{{entrypoint}}"]
{{/if}}
32 changes: 32 additions & 0 deletions src/assets/templates/python-mcp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# {{ name }}

An MCP (Model Context Protocol) server deployed on Amazon Bedrock AgentCore.

## Overview

This project implements an MCP server using FastMCP. MCP servers expose tools that can be
consumed by MCP clients (other agents or applications). The server speaks Streamable HTTP
transport at `/mcp`, matching the AgentCore Runtime MCP service contract.

## Adding Tools

Define tools using the `@mcp.tool()` decorator in `main.py`:

```python
@mcp.tool()
def my_tool(param: str) -> str:
"""Description of what the tool does."""
return f"Result: {param}"
```

## Developing locally

If installation was successful, a virtual environment is already created with dependencies installed.

`agentcore project dev` starts the server locally on `0.0.0.0:8000`. List and call tools by
sending JSON-RPC to `http://127.0.0.1:8000/mcp`.

## Deployment

`agentcore project deploy` deploys the server into Amazon Bedrock AgentCore. Invoke it with
`agentcore project invoke runtime`, supplying an MCP JSON-RPC payload (e.g. `tools/list`, `tools/call`).
27 changes: 27 additions & 0 deletions src/assets/templates/python-mcp/dockerignore.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Python
__pycache__/
*.py[cod]
*.egg-info/
.venv/
dist/
build/

# IDE
.vscode/
.idea/

# Testing
.pytest_cache/
.coverage
htmlcov/

# Secrets and environment files
.env
.env.*

# Version control
.git/

# AgentCore build artifacts
.agentcore/artifacts/
*.zip
41 changes: 41 additions & 0 deletions src/assets/templates/python-mcp/gitignore.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Environment variables
.env

# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg

# Virtual environments
.venv/
venv/
ENV/
env/

# IDE
.vscode/
.idea/
*.swp
*.swo
*~

# OS
.DS_Store
Thumbs.db
83 changes: 83 additions & 0 deletions src/assets/templates/python-mcp/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
{{#if needsOs}}
import os
{{/if}}
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("{{ name }}", host="0.0.0.0", stateless_http=True)

{{#if needsOs}}
_MOUNT_PATHS = [
{{#if sessionStorageMountPath}}"{{sessionStorageMountPath}}",{{/if}}
{{#each efsMounts}}"{{mountPath}}",{{/each}}
{{#each s3Mounts}}"{{mountPath}}",{{/each}}
]

def _safe_resolve(path: str) -> str:
resolved = os.path.realpath(path)
if not any(resolved == os.path.realpath(m) or resolved.startswith(os.path.realpath(m) + os.sep) for m in _MOUNT_PATHS):
raise ValueError(f"Path '{path}' is not within any configured mount ({', '.join(_MOUNT_PATHS)})")
return resolved

@mcp.tool()
def file_read(path: str) -> str:
"""Read a file from a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
try:
full_path = _safe_resolve(path)
with open(full_path) as f:
return f.read()
except ValueError as e:
return str(e)
except OSError as e:
return f"Error reading '{path}': {e.strerror}"

@mcp.tool()
def file_write(path: str, content: str) -> str:
"""Write a file to a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
try:
full_path = _safe_resolve(path)
parent = os.path.dirname(full_path)
if parent:
os.makedirs(parent, exist_ok=True)
with open(full_path, "w") as f:
f.write(content)
return f"Written to {path}"
except ValueError as e:
return str(e)
except OSError as e:
return f"Error writing '{path}': {e.strerror}"

@mcp.tool()
def list_files(path: str) -> str:
"""List files in a mounted filesystem directory. Use the absolute path (e.g. /mnt/tools)."""
try:
full_path = _safe_resolve(path)
entries = os.listdir(full_path)
return "\n".join(entries) if entries else "(empty directory)"
except ValueError as e:
return str(e)
except OSError as e:
return f"Error listing '{path}': {e.strerror}"

{{/if}}


@mcp.tool()
def add_numbers(a: int, b: int) -> int:
"""Add two numbers together"""
return a + b


@mcp.tool()
def multiply_numbers(a: int, b: int) -> int:
"""Multiply two numbers together"""
return a * b


@mcp.tool()
def greet_user(name: str) -> str:
"""Greet a user by name"""
return f"Hello, {name}! Nice to meet you."


if __name__ == "__main__":
mcp.run(transport="streamable-http")
17 changes: 17 additions & 0 deletions src/assets/templates/python-mcp/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[build-system]
requires = ["hatchling ~= 1.27.0"]
build-backend = "hatchling.build"

[project]
name = "{{ name }}"
version = "0.1.0"
description = "AgentCore MCP Server"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"aws-opentelemetry-distro ~= 0.18.0",
"mcp ~= 1.24.0",
]

[tool.hatch.build.targets.wheel]
packages = ["."]
67 changes: 55 additions & 12 deletions src/core/project/templates/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,9 @@ function toNpmPackageName(name: string): string {
function buildResolverKey(
framework: ScaffoldRuntimeInput["framework"],
language: ScaffoldRuntimeInput["language"],
): `${ScaffoldRuntimeInput["framework"]}/${ScaffoldRuntimeInput["language"]}` {
return `${framework}/${language}`;
protocol: ScaffoldRuntimeInput["protocol"],
): `${ScaffoldRuntimeInput["framework"]}/${ScaffoldRuntimeInput["language"]}/${NonNullable<ScaffoldRuntimeInput["protocol"]>}` {
return `${framework}/${language}/${protocol ?? "HTTP"}`;
}

// The IAM policy file the proxy template vends; wired into the runtime's
Expand Down Expand Up @@ -116,9 +117,7 @@ const importBedrockAgentResolver =
};

const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: TemplateRenderer) => ({
[buildResolverKey("none", "Python")]: async (input: RuntimeResourceConfig) => {
if (input.protocol !== undefined && input.protocol !== "HTTP")
throw new InputValidationError(`hello-world-python only supports HTTP protocol`);
[buildResolverKey("none", "Python", "HTTP")]: async (input: RuntimeResourceConfig) => {
if (input.scaffoldRuntimeInput.memory !== undefined)
throw new InputValidationError(`memory is not supported with the hello-world template`);
const tree = await FsTreeNode.fromAssetSource(
Expand All @@ -133,10 +132,7 @@ const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: Templa
);
return { tree, spec: { runtimes: [buildRuntimeSpec(input)] } };
},
[buildResolverKey("strands", "Python")]: async (input: RuntimeResourceConfig) => {
if (input.protocol !== undefined && input.protocol !== "HTTP")
throw new InputValidationError("the strands-python template only supports HTTP");

[buildResolverKey("strands", "Python", "HTTP")]: async (input: RuntimeResourceConfig) => {
const filesystemConfigurations = input.filesystemConfigurations ?? [];
const sessionStorageMountPath = filesystemConfigurations.flatMap((configuration) =>
"sessionStorage" in configuration ? [configuration.sessionStorage.mountPath] : [],
Expand Down Expand Up @@ -201,7 +197,7 @@ const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: Templa
},
};
},
[buildResolverKey("strands", "TypeScript")]: async (input: RuntimeResourceConfig) => {
[buildResolverKey("strands", "TypeScript", "HTTP")]: async (input: RuntimeResourceConfig) => {
if (input.protocol !== undefined && input.protocol !== "HTTP")
throw new InputValidationError("the strands-ts template only supports HTTP");

Expand Down Expand Up @@ -244,6 +240,53 @@ const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: Templa
},
};
},
[buildResolverKey("none", "Python", "MCP")]: async (input: RuntimeResourceConfig) => {
if (input.scaffoldRuntimeInput.memory !== undefined)
throw new InputValidationError("memory is not supported with an MCP runtime");
const filesystemConfigurations = input.filesystemConfigurations ?? [];
const sessionStorageMountPath = filesystemConfigurations.flatMap((configuration) =>
"sessionStorage" in configuration ? [configuration.sessionStorage.mountPath] : [],
)[0];
const efsMounts = filesystemConfigurations.flatMap((configuration) =>
"efsAccessPoint" in configuration
? [{ mountPath: configuration.efsAccessPoint.mountPath }]
: [],
);
const s3Mounts = filesystemConfigurations.flatMap((configuration) =>
"s3FilesAccessPoint" in configuration
? [{ mountPath: configuration.s3FilesAccessPoint.mountPath }]
: [],
);
const context = {
name: toPythonPackageName(input.name),
sessionStorageMountPath,
efsMounts,
s3Mounts,
needsOs: filesystemConfigurations.length > 0,
// The AgentCore Runtime requires OTEL dependencies to be present; the
// container launches main.py as the `main` module under
// opentelemetry-instrument, and FastMCP binds the streamable-HTTP server.
enableOtel: true,
entrypoint: "main",
};
const isContainer = input.scaffoldRuntimeInput.build === "Container";
const tree = await FsTreeNode.fromAssetSource(
{ assetSource },
{ assetDir: "templates/python-mcp" },
{
rootDirName: input.name,
transformContent: (raw) => templateRenderer.render(raw, context),
filter: (name) => {
if (name === "Dockerfile" || name === ".dockerignore") return isContainer;
return true;
},
},
);
return {
tree,
spec: { runtimes: [{ ...buildRuntimeSpec(input), protocol: "MCP" as const }] },
};
},
});

type GetRuntimeTemplateResolverConfig = {
Expand All @@ -262,8 +305,8 @@ export function getRuntimeTemplateResolver(
return { resolve: importBedrockAgentResolver(config.assetSource, config.templateRenderer) };
}

const { framework, language } = input.scaffoldRuntimeInput;
const key = buildResolverKey(framework, language);
const { framework, language, protocol } = input.scaffoldRuntimeInput;
const key = buildResolverKey(framework, language, protocol);

const resolve = getTemplateResolvers(config.assetSource, config.templateRenderer)[key];
if (!resolve) return undefined;
Expand Down
Loading
Loading