Skip to content

Commit 3aaa062

Browse files
authored
feat: add protocol mode support (HTTP, MCP, A2A) (aws#550)
* feat: add protocol mode support (HTTP, MCP, A2A, AGUI) Add the protocol dimension to all agent creation flows: non-interactive CLI, TUI wizard, and BYO agent. Each protocol mode routes to the correct template directory under a new protocol-first layout (python/{protocol}/{framework}/base/). Key changes: - Schema: ProtocolModeSchema, PROTOCOL_FRAMEWORK_MATRIX, helpers - Templates: restructure under http/, add mcp/, a2a/, agui/ dirs - Rendering: BaseRenderer uses protocol in template path, new McpRenderer - CLI: --protocol flag on add agent and create commands with validation - TUI: protocol step in wizard, conditional framework/model filtering - Schema mapper: protocolMode passthrough, MCP skips model/memory/creds - Tests: unit tests for schema, validation, rendering, and mapping * test: add integration tests for protocol mode creation flows Add create-protocols.test.ts covering MCP, A2A, AGUI, and HTTP create and add-agent flows. Fix MCP create defaulting undefined model provider by falling back to Bedrock (no credential needed). Add protocol option to createTestProject helper. * fix: always set explicit protocolMode (including HTTP) Previously HTTP agents omitted protocolMode for backwards compatibility. Now all agents have an explicit protocolMode field set to HTTP, MCP, A2A, or AGUI. This makes the config self-documenting and consistent. * fix: update A2A and AGUI templates to use bedrock-agentcore-sdk APIs Align templates with bedrock-agentcore-sdk-python PRs aws#349 and aws#350: A2A templates now use serve_a2a() from bedrock_agentcore.runtime: - Strands: StrandsA2AExecutor from strands.multiagent.a2a.executor - GoogleADK: A2aAgentExecutor with Runner + AgentCard from a2a-sdk - LangGraph: custom AgentExecutor subclass with AgentCard from a2a-sdk - Dependencies: bedrock-agentcore[a2a], removed uvicorn/langgraph-a2a AGUI templates now use serve_ag_ui() and AGUIApp from runtime: - Strands: StrandsAgent from ag_ui_strands + serve_ag_ui() - GoogleADK: AGUIApp decorator with Runner, yields AG-UI events - LangGraph: AGUIApp decorator with graph.ainvoke, yields AG-UI events - Dependencies: bedrock-agentcore[ag-ui], removed uvicorn/fake adapters * fix: rename protocolMode to protocol, remove AGUI, fix TUI crash - Rename `protocolMode` field to `protocol` in AgentEnvSpec schema and all downstream types (GenerateConfig, AddAgentConfig, AgentRenderConfig) - Remove AGUI protocol support (CDK L1 does not support it yet): delete all agui templates, remove from enums/matrix/tests/options - Fix TUI crash in Add Agent -> New Agent flow: ADD_AGENT_STEP_LABELS was missing the 'protocol' step, causing StepIndicator to crash on undefined.length - Pass protocol selection through AddAgentConfig so it isn't silently dropped when creating agents via the TUI add-agent flow - Fix type error in create/validate.ts: capture validated ProtocolMode from safeParse instead of using untyped string * Fix lint and format errors from CI - Remove unnecessary type assertion on cliOptions.protocol - Remove unused SDK_OPTIONS import from GenerateWizardUI - Remove unused getSDKOptionsForProtocol import from useGenerateWizard - Prefix unused destructured variable with underscore in test - Run prettier on 4 files that failed format check * Fix integration test: rename protocolMode to protocol, remove AGUI test The integration test still referenced the old `protocolMode` field name instead of `protocol`, and included an AGUI test case that was removed from the schema. * Fix review findings: missing __init__.py, build validation order - Add model/__init__.py to all A2A template directories (strands, googleadk, langchain_langgraph) matching HTTP template structure - Move build type validation before MCP early return in create/validate so invalid --build values are caught for MCP agents too - Add clarifying comment for phantom framework/modelProvider defaults in MCP path of create/command.tsx * Address review feedback: McpRenderer, phantom defaults, required protocol - Add shouldRenderMemory() hook to BaseRenderer, override in McpRenderer to return false instead of reimplementing entire render() method - Make framework/modelProvider optional in CreateWithAgentOptions, resolve defaults explicitly in createProjectWithAgent for MCP path - Make protocol field required in AgentEnvSpecSchema (no backwards compat needed per reviewer) - Update tests for required protocol field * Add required protocol field to all AgentEnvSpec test fixtures Making protocol required on AgentEnvSpecSchema means every test fixture that constructs an AgentCoreRuntime object needs protocol: 'HTTP'. * Remove accidentally committed ProtocolTesting directory Local test directory was inadvertently staged during merge commit. Added to .gitignore to prevent recurrence.
1 parent a61ebdd commit 3aaa062

116 files changed

Lines changed: 2846 additions & 172 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,4 @@ workdir-tmp/
6060
bun.lock
6161

6262
.agentreview
63+
ProtocolTesting/
Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
/* eslint-disable security/detect-non-literal-fs-filename */
2+
import { exists, prereqs, readProjectConfig, runCLI } from '../src/test-utils/index.js';
3+
import { randomUUID } from 'node:crypto';
4+
import { mkdir, readFile, rm } from 'node:fs/promises';
5+
import { tmpdir } from 'node:os';
6+
import { join } from 'node:path';
7+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
8+
9+
describe.skipIf(!prereqs.npm || !prereqs.git)('integration: create with protocol modes', () => {
10+
let testDir: string;
11+
12+
beforeAll(async () => {
13+
testDir = join(tmpdir(), `agentcore-integ-protocols-${randomUUID()}`);
14+
await mkdir(testDir, { recursive: true });
15+
});
16+
17+
afterAll(async () => {
18+
await rm(testDir, { recursive: true, force: true });
19+
});
20+
21+
it('creates MCP standalone project (no framework, no model provider)', async () => {
22+
const name = `Mcp${Date.now().toString().slice(-6)}`;
23+
const result = await runCLI(
24+
['create', '--name', name, '--language', 'Python', '--protocol', 'MCP', '--json'],
25+
testDir
26+
);
27+
28+
expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0);
29+
const json = JSON.parse(result.stdout);
30+
expect(json.success).toBe(true);
31+
32+
const agentName = json.agentName || name;
33+
const agentDir = join(json.projectPath, 'app', agentName);
34+
35+
expect(await exists(agentDir), 'Agent directory should exist').toBe(true);
36+
expect(await exists(join(agentDir, 'main.py')), 'main.py should exist').toBe(true);
37+
expect(await exists(join(agentDir, 'pyproject.toml')), 'pyproject.toml should exist').toBe(true);
38+
39+
// Verify pyproject.toml references mcp (FastMCP)
40+
const pyproject = await readFile(join(agentDir, 'pyproject.toml'), 'utf-8');
41+
expect(pyproject.toLowerCase().includes('mcp'), 'pyproject.toml should reference mcp').toBe(true);
42+
43+
// Verify config has protocol set to MCP
44+
const config = await readProjectConfig(json.projectPath);
45+
const agents = config.agents as Record<string, unknown>[];
46+
expect(agents).toBeDefined();
47+
expect(agents.length).toBe(1);
48+
expect(agents[0]!.name).toBe(agentName);
49+
expect(agents[0]!.protocol).toBe('MCP');
50+
51+
// MCP agents should have no credentials
52+
const credentials = (config.credentials as Record<string, unknown>[]) ?? [];
53+
expect(credentials.length).toBe(0);
54+
});
55+
56+
it('creates A2A project with Strands framework', async () => {
57+
const name = `A2a${Date.now().toString().slice(-6)}`;
58+
const result = await runCLI(
59+
[
60+
'create',
61+
'--name',
62+
name,
63+
'--language',
64+
'Python',
65+
'--protocol',
66+
'A2A',
67+
'--framework',
68+
'Strands',
69+
'--model-provider',
70+
'Bedrock',
71+
'--memory',
72+
'none',
73+
'--json',
74+
],
75+
testDir
76+
);
77+
78+
expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0);
79+
const json = JSON.parse(result.stdout);
80+
expect(json.success).toBe(true);
81+
82+
const agentName = json.agentName || name;
83+
const agentDir = join(json.projectPath, 'app', agentName);
84+
85+
expect(await exists(agentDir), 'Agent directory should exist').toBe(true);
86+
expect(await exists(join(agentDir, 'main.py')), 'main.py should exist').toBe(true);
87+
88+
// Verify config has protocol set to A2A
89+
const config = await readProjectConfig(json.projectPath);
90+
const agents = config.agents as Record<string, unknown>[];
91+
expect(agents.length).toBe(1);
92+
expect(agents[0]!.protocol).toBe('A2A');
93+
});
94+
95+
it('creates HTTP project with explicit protocol HTTP', async () => {
96+
const name = `Http${Date.now().toString().slice(-6)}`;
97+
const result = await runCLI(
98+
[
99+
'create',
100+
'--name',
101+
name,
102+
'--language',
103+
'Python',
104+
'--framework',
105+
'Strands',
106+
'--model-provider',
107+
'Bedrock',
108+
'--memory',
109+
'none',
110+
'--json',
111+
],
112+
testDir
113+
);
114+
115+
expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0);
116+
const json = JSON.parse(result.stdout);
117+
expect(json.success).toBe(true);
118+
119+
// Verify config has explicit protocol: HTTP
120+
const config = await readProjectConfig(json.projectPath);
121+
const agents = config.agents as Record<string, unknown>[];
122+
expect(agents.length).toBe(1);
123+
expect(agents[0]!.protocol).toBe('HTTP');
124+
});
125+
126+
it('rejects invalid protocol', async () => {
127+
const name = `Bad${Date.now().toString().slice(-6)}`;
128+
const result = await runCLI(
129+
['create', '--name', name, '--language', 'Python', '--protocol', 'INVALID', '--json'],
130+
testDir
131+
);
132+
133+
expect(result.exitCode).not.toBe(0);
134+
});
135+
136+
it('rejects MCP with --framework flag', async () => {
137+
const name = `McpFw${Date.now().toString().slice(-6)}`;
138+
const result = await runCLI(
139+
['create', '--name', name, '--language', 'Python', '--protocol', 'MCP', '--framework', 'Strands', '--json'],
140+
testDir
141+
);
142+
143+
expect(result.exitCode).not.toBe(0);
144+
});
145+
146+
it('rejects A2A with unsupported framework (CrewAI)', async () => {
147+
const name = `A2aCrew${Date.now().toString().slice(-6)}`;
148+
const result = await runCLI(
149+
[
150+
'create',
151+
'--name',
152+
name,
153+
'--language',
154+
'Python',
155+
'--protocol',
156+
'A2A',
157+
'--framework',
158+
'CrewAI',
159+
'--model-provider',
160+
'Bedrock',
161+
'--memory',
162+
'none',
163+
'--json',
164+
],
165+
testDir
166+
);
167+
168+
expect(result.exitCode).not.toBe(0);
169+
});
170+
});
171+
172+
describe.skipIf(!prereqs.npm || !prereqs.git)('integration: add agent with protocol modes', () => {
173+
let testDir: string;
174+
let projectPath: string;
175+
176+
beforeAll(async () => {
177+
testDir = join(tmpdir(), `agentcore-integ-add-protocol-${randomUUID()}`);
178+
await mkdir(testDir, { recursive: true });
179+
180+
// Create a base project first (HTTP, no agent)
181+
const result = await runCLI(['create', '--name', 'ProtoTest', '--no-agent', '--json'], testDir);
182+
expect(result.exitCode, `setup stderr: ${result.stderr}`).toBe(0);
183+
const json = JSON.parse(result.stdout);
184+
projectPath = json.projectPath;
185+
});
186+
187+
afterAll(async () => {
188+
await rm(testDir, { recursive: true, force: true });
189+
});
190+
191+
it('adds MCP agent to existing project', async () => {
192+
const name = `McpAgent${Date.now().toString().slice(-6)}`;
193+
const result = await runCLI(
194+
['add', 'agent', '--name', name, '--protocol', 'MCP', '--language', 'Python', '--json'],
195+
projectPath
196+
);
197+
198+
expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0);
199+
const json = JSON.parse(result.stdout);
200+
expect(json.success).toBe(true);
201+
202+
const config = await readProjectConfig(projectPath);
203+
const agents = config.agents as Record<string, unknown>[];
204+
const mcpAgent = agents.find(a => a.name === name);
205+
expect(mcpAgent).toBeDefined();
206+
expect(mcpAgent!.protocol).toBe('MCP');
207+
});
208+
209+
it('adds A2A agent to existing project', async () => {
210+
const name = `A2aAgent${Date.now().toString().slice(-6)}`;
211+
const result = await runCLI(
212+
[
213+
'add',
214+
'agent',
215+
'--name',
216+
name,
217+
'--protocol',
218+
'A2A',
219+
'--framework',
220+
'Strands',
221+
'--model-provider',
222+
'Bedrock',
223+
'--memory',
224+
'none',
225+
'--language',
226+
'Python',
227+
'--json',
228+
],
229+
projectPath
230+
);
231+
232+
expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0);
233+
const json = JSON.parse(result.stdout);
234+
expect(json.success).toBe(true);
235+
236+
const config = await readProjectConfig(projectPath);
237+
const agents = config.agents as Record<string, unknown>[];
238+
const a2aAgent = agents.find(a => a.name === name);
239+
expect(a2aAgent).toBeDefined();
240+
expect(a2aAgent!.protocol).toBe('A2A');
241+
});
242+
243+
it('adds BYO agent with MCP protocol', async () => {
244+
const name = `ByoMcp${Date.now().toString().slice(-6)}`;
245+
const result = await runCLI(
246+
[
247+
'add',
248+
'agent',
249+
'--name',
250+
name,
251+
'--type',
252+
'byo',
253+
'--protocol',
254+
'MCP',
255+
'--language',
256+
'Python',
257+
'--code-location',
258+
`app/${name}/`,
259+
'--json',
260+
],
261+
projectPath
262+
);
263+
264+
expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0);
265+
const json = JSON.parse(result.stdout);
266+
expect(json.success).toBe(true);
267+
268+
const config = await readProjectConfig(projectPath);
269+
const agents = config.agents as Record<string, unknown>[];
270+
const byoAgent = agents.find(a => a.name === name);
271+
expect(byoAgent).toBeDefined();
272+
expect(byoAgent!.protocol).toBe('MCP');
273+
});
274+
});

0 commit comments

Comments
 (0)