Skip to content

Commit f7833a9

Browse files
committed
feat: support for Bedrock AgentCore Gateway Lambda targets
1 parent b9d13fd commit f7833a9

File tree

7 files changed

+398
-7
lines changed

7 files changed

+398
-7
lines changed

README.md

Lines changed: 156 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,12 @@ Each Lambda function invocation will:
4343
1. Return the server's response to the function caller
4444
1. Shut down the MCP server child process
4545

46-
This library supports connecting to Lambda-based MCP servers in three ways:
46+
This library supports connecting to Lambda-based MCP servers in four ways:
4747

4848
1. The [MCP Streamable HTTP transport](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#streamable-http), using Amazon API Gateway. Typically authenticated using OAuth.
49-
2. A custom Streamable HTTP transport with support for SigV4, using a Lambda function URL. Authenticated with AWS IAM.
50-
3. A custom Lambda invocation transport, using the Lambda Invoke API directly. Authenticated with AWS IAM.
49+
1. The MCP Streamable HTTP transport, using Amazon Bedrock AgentCore Gateway (currently in Preview). Authenticated using OAuth.
50+
1. A custom Streamable HTTP transport with support for SigV4, using a Lambda function URL. Authenticated with AWS IAM.
51+
1. A custom Lambda invocation transport, using the Lambda Invoke API directly. Authenticated with AWS IAM.
5152

5253
## Use API Gateway
5354

@@ -206,6 +207,158 @@ See a full example as part of the sample chatbot [here](examples/chatbots/typesc
206207

207208
</details>
208209

210+
## Use Bedrock AgentCore Gateway
211+
212+
```mermaid
213+
flowchart LR
214+
App["MCP Client"]
215+
T1["MCP Server<br>(Lambda function)"]
216+
T2["Bedrock AgentCore Gateway"]
217+
T3["OAuth Server<br>(Cognito or similar)"]
218+
App -->|"MCP Streamable<br>HTTP Transport"| T2
219+
T2 -->|"Invoke"| T1
220+
T2 -->|"Authorize"| T3
221+
```
222+
223+
This solution is compatible with most MCP clients that support the streamable HTTP transport.
224+
MCP servers deployed with this architecture can typically be used with off-the-shelf
225+
MCP-compatible applications such as Cursor, Cline, Claude Desktop, etc.
226+
227+
Using Bedrock AgentCore Gateway in front of your stdio-based MCP server requires that
228+
you duplicate the MCP server's input schema (and optionally, the output schema), and
229+
provide it in the AgentCore Gateway Lambda target configuration. AgentCore Gateway
230+
can then advertise the schema to HTTP clients and validate request inputs and outputs.
231+
232+
You can choose your desired OAuth server provider for this solution, such as Amazon Cognito,
233+
Okta, or Auth0.
234+
235+
<details>
236+
237+
<summary><b>Python server example</b></summary>
238+
239+
```python
240+
import sys
241+
from mcp.client.stdio import StdioServerParameters
242+
from mcp_lambda import BedrockAgentCoreGatewayTargetHandler, StdioServerAdapterRequestHandler
243+
244+
server_params = StdioServerParameters(
245+
command=sys.executable,
246+
args=[
247+
"-m",
248+
"my_mcp_server_python_module",
249+
"--my-server-command-line-parameter",
250+
"some_value",
251+
],
252+
)
253+
254+
255+
request_handler = StdioServerAdapterRequestHandler(server_params)
256+
event_handler = BedrockAgentCoreGatewayTargetHandler(request_handler)
257+
258+
259+
def handler(event, context):
260+
return event_handler.handle(event, context)
261+
```
262+
263+
</details>
264+
265+
<details>
266+
267+
<summary><b>Typescript server example</b></summary>
268+
269+
```typescript
270+
import { Handler, Context } from "aws-lambda";
271+
import {
272+
BedrockAgentCoreGatewayTargetHandler,
273+
StdioServerAdapterRequestHandler,
274+
} from "@aws/run-mcp-servers-with-aws-lambda";
275+
276+
const serverParams = {
277+
command: "npx",
278+
args: [
279+
"--offline",
280+
"my-mcp-server-typescript-module",
281+
"--my-server-command-line-parameter",
282+
"some_value",
283+
],
284+
};
285+
286+
const requestHandler = new BedrockAgentCoreGatewayTargetHandler(
287+
new StdioServerAdapterRequestHandler(serverParams)
288+
);
289+
290+
export const handler: Handler = async (
291+
event: event: Record<string, unknown>,
292+
context: Context
293+
): Promise<event: Record<string, unknown>> => {
294+
return requestHandler.handle(event, context);
295+
};
296+
```
297+
298+
</details>
299+
300+
<details>
301+
302+
<summary><b>Python client example</b></summary>
303+
304+
```python
305+
from mcp import ClientSession
306+
from mcp.client.streamable_http import streamablehttp_client
307+
308+
# Create OAuth client provider here
309+
310+
async with streamablehttp_client(
311+
url="https://abc123.gateway.bedrock-agentcore.us-west-2.amazonaws.com/mcp",
312+
auth=oauth_client_provider,
313+
) as (
314+
read_stream,
315+
write_stream,
316+
_,
317+
):
318+
async with ClientSession(read_stream, write_stream) as session:
319+
await session.initialize()
320+
tool_result = await session.call_tool("echo", {"message": "hello"})
321+
```
322+
323+
See a full example as part of the sample chatbot [here](examples/chatbots/python/server_clients/interactive_oauth.py).
324+
325+
</details>
326+
327+
<details>
328+
329+
<summary><b>Typescript client example</b></summary>
330+
331+
```typescript
332+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
333+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
334+
335+
const client = new Client(
336+
{
337+
name: "my-client",
338+
version: "0.0.1",
339+
},
340+
{
341+
capabilities: {
342+
sampling: {},
343+
},
344+
}
345+
);
346+
347+
// Create OAuth client provider here
348+
349+
const transport = new StreamableHTTPClientTransport(
350+
"https://abc123.gateway.bedrock-agentcore.us-west-2.amazonaws.com/mcp",
351+
{
352+
authProvider: oauthProvider,
353+
}
354+
);
355+
await client.connect(transport);
356+
```
357+
358+
See a full example as part of the sample chatbot [here](examples/chatbots/typescript/src/server_clients/interactive_oauth.ts).
359+
360+
</details>
361+
209362
## Use a Lambda function URL
210363

211364
```mermaid

src/python/src/mcp_lambda/handlers/__init__.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,29 @@
77
- RequestHandler: Interface for handling individual JSON-RPC requests
88
- StreamableHttpHandler: Base class handling MCP protocol specifics
99
- APIGatewayProxyEventHandler: Handler for API Gateway V1 events
10-
- APIGatewayProxyEventV2Handler: Handler for API Gateway V2 events
10+
- APIGatewayProxyEventV2Handler: Handler for API Gateway V2 events
1111
- LambdaFunctionURLEventHandler: Handler for Lambda Function URL events
12+
- BedrockAgentCoreGatewayTargetHandler: Handler for Bedrock AgentCore Gateway events
1213
"""
1314

1415
from .api_gateway_proxy_event_handler import APIGatewayProxyEventHandler
1516
from .api_gateway_proxy_event_v2_handler import APIGatewayProxyEventV2Handler
17+
from .bedrock_agent_core_gateway_handler import BedrockAgentCoreGatewayTargetHandler
1618
from .lambda_function_url_event_handler import LambdaFunctionURLEventHandler
1719
from .request_handler import RequestHandler
18-
from .streamable_http_handler import HttpResponse, ParsedHttpRequest, StreamableHttpHandler
20+
from .streamable_http_handler import (
21+
HttpResponse,
22+
ParsedHttpRequest,
23+
StreamableHttpHandler,
24+
)
1925

2026
__all__ = [
2127
"RequestHandler",
22-
"StreamableHttpHandler",
28+
"StreamableHttpHandler",
2329
"ParsedHttpRequest",
2430
"HttpResponse",
2531
"APIGatewayProxyEventHandler",
26-
"APIGatewayProxyEventV2Handler",
32+
"APIGatewayProxyEventV2Handler",
2733
"LambdaFunctionURLEventHandler",
34+
"BedrockAgentCoreGatewayTargetHandler",
2835
]
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
from typing import Any, Dict
2+
3+
from aws_lambda_powertools.utilities.typing import LambdaContext
4+
from mcp.types import JSONRPCError, JSONRPCRequest
5+
6+
from .request_handler import RequestHandler
7+
8+
9+
class BedrockAgentCoreGatewayTargetHandler:
10+
"""
11+
Handler for Bedrock AgentCore Gateway Lambda targets
12+
13+
This handler processes direct Lambda invocations from Bedrock AgentCore Gateway.
14+
Bedrock AgentCore Gateway passes tool arguments directly in the event and
15+
provides metadata through the Lambda context's client_context.custom properties.
16+
"""
17+
18+
def __init__(self, request_handler: RequestHandler):
19+
self.request_handler = request_handler
20+
21+
def handle_event(self, event: Dict[str, Any], context: LambdaContext) -> Any:
22+
"""Handle Lambda invocation from Bedrock AgentCore Gateway"""
23+
# 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")
27+
28+
if not tool_name:
29+
raise ValueError("Missing bedrockagentcoreToolName in context")
30+
31+
# Create JSON-RPC request from gateway event
32+
jsonrpc_request = JSONRPCRequest(
33+
jsonrpc="2.0",
34+
id=1,
35+
method="tools/call",
36+
params={
37+
"name": tool_name,
38+
"arguments": event,
39+
},
40+
)
41+
42+
result = self.request_handler.handle_request(jsonrpc_request, context)
43+
44+
if isinstance(result, JSONRPCError):
45+
raise Exception(result.error.message)
46+
47+
return result.result

src/python/tests/test_handlers.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from mcp_lambda.handlers import (
2121
APIGatewayProxyEventHandler,
2222
APIGatewayProxyEventV2Handler,
23+
BedrockAgentCoreGatewayTargetHandler,
2324
LambdaFunctionURLEventHandler,
2425
RequestHandler,
2526
)
@@ -509,3 +510,74 @@ def create_event(self, method="POST", headers=None, body=None):
509510
},
510511
"isBase64Encoded": False,
511512
}
513+
514+
515+
class TestBedrockAgentCoreGatewayTargetHandler:
516+
"""Test cases for BedrockAgentCoreGatewayTargetHandler."""
517+
518+
def test_handle_valid_tool_invocation(self):
519+
"""Test handling valid tool invocation."""
520+
# Create a mock request handler that handles tools/call
521+
mock_handler = Mock(spec=RequestHandler)
522+
mock_handler.handle_request.return_value = JSONRPCResponse(
523+
jsonrpc="2.0",
524+
result={"message": "Tool executed successfully"},
525+
id=1,
526+
)
527+
528+
handler = BedrockAgentCoreGatewayTargetHandler(mock_handler)
529+
530+
# Mock context with tool name
531+
context = Mock(spec=LambdaContext)
532+
context.client_context = Mock()
533+
context.client_context.custom = {"bedrockagentcoreToolName": "test_tool"}
534+
535+
event = {"param1": "value1", "param2": "value2"}
536+
result = handler.handle_event(event, context)
537+
538+
assert result == {"message": "Tool executed successfully"}
539+
540+
# Verify the request was properly constructed
541+
call_args = mock_handler.handle_request.call_args[0]
542+
request = call_args[0]
543+
assert request.method == "tools/call"
544+
assert request.params["name"] == "test_tool"
545+
assert request.params["arguments"] == event
546+
547+
def test_missing_tool_name_raises_error(self):
548+
"""Test that missing tool name raises ValueError."""
549+
handler = BedrockAgentCoreGatewayTargetHandler(Mock(spec=RequestHandler))
550+
551+
# Mock context without tool name
552+
context = Mock(spec=LambdaContext)
553+
context.client_context = Mock()
554+
context.client_context.custom = {}
555+
556+
event = {"param1": "value1"}
557+
558+
with pytest.raises(
559+
ValueError, match="Missing bedrockagentcoreToolName in context"
560+
):
561+
handler.handle_event(event, context)
562+
563+
def test_request_handler_error_raises_exception(self):
564+
"""Test that request handler errors are raised as exceptions."""
565+
# Create a mock request handler that returns an error
566+
mock_handler = Mock(spec=RequestHandler)
567+
mock_handler.handle_request.return_value = JSONRPCError(
568+
jsonrpc="2.0",
569+
error=ErrorData(code=METHOD_NOT_FOUND, message="Tool not found"),
570+
id=1,
571+
)
572+
573+
handler = BedrockAgentCoreGatewayTargetHandler(mock_handler)
574+
575+
# Mock context with tool name
576+
context = Mock(spec=LambdaContext)
577+
context.client_context = Mock()
578+
context.client_context.custom = {"bedrockagentcoreToolName": "unknown_tool"}
579+
580+
event = {"param1": "value1"}
581+
582+
with pytest.raises(Exception, match="Tool not found"):
583+
handler.handle_event(event, context)
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { Context } from "aws-lambda";
2+
import {
3+
JSONRPCRequest,
4+
isJSONRPCError,
5+
} from "@modelcontextprotocol/sdk/types.js";
6+
import { RequestHandler } from "./requestHandler.js";
7+
8+
/**
9+
* Handler for Bedrock AgentCore Gateway Lambda targets
10+
*
11+
* This handler processes direct Lambda invocations from Bedrock AgentCore Gateway.
12+
* Bedrock AgentCore Gateway passes tool arguments directly in the event and
13+
* provides metadata through the Lambda context's client_context.custom properties.
14+
*/
15+
export class BedrockAgentCoreGatewayTargetHandler {
16+
constructor(private requestHandler: RequestHandler) {}
17+
18+
/**
19+
* Handle Lambda invocation from Bedrock AgentCore Gateway
20+
*/
21+
async handleEvent(
22+
event: Record<string, unknown>,
23+
context: Context
24+
): Promise<unknown> {
25+
// Extract tool metadata from context
26+
const toolName =
27+
context.clientContext?.Custom?.["bedrockagentcoreToolName"];
28+
29+
if (!toolName) {
30+
throw new Error("Missing bedrockagentcoreToolName in context");
31+
}
32+
33+
// Create JSON-RPC request from gateway event
34+
const jsonRpcRequest: JSONRPCRequest = {
35+
jsonrpc: "2.0",
36+
id: 1,
37+
method: "tools/call",
38+
params: {
39+
name: toolName,
40+
arguments: event,
41+
},
42+
};
43+
44+
const result = await this.requestHandler.handleRequest(
45+
jsonRpcRequest,
46+
context
47+
);
48+
49+
if (isJSONRPCError(result)) {
50+
throw new Error(result.error.message);
51+
}
52+
53+
return result.result;
54+
}
55+
}

0 commit comments

Comments
 (0)