Skip to content

Commit 180c62b

Browse files
committed
feat: Major fixes for Bedrock AgentCore Gateway support
- Expose BedrockAgentCoreGatewayTargetHandler in top-level mcp_lambda - Fix both Py and TS method names - Fix TS content lookup for tool name - Translate gateway tool name to MCP server tool name
1 parent 8ac8b42 commit 180c62b

File tree

6 files changed

+146
-29
lines changed

6 files changed

+146
-29
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -321,9 +321,9 @@ const requestHandler = new BedrockAgentCoreGatewayTargetHandler(
321321
);
322322

323323
export const handler: Handler = async (
324-
event: event: Record<string, unknown>,
324+
event: Record<string, unknown>,
325325
context: Context
326-
): Promise<event: Record<string, unknown>> => {
326+
): Promise<Record<string, unknown>> => {
327327
return requestHandler.handle(event, context);
328328
};
329329
```

src/python/src/mcp_lambda/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from .handlers import (
33
APIGatewayProxyEventHandler,
44
APIGatewayProxyEventV2Handler,
5+
BedrockAgentCoreGatewayTargetHandler,
56
LambdaFunctionURLEventHandler,
67
RequestHandler,
78
)
@@ -18,5 +19,6 @@
1819
"RequestHandler",
1920
"APIGatewayProxyEventHandler",
2021
"APIGatewayProxyEventV2Handler",
22+
"BedrockAgentCoreGatewayTargetHandler",
2123
"LambdaFunctionURLEventHandler",
2224
]

src/python/src/mcp_lambda/handlers/bedrock_agent_core_gateway_target_handler.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,24 @@ class BedrockAgentCoreGatewayTargetHandler:
1818
def __init__(self, request_handler: RequestHandler):
1919
self.request_handler = request_handler
2020

21-
def handle_event(self, event: Dict[str, Any], context: LambdaContext) -> Any:
21+
def handle(self, event: Dict[str, Any], context: LambdaContext) -> Any:
2222
"""Handle Lambda invocation from Bedrock AgentCore Gateway"""
2323
# Extract tool metadata from context
24-
tool_name = None
25-
if context.client_context and hasattr(context.client_context, "custom"):
26-
tool_name = context.client_context.custom.get("bedrockagentcoreToolName")
24+
gateway_tool_name = None
2725

28-
if not tool_name:
29-
raise ValueError("Missing bedrockagentcoreToolName in context")
26+
if context.client_context and hasattr(context.client_context, "custom"):
27+
gateway_tool_name = context.client_context.custom.get(
28+
"bedrockAgentCoreToolName"
29+
)
30+
31+
if not gateway_tool_name:
32+
raise ValueError("Missing bedrockAgentCoreToolName in context")
33+
34+
# Gateway names the tools like <target name>___<tool name>
35+
parts = gateway_tool_name.split("___", 1)
36+
if len(parts) != 2:
37+
raise ValueError(f"Invalid tool name format: {gateway_tool_name}")
38+
tool_name = parts[1]
3039

3140
# Create JSON-RPC request from gateway event
3241
jsonrpc_request = JSONRPCRequest(

src/python/tests/test_handlers.py

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -527,13 +527,13 @@ def test_handle_valid_tool_invocation(self):
527527

528528
handler = BedrockAgentCoreGatewayTargetHandler(mock_handler)
529529

530-
# Mock context with tool name
530+
# Mock context with gateway tool name
531531
context = Mock(spec=LambdaContext)
532532
context.client_context = Mock()
533-
context.client_context.custom = {"bedrockagentcoreToolName": "test_tool"}
533+
context.client_context.custom = {"bedrockAgentCoreToolName": "target___test_tool"}
534534

535535
event = {"param1": "value1", "param2": "value2"}
536-
result = handler.handle_event(event, context)
536+
result = handler.handle(event, context)
537537

538538
assert result == {"message": "Tool executed successfully"}
539539

@@ -556,9 +556,52 @@ def test_missing_tool_name_raises_error(self):
556556
event = {"param1": "value1"}
557557

558558
with pytest.raises(
559-
ValueError, match="Missing bedrockagentcoreToolName in context"
559+
ValueError, match="Missing bedrockAgentCoreToolName in context"
560560
):
561-
handler.handle_event(event, context)
561+
handler.handle(event, context)
562+
563+
def test_invalid_tool_name_format_raises_error(self):
564+
"""Test that invalid tool name format raises ValueError."""
565+
handler = BedrockAgentCoreGatewayTargetHandler(Mock(spec=RequestHandler))
566+
567+
# Mock context with invalid tool name format
568+
context = Mock(spec=LambdaContext)
569+
context.client_context = Mock()
570+
context.client_context.custom = {"bedrockAgentCoreToolName": "invalid_format"}
571+
572+
event = {"param1": "value1"}
573+
574+
with pytest.raises(
575+
ValueError, match="Invalid tool name format: invalid_format"
576+
):
577+
handler.handle(event, context)
578+
579+
def test_multiple_delimiters_in_tool_name(self):
580+
"""Test that tool name with multiple delimiters works correctly."""
581+
# Create a mock request handler that handles tools/call
582+
mock_handler = Mock(spec=RequestHandler)
583+
mock_handler.handle_request.return_value = JSONRPCResponse(
584+
jsonrpc="2.0",
585+
result={"message": "Tool executed successfully"},
586+
id=1,
587+
)
588+
589+
handler = BedrockAgentCoreGatewayTargetHandler(mock_handler)
590+
591+
# Mock context with gateway tool name containing multiple delimiters
592+
context = Mock(spec=LambdaContext)
593+
context.client_context = Mock()
594+
context.client_context.custom = {"bedrockAgentCoreToolName": "target___test___tool"}
595+
596+
event = {"param1": "value1"}
597+
result = handler.handle(event, context)
598+
599+
assert result == {"message": "Tool executed successfully"}
600+
601+
# Verify the extracted tool name is everything after the first delimiter
602+
call_args = mock_handler.handle_request.call_args[0]
603+
request = call_args[0]
604+
assert request.params["name"] == "test___tool"
562605

563606
def test_request_handler_error_raises_exception(self):
564607
"""Test that request handler errors are raised as exceptions."""
@@ -572,12 +615,12 @@ def test_request_handler_error_raises_exception(self):
572615

573616
handler = BedrockAgentCoreGatewayTargetHandler(mock_handler)
574617

575-
# Mock context with tool name
618+
# Mock context with gateway tool name
576619
context = Mock(spec=LambdaContext)
577620
context.client_context = Mock()
578-
context.client_context.custom = {"bedrockagentcoreToolName": "unknown_tool"}
621+
context.client_context.custom = {"bedrockAgentCoreToolName": "target___unknown_tool"}
579622

580623
event = {"param1": "value1"}
581624

582625
with pytest.raises(Exception, match="Tool not found"):
583-
handler.handle_event(event, context)
626+
handler.handle(event, context)

src/typescript/src/handlers/bedrockAgentCoreGatewayTargetHandler.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,29 @@ export class BedrockAgentCoreGatewayTargetHandler {
1818
/**
1919
* Handle Lambda invocation from Bedrock AgentCore Gateway
2020
*/
21-
async handleEvent(
21+
async handle(
2222
event: Record<string, unknown>,
2323
context: Context
2424
): Promise<unknown> {
2525
// Extract tool metadata from context
26-
const toolName =
27-
context.clientContext?.Custom?.["bedrockagentcoreToolName"];
26+
const clientContext = context.clientContext as unknown as Record<string, unknown> | undefined;
27+
const custom = clientContext?.["custom"] as Record<string, unknown> | undefined;
28+
const gatewayToolName = custom?.["bedrockAgentCoreToolName"] as string | undefined;
2829

29-
if (!toolName) {
30-
throw new Error("Missing bedrockagentcoreToolName in context");
30+
if (!gatewayToolName) {
31+
throw new Error("Missing bedrockAgentCoreToolName in context");
3132
}
3233

34+
// Gateway names the tools like <target name>___<tool name>
35+
const delimiter = "___";
36+
const delimiterIndex = gatewayToolName.indexOf(delimiter);
37+
if (delimiterIndex === -1) {
38+
throw new Error(`Invalid gateway tool name format: ${gatewayToolName}`);
39+
}
40+
const toolName = gatewayToolName.substring(
41+
delimiterIndex + delimiter.length
42+
);
43+
3344
// Create JSON-RPC request from gateway event
3445
const jsonRpcRequest: JSONRPCRequest = {
3546
jsonrpc: "2.0",

src/typescript/src/handlers/handlers.test.ts

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { RequestHandler } from "./requestHandler.js";
2323
class MockRequestHandler implements RequestHandler {
2424
private responses: Map<string, JSONRPCResponse | JSONRPCError> = new Map();
2525
private shouldThrow = false;
26+
private lastCall: JSONRPCRequest | null = null;
2627

2728
setResponse(method: string, response: JSONRPCResponse | JSONRPCError) {
2829
this.responses.set(method, response);
@@ -32,10 +33,16 @@ class MockRequestHandler implements RequestHandler {
3233
this.shouldThrow = shouldThrow;
3334
}
3435

36+
getLastCall(): JSONRPCRequest | null {
37+
return this.lastCall;
38+
}
39+
3540
async handleRequest(
3641
request: JSONRPCRequest,
3742
_context: Context
3843
): Promise<JSONRPCResponse | JSONRPCError> {
44+
this.lastCall = request;
45+
3946
if (this.shouldThrow) {
4047
throw new Error("Mock handler error");
4148
}
@@ -910,11 +917,11 @@ describe("BedrockAgentCoreGatewayTargetHandler", () => {
910917
handler = new BedrockAgentCoreGatewayTargetHandler(mockRequestHandler);
911918
mockContext = {
912919
clientContext: {
913-
Custom: {
914-
bedrockagentcoreToolName: "test_tool",
920+
custom: {
921+
bedrockAgentCoreToolName: "target___test_tool",
915922
},
916923
},
917-
} as Context;
924+
} as unknown as Context;
918925
});
919926

920927
it("should handle valid tool invocation", async () => {
@@ -926,18 +933,63 @@ describe("BedrockAgentCoreGatewayTargetHandler", () => {
926933
mockRequestHandler.setResponse("tools/call", expectedResponse);
927934

928935
const event = { param1: "value1", param2: "value2" };
929-
const result = await handler.handleEvent(event, mockContext);
936+
const result = await handler.handle(event, mockContext);
930937

931938
expect(result).toEqual({ message: "Tool executed successfully" });
939+
940+
// Verify the extracted tool name is everything after the first delimiter
941+
const lastCall = mockRequestHandler.getLastCall();
942+
expect(lastCall?.params?.name).toBe("test_tool");
932943
});
933944

934945
it("should throw error when tool name is missing", async () => {
935-
const contextWithoutTool = { clientContext: { Custom: {} } } as Context;
946+
const contextWithoutTool = { clientContext: { custom: {} } } as unknown as Context;
947+
const event = { param1: "value1" };
948+
949+
await expect(handler.handle(event, contextWithoutTool)).rejects.toThrow(
950+
"Missing bedrockAgentCoreToolName in context"
951+
);
952+
});
953+
954+
it("should throw error when tool name format is invalid", async () => {
955+
const contextWithInvalidFormat = {
956+
clientContext: {
957+
custom: {
958+
bedrockAgentCoreToolName: "invalid_format",
959+
},
960+
},
961+
} as unknown as Context;
936962
const event = { param1: "value1" };
937963

938964
await expect(
939-
handler.handleEvent(event, contextWithoutTool)
940-
).rejects.toThrow("Missing bedrockagentcoreToolName in context");
965+
handler.handle(event, contextWithInvalidFormat)
966+
).rejects.toThrow("Invalid gateway tool name format: invalid_format");
967+
});
968+
969+
it("should handle tool name with multiple delimiters", async () => {
970+
const contextWithMultipleDelimiters = {
971+
clientContext: {
972+
custom: {
973+
bedrockAgentCoreToolName: "target___test___tool",
974+
},
975+
},
976+
} as unknown as Context;
977+
978+
const expectedResponse: JSONRPCResponse = {
979+
jsonrpc: "2.0",
980+
result: { message: "Tool executed successfully" },
981+
id: 1,
982+
};
983+
mockRequestHandler.setResponse("tools/call", expectedResponse);
984+
985+
const event = { param1: "value1" };
986+
const result = await handler.handle(event, contextWithMultipleDelimiters);
987+
988+
expect(result).toEqual({ message: "Tool executed successfully" });
989+
990+
// Verify the extracted tool name is everything after the first delimiter
991+
const lastCall = mockRequestHandler.getLastCall();
992+
expect(lastCall?.params?.name).toBe("test___tool");
941993
});
942994

943995
it("should throw error when request handler returns error", async () => {
@@ -949,7 +1001,7 @@ describe("BedrockAgentCoreGatewayTargetHandler", () => {
9491001
mockRequestHandler.setResponse("tools/call", errorResponse);
9501002

9511003
const event = { param1: "value1" };
952-
await expect(handler.handleEvent(event, mockContext)).rejects.toThrow(
1004+
await expect(handler.handle(event, mockContext)).rejects.toThrow(
9531005
"Method not found"
9541006
);
9551007
});

0 commit comments

Comments
 (0)