From 4023b713135148270547a583829dfbe4e349895e Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Mon, 31 Aug 2026 22:23:13 +0000 Subject: [PATCH 1/6] feat: add strands-py-mcp FastMCP template assets --- .../strands-mcp-python/Dockerfile.template | 40 +++++++++ .../templates/strands-mcp-python/README.md | 32 +++++++ .../strands-mcp-python/dockerignore.template | 27 ++++++ .../strands-mcp-python/gitignore.template | 41 +++++++++ .../templates/strands-mcp-python/main.py | 83 +++++++++++++++++++ .../strands-mcp-python/pyproject.toml | 17 ++++ 6 files changed, 240 insertions(+) create mode 100644 src/assets/templates/strands-mcp-python/Dockerfile.template create mode 100644 src/assets/templates/strands-mcp-python/README.md create mode 100644 src/assets/templates/strands-mcp-python/dockerignore.template create mode 100644 src/assets/templates/strands-mcp-python/gitignore.template create mode 100644 src/assets/templates/strands-mcp-python/main.py create mode 100644 src/assets/templates/strands-mcp-python/pyproject.toml diff --git a/src/assets/templates/strands-mcp-python/Dockerfile.template b/src/assets/templates/strands-mcp-python/Dockerfile.template new file mode 100644 index 000000000..cb3569eff --- /dev/null +++ b/src/assets/templates/strands-mcp-python/Dockerfile.template @@ -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}} diff --git a/src/assets/templates/strands-mcp-python/README.md b/src/assets/templates/strands-mcp-python/README.md new file mode 100644 index 000000000..af0cf4c46 --- /dev/null +++ b/src/assets/templates/strands-mcp-python/README.md @@ -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`). diff --git a/src/assets/templates/strands-mcp-python/dockerignore.template b/src/assets/templates/strands-mcp-python/dockerignore.template new file mode 100644 index 000000000..a0c4eb658 --- /dev/null +++ b/src/assets/templates/strands-mcp-python/dockerignore.template @@ -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 diff --git a/src/assets/templates/strands-mcp-python/gitignore.template b/src/assets/templates/strands-mcp-python/gitignore.template new file mode 100644 index 000000000..f36f968a0 --- /dev/null +++ b/src/assets/templates/strands-mcp-python/gitignore.template @@ -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 \ No newline at end of file diff --git a/src/assets/templates/strands-mcp-python/main.py b/src/assets/templates/strands-mcp-python/main.py new file mode 100644 index 000000000..813629929 --- /dev/null +++ b/src/assets/templates/strands-mcp-python/main.py @@ -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") diff --git a/src/assets/templates/strands-mcp-python/pyproject.toml b/src/assets/templates/strands-mcp-python/pyproject.toml new file mode 100644 index 000000000..960168bc5 --- /dev/null +++ b/src/assets/templates/strands-mcp-python/pyproject.toml @@ -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 = ["."] From 738ca52de69a396698286a484d78c706320dcaa2 Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Mon, 31 Aug 2026 22:23:14 +0000 Subject: [PATCH 2/6] feat: scaffold MCP runtimes via protocol-keyed template resolution --- src/core/project/templates/runtime.ts | 67 +++++++++++++++++++---- src/handlers/project/add/runtime/index.ts | 2 + src/handlers/project/create/index.ts | 16 +++++- src/handlers/project/create/screen.tsx | 5 ++ src/handlers/project/shortcuts.ts | 13 +++++ src/handlers/project/types.ts | 3 +- 6 files changed, 92 insertions(+), 14 deletions(-) diff --git a/src/core/project/templates/runtime.ts b/src/core/project/templates/runtime.ts index 10ab9cd7d..c07c0025e 100644 --- a/src/core/project/templates/runtime.ts +++ b/src/core/project/templates/runtime.ts @@ -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}` { + return `${framework}/${language}/${protocol ?? "HTTP"}`; } // The IAM policy file the proxy template vends; wired into the runtime's @@ -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( @@ -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] : [], @@ -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"); @@ -244,6 +240,53 @@ const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: Templa }, }; }, + [buildResolverKey("strands", "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/strands-mcp-python" }, + { + 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 = { @@ -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; diff --git a/src/handlers/project/add/runtime/index.ts b/src/handlers/project/add/runtime/index.ts index 892354e52..4a49fcd83 100644 --- a/src/handlers/project/add/runtime/index.ts +++ b/src/handlers/project/add/runtime/index.ts @@ -181,6 +181,7 @@ export const createAddRuntimeHandler = (config: AddProjectResourceConfig) => ? resolveRuntimeTemplateShortcut(flags.template!, { runtimeName: flags.name, build: flags.build, + protocol: flags.protocol, modelProvider: flags["model-provider"], apiKey, memory: flags.memory, @@ -191,6 +192,7 @@ export const createAddRuntimeHandler = (config: AddProjectResourceConfig) => build: flags.build, language: flags.language, framework: flags.framework, + protocol: flags.protocol, modelProvider: flags["model-provider"], apiKey, memory: MEMORY_SHORTCUTS[flags.memory ?? defaultMemory](runtimeName), diff --git a/src/handlers/project/create/index.ts b/src/handlers/project/create/index.ts index 61fb2be66..204c79547 100644 --- a/src/handlers/project/create/index.ts +++ b/src/handlers/project/create/index.ts @@ -22,6 +22,7 @@ import { HarnessSpecSchema, type HarnessModelProvider, } from "../../../projectSchemas/harness"; +import { ProtocolModeSchema } from "../../../projectSchemas/constants"; import { InputValidationError } from "../../../errors"; import { parseJsonFlag } from "../../utils"; import { DEFAULT_HARNESS_MODEL } from "../add/harness"; @@ -47,6 +48,7 @@ const RUNTIME_PATH_FLAGS = [ "build", "language", "framework", + "protocol", "api-key", "runtime-name", "memory", @@ -112,6 +114,7 @@ export const createCreateProjectHandler = (config: CreateProjectHandlerConfig) = "agent framework for the scaffolded runtime code", z.enum(["strands", "none"]).optional(), ), + flag("protocol", "server protocol: HTTP, MCP, A2A, AGUI", ProtocolModeSchema.optional()), flag( "model-provider", "model provider: bedrock, open_ai, gemini, or lite_llm for harnesses; Bedrock for runtime code", @@ -225,7 +228,15 @@ export const createCreateProjectHandler = (config: CreateProjectHandlerConfig) = const isImport = flags["type"] === "import"; const scaffoldingChoiceFlags = ( - ["build", "language", "framework", "model-provider", "api-key", "memory"] as const + [ + "build", + "language", + "framework", + "protocol", + "model-provider", + "api-key", + "memory", + ] as const ).filter((f) => flags[f] !== undefined); if (isImport && (isTemplate || scaffoldingChoiceFlags.length > 0)) { const offending = isTemplate ? "template" : scaffoldingChoiceFlags[0]; @@ -290,6 +301,7 @@ type RuntimePathFlagValues = { build?: "CodeZip" | "Container"; language?: "Python" | "TypeScript"; framework?: "strands" | "none"; + protocol?: z.infer; "model-provider"?: ModelProviderFlag; "api-key"?: string; memory?: (typeof MEMORY_SHORTCUT_NAMES)[number]; @@ -325,6 +337,7 @@ async function resolveScaffoldRuntimeInput( ? resolveRuntimeTemplateShortcut(flags["template"], { runtimeName: flags["runtime-name"], build: flags["build"], + protocol: flags["protocol"], modelProvider, apiKey, memory: flags["memory"], @@ -334,6 +347,7 @@ async function resolveScaffoldRuntimeInput( build: flags["build"], language: flags["language"], framework: flags["framework"], + protocol: flags["protocol"], modelProvider, apiKey, memory: MEMORY_SHORTCUTS[flags["memory"] ?? defaultMemory](runtimeName), diff --git a/src/handlers/project/create/screen.tsx b/src/handlers/project/create/screen.tsx index 8413ea9ab..1271ae677 100644 --- a/src/handlers/project/create/screen.tsx +++ b/src/handlers/project/create/screen.tsx @@ -81,6 +81,11 @@ const TEMPLATE_OPTIONS: { label: "strands-python (recommended)", description: "Strands agent on Bedrock with memory (CodeZip build)", }, + { + template: "strands-py-mcp", + label: "strands-py-mcp", + description: "MCP server exposing tools via FastMCP (CodeZip build)", + }, ]; const MEMORY_OPTIONS: { memory: MemoryShortcutName; label: string; description: string }[] = [ diff --git a/src/handlers/project/shortcuts.ts b/src/handlers/project/shortcuts.ts index f16bfa853..9dc0f340e 100644 --- a/src/handlers/project/shortcuts.ts +++ b/src/handlers/project/shortcuts.ts @@ -85,6 +85,16 @@ export const RUNTIME_TEMPLATE_SHORTCUTS = { memory: "longAndShortTerm", runtimeVersion: "NODE_22", }, + "strands-py-mcp": { + runtimeName: "mcp_server", + build: "CodeZip", + language: "Python", + framework: "strands", + protocol: "MCP", + modelProvider: "Bedrock", + memory: "none", + runtimeVersion: "PYTHON_3_14", + }, } as const satisfies Record; export type RuntimeTemplateShortcutName = keyof typeof RUNTIME_TEMPLATE_SHORTCUTS; @@ -96,6 +106,7 @@ export const RUNTIME_TEMPLATE_SHORTCUT_NAMES = Object.keys( type RuntimeTemplateOverrides = { runtimeName?: string; build?: ScaffoldRuntimeInput["build"]; + protocol?: ScaffoldRuntimeInput["protocol"]; modelProvider?: ScaffoldRuntimeInput["modelProvider"]; apiKey?: string; memory?: MemoryShortcutName; @@ -108,6 +119,7 @@ export function resolveRuntimeTemplateShortcut( const template: RuntimeTemplateShortcut = RUNTIME_TEMPLATE_SHORTCUTS[name]; const runtimeName = overrides?.runtimeName ?? template.runtimeName; const build = overrides?.build ?? template.build; + const protocol = overrides?.protocol ?? template.protocol; const memoryShortcutName = overrides?.memory ?? template.memory; const memory = MEMORY_SHORTCUTS[memoryShortcutName](runtimeName); @@ -116,6 +128,7 @@ export function resolveRuntimeTemplateShortcut( build, language: template.language, framework: template.framework, + protocol, modelProvider: overrides?.modelProvider ?? template.modelProvider, ...(overrides?.apiKey !== undefined && { apiKey: overrides.apiKey }), ...(memory && { memory }), diff --git a/src/handlers/project/types.ts b/src/handlers/project/types.ts index 4e142deb5..8fad8a811 100644 --- a/src/handlers/project/types.ts +++ b/src/handlers/project/types.ts @@ -11,7 +11,7 @@ import z from "zod"; import type { ImportBedrockAgentInput, RuntimeResourceConfig } from "./add/runtime/types"; import type { OnlineEvalConfigSchema } from "../../projectSchemas/online-eval-config"; import { AgentNameSchema, BuildTypeSchema } from "../../projectSchemas/runtime"; -import { RuntimeVersionSchema } from "../../projectSchemas/constants"; +import { ProtocolModeSchema, RuntimeVersionSchema } from "../../projectSchemas/constants"; import type { AgentCoreGateway, AgentCoreGatewayTarget } from "../../projectSchemas/gateway"; import type { PolicyEngineSchema, PolicySchema } from "../../projectSchemas/policy"; import type { AwsDeploymentTarget } from "../../projectSchemas/aws-targets"; @@ -32,6 +32,7 @@ export const ScaffoldRuntimeInputSchema = z build: BuildTypeSchema, language: z.enum(["Python", "TypeScript"]), framework: z.enum(["strands", "none"]), + protocol: ProtocolModeSchema.optional(), modelProvider: z.enum(["Bedrock"]), apiKey: z.string().min(1).optional(), memory: MemorySchema.optional(), From b9a13f8c86d5614cc875845795e37dea5f5aa034 Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Mon, 31 Aug 2026 22:23:14 +0000 Subject: [PATCH 3/6] test: cover strands-py-mcp scaffolding --- .../project/add/runtime/index.test.ts | 60 +++++++++++++++++++ src/handlers/project/project.test.ts | 56 +++++++++++++++++ 2 files changed, 116 insertions(+) diff --git a/src/handlers/project/add/runtime/index.test.ts b/src/handlers/project/add/runtime/index.test.ts index 13a41fc4c..f757e5da4 100644 --- a/src/handlers/project/add/runtime/index.test.ts +++ b/src/handlers/project/add/runtime/index.test.ts @@ -105,6 +105,19 @@ describe("project add runtime", () => { build: "Container", dockerfile: "Dockerfile", }, + "strands-py-mcp template preset": { + build: "CodeZip", + protocol: "MCP", + }, + "strands-py-mcp overrides to Container": { + build: "Container", + dockerfile: "Dockerfile", + protocol: "MCP", + }, + "custom strands MCP runtime": { + build: "CodeZip", + protocol: "MCP", + }, "all infrastructure flags": { description: "Configured runtime", executionRoleArn: "arn:aws:iam::123456789012:role/MyRole", @@ -163,6 +176,30 @@ describe("project add runtime", () => { "strands template overrides to Container", ["--name", "my_agent", "--template", "strands-python", "--build", "Container"], ], + ["strands-py-mcp template preset", ["--name", "my_mcp", "--template", "strands-py-mcp"]], + [ + "strands-py-mcp overrides to Container", + ["--name", "my_mcp", "--template", "strands-py-mcp", "--build", "Container"], + ], + [ + "custom strands MCP runtime", + [ + "--name", + "mcp_custom", + "--build", + "CodeZip", + "--language", + "Python", + "--framework", + "strands", + "--protocol", + "MCP", + "--model-provider", + "Bedrock", + "--memory", + "none", + ], + ], ["custom — all scaffolding flags", ["--name", "my_agent", ...allScaffoldingFlags]], [ "custom — framework strands", @@ -479,6 +516,29 @@ describe("project add runtime", () => { "hello-world-python only supports HTTP", ["--name", "my_agent", "--template", "hello-world-python", "--protocol", "MCP"], ], + [ + "strands-py-mcp does not support memory", + ["--name", "my_agent", "--template", "strands-py-mcp", "--memory", "shortTerm"], + ], + [ + "custom strands MCP runtime does not support memory", + [ + "--name", + "my_agent", + "--build", + "CodeZip", + "--language", + "Python", + "--framework", + "strands", + "--protocol", + "MCP", + "--model-provider", + "Bedrock", + "--memory", + "shortTerm", + ], + ], [ "--memory shortTerm is not supported with --framework none", [ diff --git a/src/handlers/project/project.test.ts b/src/handlers/project/project.test.ts index 328646ccb..aa67aaa59 100644 --- a/src/handlers/project/project.test.ts +++ b/src/handlers/project/project.test.ts @@ -517,6 +517,62 @@ describe("project create", () => { }); }); + test("scaffolds an MCP server from the strands-py-mcp template (CodeZip default)", async () => { + const directory = await inTempDirectory(); + await run([ + "create", + "--name", + "MyProject", + "--template", + "strands-py-mcp", + "--skip-install", + "--skip-git", + ]); + + const projectRoot = join(directory, "MyProject"); + const spec = await Bun.file(join(projectRoot, "agentcore", "agentcore.json")).json(); + expect(spec.runtimes[0]).toMatchObject({ + name: "mcp_server", + build: "CodeZip", + protocol: "MCP", + codeLocation: "app/mcp_server", + runtimeVersion: "PYTHON_3_14", + }); + const runtimeRoot = join(projectRoot, "app", "mcp_server"); + const mainPy = await Bun.file(join(runtimeRoot, "main.py")).text(); + expect(mainPy).toContain("FastMCP"); + expect(mainPy).toContain('mcp.run(transport="streamable-http")'); + expect(await Bun.file(join(runtimeRoot, "Dockerfile")).exists()).toBe(false); + }); + + test("scaffolds a Container MCP server from the strands-py-mcp template", async () => { + const directory = await inTempDirectory(); + await run([ + "create", + "--name", + "MyProject", + "--template", + "strands-py-mcp", + "--build", + "Container", + "--skip-install", + "--skip-git", + ]); + + const projectRoot = join(directory, "MyProject"); + const spec = await Bun.file(join(projectRoot, "agentcore", "agentcore.json")).json(); + expect(spec.runtimes[0]).toMatchObject({ + name: "mcp_server", + build: "Container", + protocol: "MCP", + dockerfile: "Dockerfile", + }); + expect(spec.runtimes[0].runtimeVersion).toBeUndefined(); + expect(await Bun.file(join(projectRoot, "app", "mcp_server", "Dockerfile")).exists()).toBe( + true, + ); + }); + test.each([ ["default", [], ["SEMANTIC", "USER_PREFERENCE", "SUMMARIZATION", "EPISODIC"]], ["none", ["--memory", "none"], []], From 7fa93ab83de75c7274eb047c2dce7a50c31fb705 Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Mon, 31 Aug 2026 23:53:44 +0000 Subject: [PATCH 4/6] refactor: restrict --protocol flag to HTTP and MCP --- src/handlers/project/add/runtime/index.ts | 4 ++-- src/handlers/project/create/index.ts | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/handlers/project/add/runtime/index.ts b/src/handlers/project/add/runtime/index.ts index 4a49fcd83..fbd1a4561 100644 --- a/src/handlers/project/add/runtime/index.ts +++ b/src/handlers/project/add/runtime/index.ts @@ -5,7 +5,7 @@ import { parseJsonFlag, parseTags } from "../../../utils"; import { InputValidationError } from "../../../../errors"; import { type EnvVar, BuildTypeSchema } from "../../../../projectSchemas/runtime"; import { RuntimeAuthorizerTypeSchema } from "../../../../projectSchemas/auth"; -import { NetworkModeSchema, ProtocolModeSchema } from "../../../../projectSchemas/constants"; +import { NetworkModeSchema } from "../../../../projectSchemas/constants"; import { SourceResolver } from "../../../../io"; import { LANGUAGE_VERSION_DEFAULTS, @@ -87,7 +87,7 @@ export const createAddRuntimeHandler = (config: AddProjectResourceConfig) => "additional IAM policy ARNs or policy document paths for the execution role", z.array(z.string()).optional(), ), - flag("protocol", "server protocol: HTTP, MCP, A2A, AGUI", ProtocolModeSchema.optional()), + flag("protocol", "server protocol: HTTP or MCP", z.enum(["HTTP", "MCP"]).optional()), flag( "network-mode", "network mode for the runtime environment (PUBLIC or VPC)", diff --git a/src/handlers/project/create/index.ts b/src/handlers/project/create/index.ts index 204c79547..5c6284f63 100644 --- a/src/handlers/project/create/index.ts +++ b/src/handlers/project/create/index.ts @@ -22,7 +22,6 @@ import { HarnessSpecSchema, type HarnessModelProvider, } from "../../../projectSchemas/harness"; -import { ProtocolModeSchema } from "../../../projectSchemas/constants"; import { InputValidationError } from "../../../errors"; import { parseJsonFlag } from "../../utils"; import { DEFAULT_HARNESS_MODEL } from "../add/harness"; @@ -114,7 +113,7 @@ export const createCreateProjectHandler = (config: CreateProjectHandlerConfig) = "agent framework for the scaffolded runtime code", z.enum(["strands", "none"]).optional(), ), - flag("protocol", "server protocol: HTTP, MCP, A2A, AGUI", ProtocolModeSchema.optional()), + flag("protocol", "server protocol: HTTP or MCP", z.enum(["HTTP", "MCP"]).optional()), flag( "model-provider", "model provider: bedrock, open_ai, gemini, or lite_llm for harnesses; Bedrock for runtime code", @@ -301,7 +300,7 @@ type RuntimePathFlagValues = { build?: "CodeZip" | "Container"; language?: "Python" | "TypeScript"; framework?: "strands" | "none"; - protocol?: z.infer; + protocol?: "HTTP" | "MCP"; "model-provider"?: ModelProviderFlag; "api-key"?: string; memory?: (typeof MEMORY_SHORTCUT_NAMES)[number]; From 7336a5b14c8d8a6cabbed68b8a3c55317e49cbf3 Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Tue, 1 Sep 2026 00:08:24 +0000 Subject: [PATCH 5/6] fix: lock --protocol as a scaffolding flag on create and add --- src/handlers/project/add/runtime/index.ts | 4 ++-- src/handlers/project/create/index.ts | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/handlers/project/add/runtime/index.ts b/src/handlers/project/add/runtime/index.ts index fbd1a4561..b3eabfba0 100644 --- a/src/handlers/project/add/runtime/index.ts +++ b/src/handlers/project/add/runtime/index.ts @@ -130,13 +130,14 @@ export const createAddRuntimeHandler = (config: AddProjectResourceConfig) => "build", "language", "framework", + "protocol", "model-provider", "api-key", "memory", ] as const; const presentScaffoldingFlags = scaffoldingFlags.filter((f) => flags[f] !== undefined); const isTemplate = flags["template"] !== undefined; - const lockedFlag = (["language", "framework"] as const).find( + const lockedFlag = (["language", "framework", "protocol"] as const).find( (flagName) => flags[flagName] !== undefined, ); if (isTemplate && lockedFlag) { @@ -181,7 +182,6 @@ export const createAddRuntimeHandler = (config: AddProjectResourceConfig) => ? resolveRuntimeTemplateShortcut(flags.template!, { runtimeName: flags.name, build: flags.build, - protocol: flags.protocol, modelProvider: flags["model-provider"], apiKey, memory: flags.memory, diff --git a/src/handlers/project/create/index.ts b/src/handlers/project/create/index.ts index 5c6284f63..58902cb2a 100644 --- a/src/handlers/project/create/index.ts +++ b/src/handlers/project/create/index.ts @@ -218,7 +218,7 @@ export const createCreateProjectHandler = (config: CreateProjectHandlerConfig) = ); } - const lockedFlag = (["language", "framework"] as const).find( + const lockedFlag = (["language", "framework", "protocol"] as const).find( (flagName) => flags[flagName] !== undefined, ); if (isTemplate && lockedFlag) { @@ -336,7 +336,6 @@ async function resolveScaffoldRuntimeInput( ? resolveRuntimeTemplateShortcut(flags["template"], { runtimeName: flags["runtime-name"], build: flags["build"], - protocol: flags["protocol"], modelProvider, apiKey, memory: flags["memory"], From c32e7fbe8897577569542d71a0fc6c84e36e5460 Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Tue, 1 Sep 2026 00:08:24 +0000 Subject: [PATCH 6/6] refactor: rename mcp template to py-mcp with framework none --- .../Dockerfile.template | 0 .../README.md | 0 .../dockerignore.template | 0 .../gitignore.template | 0 .../main.py | 0 .../pyproject.toml | 0 src/core/project/templates/runtime.ts | 4 +- .../project/add/runtime/index.test.ts | 69 ++++++++++++++----- src/handlers/project/create/screen.tsx | 4 +- src/handlers/project/project.test.ts | 8 +-- src/handlers/project/shortcuts.ts | 8 +-- 11 files changed, 63 insertions(+), 30 deletions(-) rename src/assets/templates/{strands-mcp-python => python-mcp}/Dockerfile.template (100%) rename src/assets/templates/{strands-mcp-python => python-mcp}/README.md (100%) rename src/assets/templates/{strands-mcp-python => python-mcp}/dockerignore.template (100%) rename src/assets/templates/{strands-mcp-python => python-mcp}/gitignore.template (100%) rename src/assets/templates/{strands-mcp-python => python-mcp}/main.py (100%) rename src/assets/templates/{strands-mcp-python => python-mcp}/pyproject.toml (100%) diff --git a/src/assets/templates/strands-mcp-python/Dockerfile.template b/src/assets/templates/python-mcp/Dockerfile.template similarity index 100% rename from src/assets/templates/strands-mcp-python/Dockerfile.template rename to src/assets/templates/python-mcp/Dockerfile.template diff --git a/src/assets/templates/strands-mcp-python/README.md b/src/assets/templates/python-mcp/README.md similarity index 100% rename from src/assets/templates/strands-mcp-python/README.md rename to src/assets/templates/python-mcp/README.md diff --git a/src/assets/templates/strands-mcp-python/dockerignore.template b/src/assets/templates/python-mcp/dockerignore.template similarity index 100% rename from src/assets/templates/strands-mcp-python/dockerignore.template rename to src/assets/templates/python-mcp/dockerignore.template diff --git a/src/assets/templates/strands-mcp-python/gitignore.template b/src/assets/templates/python-mcp/gitignore.template similarity index 100% rename from src/assets/templates/strands-mcp-python/gitignore.template rename to src/assets/templates/python-mcp/gitignore.template diff --git a/src/assets/templates/strands-mcp-python/main.py b/src/assets/templates/python-mcp/main.py similarity index 100% rename from src/assets/templates/strands-mcp-python/main.py rename to src/assets/templates/python-mcp/main.py diff --git a/src/assets/templates/strands-mcp-python/pyproject.toml b/src/assets/templates/python-mcp/pyproject.toml similarity index 100% rename from src/assets/templates/strands-mcp-python/pyproject.toml rename to src/assets/templates/python-mcp/pyproject.toml diff --git a/src/core/project/templates/runtime.ts b/src/core/project/templates/runtime.ts index c07c0025e..d5ec1d325 100644 --- a/src/core/project/templates/runtime.ts +++ b/src/core/project/templates/runtime.ts @@ -240,7 +240,7 @@ const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: Templa }, }; }, - [buildResolverKey("strands", "Python", "MCP")]: async (input: RuntimeResourceConfig) => { + [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 ?? []; @@ -272,7 +272,7 @@ const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: Templa const isContainer = input.scaffoldRuntimeInput.build === "Container"; const tree = await FsTreeNode.fromAssetSource( { assetSource }, - { assetDir: "templates/strands-mcp-python" }, + { assetDir: "templates/python-mcp" }, { rootDirName: input.name, transformContent: (raw) => templateRenderer.render(raw, context), diff --git a/src/handlers/project/add/runtime/index.test.ts b/src/handlers/project/add/runtime/index.test.ts index f757e5da4..b6995dd31 100644 --- a/src/handlers/project/add/runtime/index.test.ts +++ b/src/handlers/project/add/runtime/index.test.ts @@ -71,8 +71,6 @@ describe("project add runtime", () => { "arn:aws:iam::123456789012:role/MyRole", "--additional-policies", "arn:aws:iam::123456789012:policy/MyPolicy", - "--protocol", - "HTTP", "--network-mode", "VPC", "--network-config", @@ -105,16 +103,16 @@ describe("project add runtime", () => { build: "Container", dockerfile: "Dockerfile", }, - "strands-py-mcp template preset": { + "py-mcp template preset": { build: "CodeZip", protocol: "MCP", }, - "strands-py-mcp overrides to Container": { + "py-mcp overrides to Container": { build: "Container", dockerfile: "Dockerfile", protocol: "MCP", }, - "custom strands MCP runtime": { + "custom MCP runtime": { build: "CodeZip", protocol: "MCP", }, @@ -122,7 +120,6 @@ describe("project add runtime", () => { description: "Configured runtime", executionRoleArn: "arn:aws:iam::123456789012:role/MyRole", additionalPolicies: ["arn:aws:iam::123456789012:policy/MyPolicy"], - protocol: "HTTP", networkMode: "VPC", networkConfig: { subnets: ["subnet-0123456789abcdef0"], @@ -176,13 +173,43 @@ describe("project add runtime", () => { "strands template overrides to Container", ["--name", "my_agent", "--template", "strands-python", "--build", "Container"], ], - ["strands-py-mcp template preset", ["--name", "my_mcp", "--template", "strands-py-mcp"]], + ["py-mcp template preset", ["--name", "my_mcp", "--template", "py-mcp"]], + [ + "py-mcp overrides to Container", + ["--name", "my_mcp", "--template", "py-mcp", "--build", "Container"], + ], [ - "strands-py-mcp overrides to Container", - ["--name", "my_mcp", "--template", "strands-py-mcp", "--build", "Container"], + "strands-python with session, EFS, and S3 mounts", + [ + "--name", + "fs_agent", + "--template", + "strands-python", + "--network-mode", + "VPC", + "--network-config", + '{"subnets":["subnet-0123456789abcdef0"],"securityGroups":["sg-0123456789abcdef0"]}', + "--filesystem-configurations", + '[{"sessionStorage":{"mountPath":"/mnt/session"}},{"efsAccessPoint":{"accessPointArn":"arn:aws:elasticfilesystem:us-east-1:123456789012:access-point/fsap-0123456789abcdef0","mountPath":"/mnt/efs"}},{"s3FilesAccessPoint":{"accessPointArn":"arn:aws:s3files:us-east-1:123456789012:file-system/fs-0123456789abcdef01/access-point/fsap-0123456789abcdef1","mountPath":"/mnt/s3"}}]', + ], ], [ - "custom strands MCP runtime", + "py-mcp with session, EFS, and S3 mounts", + [ + "--name", + "fs_mcp", + "--template", + "py-mcp", + "--network-mode", + "VPC", + "--network-config", + '{"subnets":["subnet-0123456789abcdef0"],"securityGroups":["sg-0123456789abcdef0"]}', + "--filesystem-configurations", + '[{"sessionStorage":{"mountPath":"/mnt/session"}},{"efsAccessPoint":{"accessPointArn":"arn:aws:elasticfilesystem:us-east-1:123456789012:access-point/fsap-0123456789abcdef0","mountPath":"/mnt/efs"}},{"s3FilesAccessPoint":{"accessPointArn":"arn:aws:s3files:us-east-1:123456789012:file-system/fs-0123456789abcdef01/access-point/fsap-0123456789abcdef1","mountPath":"/mnt/s3"}}]', + ], + ], + [ + "custom MCP runtime", [ "--name", "mcp_custom", @@ -191,7 +218,7 @@ describe("project add runtime", () => { "--language", "Python", "--framework", - "strands", + "none", "--protocol", "MCP", "--model-provider", @@ -473,7 +500,7 @@ describe("project add runtime", () => { ], ], [ - "strands-python only supports HTTP", + "--protocol cannot override the strands-python template", ["--name", "my_agent", "--template", "strands-python", "--protocol", "MCP"], ], [ @@ -513,15 +540,23 @@ describe("project add runtime", () => { ["--name", "my_agent", ...template, "--network-config", "{bad}"], ], [ - "hello-world-python only supports HTTP", + "--protocol cannot override the hello-world-python template", ["--name", "my_agent", "--template", "hello-world-python", "--protocol", "MCP"], ], [ - "strands-py-mcp does not support memory", - ["--name", "my_agent", "--template", "strands-py-mcp", "--memory", "shortTerm"], + "py-mcp does not support memory", + ["--name", "my_agent", "--template", "py-mcp", "--memory", "shortTerm"], + ], + [ + "--protocol alone requires --framework and --language", + ["--name", "my_agent", "--protocol", "MCP"], ], [ - "custom strands MCP runtime does not support memory", + "--protocol cannot override a template", + ["--name", "my_agent", "--template", "py-mcp", "--protocol", "MCP"], + ], + [ + "custom MCP runtime does not support memory", [ "--name", "my_agent", @@ -530,7 +565,7 @@ describe("project add runtime", () => { "--language", "Python", "--framework", - "strands", + "none", "--protocol", "MCP", "--model-provider", diff --git a/src/handlers/project/create/screen.tsx b/src/handlers/project/create/screen.tsx index 1271ae677..b51b3ec1c 100644 --- a/src/handlers/project/create/screen.tsx +++ b/src/handlers/project/create/screen.tsx @@ -82,8 +82,8 @@ const TEMPLATE_OPTIONS: { description: "Strands agent on Bedrock with memory (CodeZip build)", }, { - template: "strands-py-mcp", - label: "strands-py-mcp", + template: "py-mcp", + label: "py-mcp", description: "MCP server exposing tools via FastMCP (CodeZip build)", }, ]; diff --git a/src/handlers/project/project.test.ts b/src/handlers/project/project.test.ts index aa67aaa59..a80a062d5 100644 --- a/src/handlers/project/project.test.ts +++ b/src/handlers/project/project.test.ts @@ -517,14 +517,14 @@ describe("project create", () => { }); }); - test("scaffolds an MCP server from the strands-py-mcp template (CodeZip default)", async () => { + test("scaffolds an MCP server from the py-mcp template (CodeZip default)", async () => { const directory = await inTempDirectory(); await run([ "create", "--name", "MyProject", "--template", - "strands-py-mcp", + "py-mcp", "--skip-install", "--skip-git", ]); @@ -545,14 +545,14 @@ describe("project create", () => { expect(await Bun.file(join(runtimeRoot, "Dockerfile")).exists()).toBe(false); }); - test("scaffolds a Container MCP server from the strands-py-mcp template", async () => { + test("scaffolds a Container MCP server from the py-mcp template", async () => { const directory = await inTempDirectory(); await run([ "create", "--name", "MyProject", "--template", - "strands-py-mcp", + "py-mcp", "--build", "Container", "--skip-install", diff --git a/src/handlers/project/shortcuts.ts b/src/handlers/project/shortcuts.ts index 9dc0f340e..30c6bf44e 100644 --- a/src/handlers/project/shortcuts.ts +++ b/src/handlers/project/shortcuts.ts @@ -85,11 +85,11 @@ export const RUNTIME_TEMPLATE_SHORTCUTS = { memory: "longAndShortTerm", runtimeVersion: "NODE_22", }, - "strands-py-mcp": { + "py-mcp": { runtimeName: "mcp_server", build: "CodeZip", language: "Python", - framework: "strands", + framework: "none", protocol: "MCP", modelProvider: "Bedrock", memory: "none", @@ -106,7 +106,6 @@ export const RUNTIME_TEMPLATE_SHORTCUT_NAMES = Object.keys( type RuntimeTemplateOverrides = { runtimeName?: string; build?: ScaffoldRuntimeInput["build"]; - protocol?: ScaffoldRuntimeInput["protocol"]; modelProvider?: ScaffoldRuntimeInput["modelProvider"]; apiKey?: string; memory?: MemoryShortcutName; @@ -119,7 +118,6 @@ export function resolveRuntimeTemplateShortcut( const template: RuntimeTemplateShortcut = RUNTIME_TEMPLATE_SHORTCUTS[name]; const runtimeName = overrides?.runtimeName ?? template.runtimeName; const build = overrides?.build ?? template.build; - const protocol = overrides?.protocol ?? template.protocol; const memoryShortcutName = overrides?.memory ?? template.memory; const memory = MEMORY_SHORTCUTS[memoryShortcutName](runtimeName); @@ -128,7 +126,7 @@ export function resolveRuntimeTemplateShortcut( build, language: template.language, framework: template.framework, - protocol, + protocol: template.protocol, modelProvider: overrides?.modelProvider ?? template.modelProvider, ...(overrides?.apiKey !== undefined && { apiKey: overrides.apiKey }), ...(memory && { memory }),