Skip to content

Commit c2c646c

Browse files
authored
feat: dev and invoke support for MCP and A2A protocols (#554)
* feat: protocol-aware dev and invoke for MCP and A2A agents Make `agentcore dev` and `agentcore invoke` protocol-aware so MCP and A2A agents can be locally run and tested the same way HTTP agents are today. Dev server: MCP/A2A agents run `python main.py` directly instead of `uvicorn --reload`, since these templates call serve functions inside __main__ rather than exposing a module-level ASGI app. Invoke: A2A uses JSON-RPC message/send to POST /. MCP uses JSON-RPC initialize + tools/list + tools/call to POST /mcp. Both protocols include retry logic for server startup race conditions. TUI: MCP shows available tools on server ready, accepts tool calls as `tool_name {"args"}` in the chat input. A2A uses the same chat UX as HTTP. Protocol and endpoint URL shown in header. CLI: New --tool and --input flags for MCP tool calls via --invoke. Deployed invoke for non-HTTP protocols deferred with clear message. MCP template updated to read PORT from env var for dev server port assignment. * fix: MCP session ID plumbing, tool list in header, A2A venv setup Three fixes from manual testing: 1. MCP "Missing session ID" error: listMcpTools now returns the session ID from the initialize handshake. The TUI and CLI both pass it to subsequent callMcpTool calls. 2. MCP tool list UX: moved tool list from scrollable conversation area into the persistent header so tools are always visible. Conversation area now only shows tool call results. 3. A2A venv setup: non-HTTP protocols checked for the python binary as a proxy for "deps installed", but python always exists once the venv is created. Now checks for uv.lock to verify deps were actually synced. * fix: break circular dependency between invoke and invoke-a2a Extract ServerError, ConnectionError, SSELogger, and InvokeStreamingOptions into invoke-types.ts. Both invoke.ts and invoke-a2a.ts now import from invoke-types instead of each other, breaking the circular import that caused "Cannot access before initialization" in the esbuild bundle. * fix: move isMcp declaration before its first use in DevScreen The isMcp variable was declared in the help text section but used earlier in the height calculation, causing "Cannot access before initialization" at runtime in the esbuild bundle. * fix: always run uv sync for non-HTTP protocols on dev start The uv.lock check wasn't reliable since the lock file can exist from a previous sync before protocol-specific deps were added. Now always runs uv sync when venv exists for MCP/A2A agents. uv sync is fast (~100ms) when deps are already installed. * fix: cap MCP tool list to 5 in header, add input placeholder - Tool list in header shows at most 5 tools with overflow indicator - Typing "list" shows all tools in the scrollable conversation area - Input field shows placeholder "tool_name {"arg": "value"}" for MCP - Fix lint errors in test files and DevScreen * fix: show clear error when only MCP/A2A agents exist in deployed invoke Instead of "No deployed agents found", now shows "Invoke for deployed MCP/A2A agents is not yet supported. Use agentcore dev for local testing." * fix: remove deployed invoke guards for MCP/A2A agents InvokeAgentRuntime supports all protocols. Remove the guards that were blocking MCP and A2A agents from appearing in deployed invoke. * feat: protocol-aware invoke and A2A agent card display - Add fetchA2AAgentCard() to fetch /.well-known/agent.json from A2A agents - DevScreen: show A2A agent card (name, description, skills) in header - InvokeScreen: show protocol in header, include protocol in agent list - useInvokeFlow: pass protocol through to InvokeConfig - Remove all deployed invoke guards for non-HTTP protocols * fix: A2A dev uses fixed port 9000, add messageId to A2A requests - A2A agents always run on port 9000 (serve_a2a default). If port 9000 is in use, show an error instead of silently using another port. - Add required messageId (UUID) to A2A message/send JSON-RPC requests, fixing "Field required" validation error from a2a-sdk. - Revert A2A template PORT changes (not needed, port is fixed). * fix: proper A2A streaming via message/stream, fix response parsing - Use message/stream (SSE) instead of message/send for streaming - Parse artifact-update events for text content as it streams - Parse status-update events for status message text - Handle both kind:'text' and type:'text' part formats - Extract text from full Task results (artifacts + status.message) - Add "Send a message..." placeholder for A2A input in dev and invoke * fix: skip status-update text to prevent doubled A2A responses Status-update events duplicate the same text as artifact-update events. Only yield text from artifact-update events to avoid showing the response twice. * feat: show A2A task status updates (working, input-required, etc.) Display non-terminal status states from status-update SSE events as [state] indicators. Terminal states (completed, canceled) are skipped since artifact content covers them. * fix: stream A2A responses incrementally from status-update events A2A servers stream incremental text via status-update SSE events with message parts, then send a final artifact-update with the complete text. Previously we discarded status-update text and only yielded the artifact-update, causing long responses to appear all at once. Now extractSSEEventText yields text from status-update message parts for real incremental streaming. When status-update text has been streamed, the duplicate artifact-update is skipped. Also adds A2A task status display (Working.../Completed...) in the TUI, container server readiness polling, and lint fixes. * fix: show MCP tool list and usage hint in conversation on startup When MCP tools are fetched on server ready, display them in the conversation area with usage instructions. Also fixes stale closure bug where typing "list" showed old tool data by using a ref for fresh values after async fetch. * fix: guard deployed invoke against unsupported MCP/A2A protocols Show a clear message that deployed invoke for MCP and A2A agents is not yet supported, directing users to "agentcore dev" for local testing. Applies to both the TUI invoke screen and the non-interactive CLI invoke command. * fix: enable deployed invoke for MCP and A2A agents AgentCore Runtime handles protocol translation, so deployed invoke works the same for all protocols via InvokeAgentRuntimeCommand. Remove the incorrect unsupported-protocol guards. * fix: add MCP hint and placeholder in deployed invoke screen Show a hint explaining MCP input format when the conversation is empty, and set protocol-appropriate placeholder text on the input. * feat: MCP tool listing and tool calls in deployed invoke Add MCP support to deployed invoke (both TUI and CLI): - New mcpListTools/mcpCallTool functions send JSON-RPC payloads through InvokeAgentRuntime with mcpSessionId/mcpProtocolVersion - TUI: auto-fetches tools on agent selection, shows tool list hint in conversation, parses "tool_name {args}" input for tool calls - CLI: "agentcore invoke list-tools" lists tools, "agentcore invoke call-tool --tool name --input '{...}'" calls tools * fix: retry MCP calls on cold-start initialization timeouts Deployed runtimes may need >30s to cold-start. Add retry logic (3 attempts with 2s delay) to mcpListTools and mcpCallTool so they survive the "initialization time exceeded" error. * fix: MCP deployment and invoke compatibility - Match MCP template to starter toolkit (mcp.run with streamable-http) - Use bedrock-agentcore-starter-toolkit dependency for deployment - Disable OTEL instrumentation for MCP agents (incompatible with mcp.run) - Fix accept header for MCP SDK calls (application/json, text/event-stream) - Tolerate JSON-RPC errors from stateless MCP initialize - Use fixed port 8000 for MCP local dev server * style: format useInvokeFlow after rebase * fix: protocol backward compat, fixed ports for A2A/MCP in CLI paths - Make protocol field optional in CLI schema (backward compat with existing projects) - Use fixed port 9000 for A2A and 8000 for MCP in --logs and --invoke paths - Add a2a-sdk[all] to Strands A2A template dependencies * feat: A2A deployed invoke via JSON-RPC message/send Deployed A2A agents require JSON-RPC 2.0 message/send format, not the HTTP {"prompt": "..."} payload. Add invokeA2ARuntime() that wraps user text in the proper A2A wire format and parses artifacts from the response. Wire it into both the non-interactive CLI and TUI invoke paths. * fix: address code review findings for protocol dev/invoke PR - Use shared helpers (sleep, isConnectionError, getEndpointUrl, formatMcpToolList, parseJsonRpcResponse) from utils.ts to eliminate duplication across 10+ files - Fix A2A message parts to use `kind: 'text'` per A2A spec (was `type: 'text'`) - Add mcpInitSession for lightweight MCP session init (saves 2 requests vs full listTools) - Fix double tool-list display on MCP "list" command in useInvokeFlow - Wrap fetchAgentCard/fetchMcpTools in useCallback with refs to fix stale closures - Fix ensurePythonVenv for non-HTTP: check python binary instead of running uv sync every time - Replace fake async generator with singleValueStream helper - Fix isConnectionError to use exact match for 'fetch failed' * fix: remove bedrock-agentcore-starter-toolkit from MCP template deps Not needed for MCP to work — only mcp package is required. * fix: address Aidan's review comments on protocol dev/invoke PR 1. Move parseJsonRpcResponse to src/lib/utils/json-rpc.ts to avoid cross-layer dependency (aws/ importing from operations/dev/) 2. parseJsonRpcResponse now throws on unparseable input instead of silently returning {} — prevents silent failures on HTML error pages 3. Wrap MCP/A2A invoke paths in action.ts with try-catch returning { success: false, error } per project convention 4. Add user-friendly error for malformed --input JSON in dev command 5. Add tests for parseA2AResponse (7 cases covering kind/type parts, errors, history fallback, non-JSON) 6. Add tests for shared utils (getEndpointUrl, formatMcpToolList, isConnectionError, sleep) and parseJsonRpcResponse (9 cases)
1 parent 5c8d1b4 commit c2c646c

39 files changed

Lines changed: 2634 additions & 183 deletions

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@aws/agentcore",
3-
"version": "0.3.0-preview.5.1",
3+
"version": "0.3.0-preview.6.0",
44
"description": "CLI for Amazon Bedrock AgentCore",
55
"license": "Apache-2.0",
66
"repository": {

src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1645,7 +1645,8 @@ readme = "README.md"
16451645
requires-python = ">=3.10"
16461646
dependencies = [
16471647
{{#if (eq modelProvider "Anthropic")}}"anthropic >= 0.30.0",
1648-
{{/if}}"aws-opentelemetry-distro",
1648+
{{/if}}"a2a-sdk[all] >= 0.2.0",
1649+
"aws-opentelemetry-distro",
16491650
"bedrock-agentcore[a2a] >= 1.0.3",
16501651
"botocore[crt] >= 1.35.0",
16511652
{{#if (eq modelProvider "Gemini")}}"google-genai >= 1.0.0",
@@ -4076,29 +4077,30 @@ Thumbs.db
40764077
40774078
exports[`Assets Directory Snapshots > Python framework assets > python/python/mcp/standalone/base/main.py should match snapshot 1`] = `
40784079
"from mcp.server.fastmcp import FastMCP
4079-
import uvicorn
40804080
4081-
mcp = FastMCP("{{ name }}")
4081+
mcp = FastMCP("{{ name }}", host="0.0.0.0", stateless_http=True)
40824082
40834083
40844084
@mcp.tool()
40854085
def add_numbers(a: int, b: int) -> int:
4086-
"""Return the sum of two numbers."""
4086+
"""Add two numbers together"""
40874087
return a + b
40884088
40894089
40904090
@mcp.tool()
4091-
def greet(name: str) -> str:
4092-
"""Return a greeting for the given name."""
4093-
return f"Hello, {name}!"
4091+
def multiply_numbers(a: int, b: int) -> int:
4092+
"""Multiply two numbers together"""
4093+
return a * b
4094+
4095+
4096+
@mcp.tool()
4097+
def greet_user(name: str) -> str:
4098+
"""Greet a user by name"""
4099+
return f"Hello, {name}! Nice to meet you."
40944100
40954101
40964102
if __name__ == "__main__":
4097-
uvicorn.run(
4098-
mcp.streamable_http_app(),
4099-
host="0.0.0.0",
4100-
port=8000,
4101-
)
4103+
mcp.run(transport="streamable-http")
41024104
"
41034105
`;
41044106
@@ -4114,10 +4116,7 @@ description = "AgentCore MCP Server"
41144116
readme = "README.md"
41154117
requires-python = ">=3.10"
41164118
dependencies = [
4117-
"aws-opentelemetry-distro",
4118-
"bedrock-agentcore >= 1.0.3",
41194119
"mcp >= 1.19.0",
4120-
"uvicorn >= 0.30.0",
41214120
]
41224121
41234122
[tool.hatch.build.targets.wheel]

src/assets/python/a2a/strands/base/pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ readme = "README.md"
1010
requires-python = ">=3.10"
1111
dependencies = [
1212
{{#if (eq modelProvider "Anthropic")}}"anthropic >= 0.30.0",
13-
{{/if}}"aws-opentelemetry-distro",
13+
{{/if}}"a2a-sdk[all] >= 0.2.0",
14+
"aws-opentelemetry-distro",
1415
"bedrock-agentcore[a2a] >= 1.0.3",
1516
"botocore[crt] >= 1.35.0",
1617
{{#if (eq modelProvider "Gemini")}}"google-genai >= 1.0.0",
Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,25 @@
11
from mcp.server.fastmcp import FastMCP
2-
import uvicorn
32

4-
mcp = FastMCP("{{ name }}")
3+
mcp = FastMCP("{{ name }}", host="0.0.0.0", stateless_http=True)
54

65

76
@mcp.tool()
87
def add_numbers(a: int, b: int) -> int:
9-
"""Return the sum of two numbers."""
8+
"""Add two numbers together"""
109
return a + b
1110

1211

1312
@mcp.tool()
14-
def greet(name: str) -> str:
15-
"""Return a greeting for the given name."""
16-
return f"Hello, {name}!"
13+
def multiply_numbers(a: int, b: int) -> int:
14+
"""Multiply two numbers together"""
15+
return a * b
16+
17+
18+
@mcp.tool()
19+
def greet_user(name: str) -> str:
20+
"""Greet a user by name"""
21+
return f"Hello, {name}! Nice to meet you."
1722

1823

1924
if __name__ == "__main__":
20-
uvicorn.run(
21-
mcp.streamable_http_app(),
22-
host="0.0.0.0",
23-
port=8000,
24-
)
25+
mcp.run(transport="streamable-http")

src/assets/python/mcp/standalone/base/pyproject.toml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,7 @@ description = "AgentCore MCP Server"
99
readme = "README.md"
1010
requires-python = ">=3.10"
1111
dependencies = [
12-
"aws-opentelemetry-distro",
13-
"bedrock-agentcore >= 1.0.3",
1412
"mcp >= 1.19.0",
15-
"uvicorn >= 0.30.0",
1613
]
1714

1815
[tool.hatch.build.targets.wheel]

src/cli/aws/__tests__/agentcore.test.ts

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { extractResult, parseSSE, parseSSELine } from '../agentcore.js';
1+
import { extractResult, parseA2AResponse, parseSSE, parseSSELine } from '../agentcore.js';
22
import { describe, expect, it } from 'vitest';
33

44
describe('parseSSELine', () => {
@@ -97,3 +97,82 @@ describe('extractResult', () => {
9797
expect(extractResult('')).toBe('');
9898
});
9999
});
100+
101+
describe('parseA2AResponse', () => {
102+
it('extracts text from artifacts with kind:text parts', () => {
103+
const response = JSON.stringify({
104+
jsonrpc: '2.0',
105+
id: 1,
106+
result: {
107+
artifacts: [{ parts: [{ kind: 'text', text: 'Hello from A2A' }] }],
108+
},
109+
});
110+
expect(parseA2AResponse(response)).toBe('Hello from A2A');
111+
});
112+
113+
it('extracts text from artifacts with type:text parts (backward compat)', () => {
114+
const response = JSON.stringify({
115+
jsonrpc: '2.0',
116+
id: 1,
117+
result: {
118+
artifacts: [{ parts: [{ type: 'text', text: 'Hello' }] }],
119+
},
120+
});
121+
expect(parseA2AResponse(response)).toBe('Hello');
122+
});
123+
124+
it('concatenates text from multiple parts', () => {
125+
const response = JSON.stringify({
126+
jsonrpc: '2.0',
127+
id: 1,
128+
result: {
129+
artifacts: [
130+
{
131+
parts: [
132+
{ kind: 'text', text: 'part1' },
133+
{ kind: 'text', text: 'part2' },
134+
],
135+
},
136+
],
137+
},
138+
});
139+
expect(parseA2AResponse(response)).toBe('part1part2');
140+
});
141+
142+
it('returns error message for JSON-RPC error', () => {
143+
const response = JSON.stringify({
144+
jsonrpc: '2.0',
145+
id: 1,
146+
error: { code: -32600, message: 'Bad request' },
147+
});
148+
expect(parseA2AResponse(response)).toBe('Error: Bad request');
149+
});
150+
151+
it('falls back to history for agent messages', () => {
152+
const response = JSON.stringify({
153+
jsonrpc: '2.0',
154+
id: 1,
155+
result: {
156+
history: [
157+
{ role: 'user', parts: [{ kind: 'text', text: 'hi' }] },
158+
{ role: 'agent', parts: [{ kind: 'text', text: 'Hello!' }] },
159+
],
160+
},
161+
});
162+
expect(parseA2AResponse(response)).toBe('Hello!');
163+
});
164+
165+
it('returns stringified result when no text parts found', () => {
166+
const response = JSON.stringify({
167+
jsonrpc: '2.0',
168+
id: 1,
169+
result: { id: 'task-1', status: { state: 'completed' } },
170+
});
171+
const parsed = parseA2AResponse(response);
172+
expect(parsed).toContain('task-1');
173+
});
174+
175+
it('returns raw text for non-JSON input', () => {
176+
expect(parseA2AResponse('not json')).toBe('not json');
177+
});
178+
});

0 commit comments

Comments
 (0)