nite Multiple MCP Servers Through Amazon Bedrock AgentCore Gateway
31st March 2026
As AI agents scale in enterprises, teams build dozens of specialized MCP (Model Context Protocol) servers — one for order management, another for product catalog, yet another for promotions. Each server has its own endpoint, its own auth, its own tool definitions. The agent that consumes these tools suddenly becomes an integration nightmare.
Amazon Bedrock AgentCore Gateway solves this by acting as a single front door to all your MCP servers. In this post, we’ll deploy two MCP servers with separate authentication providers behind one gateway, prove the unified auth model works, and dig into the internals of how the gateway handles tool caching, routing, and session management.
Architecture Overview
┌─── Order MCP Server (Cognito Pool A)
Agent ──(1 token)──> AgentCore Gateway ──┤
└─── Catalog MCP Server (Cognito Pool B)
The agent authenticates once with the gateway. The gateway handles outbound auth to each MCP server independently. The agent never sees backend credentials.
What We’ll Build
- Order MCP Server — tools for getOrder, updateOrder, cancelOrder
- Catalog MCP Server — tools for searchProducts, getProductDetails, checkInventory
- AgentCore Gateway — single entry point with JWT auth
- Strands Agent — AI agent that discovers and invokes all 6 tools through the gateway
Each MCP server has its own Cognito user pool (simulating different teams with different auth providers). The agent only knows about the gateway’s Cognito pool.
Step 1: Create the MCP Servers
Order Management Server
from mcp.server.fastmcp import FastMCP
mcp = FastMCP(host="0.0.0.0", stateless_http=True)
@mcp.tool()
def getOrder(orderId: int) -> dict:
"""Get details of an existing order by order ID"""
return {
"orderId": orderId,
"status": "shipped",
"items": [{"name": "Widget A", "qty": 2, "price": 29.99}],
"total": 59.98,
}
@mcp.tool()
def updateOrder(orderId: int, status: str = "processing") -> dict:
"""Update an existing order's status"""
return {"orderId": orderId, "previousStatus": "pending", "newStatus": status, "updated": True}
@mcp.tool()
def cancelOrder(orderId: int) -> dict:
"""Cancel an existing order by order ID"""
return {"orderId": orderId, "status": "cancelled", "refundInitiated": True}
if __name__ == "__main__":
mcp.run(transport="streamable-http")
Product Catalog Server
from mcp.server.fastmcp import FastMCP
mcp = FastMCP(host="0.0.0.0", stateless_http=True)
@mcp.tool()
def searchProducts(query: str) -> dict:
"""Search the product catalog by keyword"""
return {
"query": query,
"results": [
{"id": 101, "name": "Widget A", "price": 29.99, "inStock": True},
{"id": 102, "name": "Widget B", "price": 49.99, "inStock": True},
{"id": 103, "name": "Gadget Pro", "price": 99.99, "inStock": False},
],
}
@mcp.tool()
def getProductDetails(productId: int) -> dict:
"""Get detailed information about a specific product"""
return {"id": productId, "name": "Widget A", "price": 29.99, "inStock": True, "rating": 4.5}
@mcp.tool()
def checkInventory(productId: int) -> dict:
"""Check real-time inventory levels for a product"""
return {"productId": productId, "available": 142, "warehouse": "US-East"}
if __name__ == "__main__":
mcp.run(transport="streamable-http")
Two requirements for AgentCore Runtime compatibility: stateless_http=True and host="0.0.0.0" on default port 8000.
Step 2: Set Up Authentication
We create three separate Cognito user pools to demonstrate the unified auth model:
| Pool | Purpose | Who uses it |
|---|---|---|
| Gateway Pool | Inbound auth — who can call the gateway | Agent |
| Order Runtime Pool | Outbound auth — gateway calls Order server | Gateway |
| Catalog Runtime Pool | Outbound auth — gateway calls Catalog server | Gateway |
# Create Gateway Cognito Pool (agent authenticates here)
gateway_pool = cognito_client.create_user_pool(PoolName="AgentCoreGatewayPool")
cognito_client.create_resource_server(
UserPoolId=gateway_pool_id,
Identifier="agentcore-gateway",
Scopes=[{"ScopeName": "invoke", "ScopeDescription": "Invoke gateway tools"}],
)
gateway_app = cognito_client.create_user_pool_client(
UserPoolId=gateway_pool_id,
AllowedOAuthFlows=["client_credentials"],
AllowedOAuthScopes=["agentcore-gateway/invoke"],
GenerateSecret=True,
)
Step 3: Create the AgentCore Gateway
gateway_client = boto3.client("bedrock-agentcore-control")
auth_config = {
"customJWTAuthorizer": {
"allowedClients": [gateway_client_id],
"discoveryUrl": gateway_discovery_url,
}
}
create_response = gateway_client.create_gateway(
name="DemoGateway",
roleArn=role_arn,
protocolType="MCP",
authorizerType="CUSTOM_JWT",
authorizerConfiguration=auth_config,
)
gateway_id = create_response["gatewayId"]
gateway_url = create_response["gatewayUrl"]
Step 4: Deploy MCP Servers to AgentCore Runtime
from bedrock_agentcore_starter_toolkit import Runtime
agentcore_runtime = Runtime()
agentcore_runtime.configure(
entrypoint="server.py",
auto_create_execution_role=True,
auto_create_ecr=True,
requirements_file="requirements.txt",
region=region,
authorizer_configuration=runtime_auth_config,
protocol="MCP",
agent_name="mcp_server_agentcore",
)
launch_result = agentcore_runtime.launch()
The toolkit handles Dockerfile generation, ECR repository creation, CodeBuild, and Runtime agent registration. Repeat for the catalog server with its own Cognito pool.
Step 5: Add MCP Servers as Gateway Targets
# Create credential provider for outbound auth
cognito_provider = identity_client.create_oauth2_credential_provider(
name="gateway-mcp-server-identity",
credentialProviderVendor="CustomOauth2",
oauth2ProviderConfigInput={
"customOauth2ProviderConfig": {
"oauthDiscovery": {"discoveryUrl": runtime_discovery_url},
"clientId": runtime_client_id,
"clientSecret": runtime_client_secret,
}
},
)
# Add MCP server as gateway target
gateway_client.create_gateway_target(
name="mcp-server-target",
gatewayIdentifier=gateway_id,
targetConfiguration={"mcp": {"mcpServer": {"endpoint": mcp_url}}},
credentialProviderConfigurations=[{
"credentialProviderType": "OAUTH",
"credentialProvider": {
"oauthCredentialProvider": {
"providerArn": cognito_provider_arn,
"scopes": ["agentcore-runtime/invoke"],
}
},
}],
)
When create_gateway_target is called, the gateway performs an implicit synchronisation — it connects to the MCP server, calls tools/list, caches the tool definitions, and generates embeddings for semantic search.
Step 6: Test with the Agent
from strands import Agent
from strands.models.bedrock import BedrockModel
from mcp.client.streamable_http import streamablehttp_client
from strands.tools.mcp.mcp_client import MCPClient
# ONE token for the gateway — agent never sees backend credentials
token = get_cognito_token(gateway_pool_id, gateway_client_id, gateway_client_secret)
# ONE connection to the gateway
def create_transport():
return streamablehttp_client(gateway_url, headers={"Authorization": f"Bearer {token}"})
client = MCPClient(create_transport)
with client:
tools = client.list_tools_sync() # Returns ALL tools from ALL servers
agent = Agent(model=BedrockModel(model_id="us.anthropic.claude-sonnet-4-6"), tools=tools)
agent("Search for widgets in the catalog, then check order 42")
Test Results
Tool discovery — 6 tools from 2 servers, 1 connection:
Order Server tools (3):
- mcp-server-target___cancelOrder
- mcp-server-target___getOrder
- mcp-server-target___updateOrder
Catalog Server tools (3):
- catalog-server-target___searchProducts
- catalog-server-target___getProductDetails
- catalog-server-target___checkInventory
Cross-server invocation — single prompt hits both backends:
Prompt: "Search for widgets in the catalog, then check order 42"
Tool #1: catalog-server-target___searchProducts → 3 products found
Tool #2: mcp-server-target___getOrder → Order 42 contains Widget A (shipped)
"Order #42 already contains 2x Widget A and has been shipped."
Auth summary:
Tokens obtained by agent: 1 (gateway token)
Tokens managed by gateway: 2 (one per backend server)
MCP connections by agent: 1 (to gateway)
Backend credentials seen by agent: 0
How the Gateway Works Internally
Tool caching and synchronisation
When you add a gateway target, the gateway pulls tool definitions from the MCP server:
| What’s pulled | Example |
|---|---|
| Tool name | getOrder → stored as mcp-server-target___getOrder |
| Description | "Get details of an existing order by order ID" |
| Input schema | {"orderId": {"type": "integer"}} (from Python type hints) |
| Embedding | Vector representation for semantic search |
tools/list reads from this cache — it never hits the live MCP server. tools/call is real-time — the gateway forwards to the live MCP server with a fresh OAuth token.
To refresh the cache after deploying new tools:
gateway_client.synchronize_gateway_targets(
gatewayIdentifier=gateway_id,
targetId=target_id,
)
Naming collision prevention
The gateway automatically prefixes tool names with the target name using triple underscores:
Target "mcp-server-target": getOrder → mcp-server-target___getOrder
Target "catalog-server-target": getOrder → catalog-server-target___getOrder
Teams name their tools freely. The gateway namespaces them during sync.
Session management and microVM routing
Request 1 (no session ID) → new microVM spins up (cold start) → returns session ID "abc"
Request 2 (session ID "abc") → same microVM (warm, fast)
Request 3 (session ID "abc") → same microVM (warm, fast)
Stateless mode (stateless_http=True): session ID is an optimisation. Losing it means a cold start, but the request still works — any microVM can handle any request.
Stateful mode (stateless_http=False): session ID is required. The server holds state in memory. Losing the session ID breaks the workflow because the state lives on a specific microVM.
Hidden Values: What the Gateway Gives You
Protocol translation — your REST APIs become MCP tools
targetConfiguration = {
"mcp": {
"mcpServer": {"endpoint": "https://..."}, # MCP server
"lambda": {"lambdaArn": "arn:aws:lambda:..."}, # Lambda function
"openApiSchema": {"s3": {"uri": "s3://..."}}, # REST API via OpenAPI
"apiGateway": {"restApiId": "...", "stage": "..."}, # API Gateway REST API
}
}
Your existing REST APIs become MCP tools without writing an MCP server. The agent calls tools/call and the gateway converts it to an HTTP request, Lambda invocation, or AWS service call.
API Gateway integration with tool filtering
"apiGatewayToolConfiguration": {
"toolFilters": [
{"filterPath": "/orders/*", "methods": ["GET", "POST"]},
# /admin/* endpoints are NOT exposed
],
"toolOverrides": [
{
"name": "getOrder",
"description": "Fetch order by ID", # override auto-generated description
"path": "/orders/{id}",
"method": "GET",
}
]
}
Credential rotation without agent downtime
# Backend team rotates credentials — zero agent changes required
identity_client.update_oauth2_credential_provider(
name="gateway-mcp-server-identity",
oauth2ProviderConfigInput={
"customOauth2ProviderConfig": {
"clientId": same_client_id,
"clientSecret": "NEW_ROTATED_SECRET",
}
},
)
Three auth methods per target
| Type | Use case |
|---|---|
OAUTH | MCP servers with Cognito/OAuth2 |
API_KEY | Third-party MCP servers with API key auth |
GATEWAY_IAM_ROLE | AWS services that use SigV4/IAM |
One gateway can route to an MCP server via OAuth, a third-party API via API key, and a Lambda via IAM — all from the same agent connection.
Failure isolation between targets
Agent: "Search products and check order 42"
catalog-server-target___searchProducts → Catalog server (UP) → ✅ results
mcp-server-target___getOrder → Order server (DOWN) → ❌ this tool only
The catalog call succeeds even when the order server is down. Without a gateway, a shared connection failure takes out all tools.
Gateway federation
Regional Gateway (US) ──┐
Regional Gateway (EU) ──┼──> Global Gateway ──> Agent
Regional Gateway (APAC) ──┘
One AgentCore Gateway can serve as a target for another gateway. Each region manages its own MCP servers. A global gateway aggregates them. Organizational boundaries become routing boundaries.
When to Use AgentCore Gateway
Use it when:
- Multiple MCP servers across teams
- Different auth providers per backend
- Mixed backends (MCP + Lambda + REST APIs)
- Need centralized tool management and discovery
Skip it when: single MCP server, single agent — direct connection is simpler and one less network hop.
Project Structure
agentcore_gateway/
├── mcp_server/
│ ├── server.py # Order Management MCP server
│ └── requirements.txt
├── mcp_server_catalog/
│ ├── server.py # Product Catalog MCP server
│ └── requirements.txt
├── agent/
│ └── ordering_agent.py # Connects via gateway
└── scripts/
├── 01_setup_cognito.py # Create 3 Cognito pools
├── 02_setup_iam.py # IAM role for AgentCore
├── 03_deploy_gateway.py # Gateway + Order server + target
├── 04_test_agent.py # Basic agent test
├── 05_cleanup.py # Tear down all resources
├── 06_add_catalog_server.py # Deploy catalog with separate auth
└── 07_test_unified_auth.py # Prove unified auth works
python scripts/01_setup_cognito.py
python scripts/02_setup_iam.py
python scripts/03_deploy_gateway.py
python scripts/06_add_catalog_server.py
python scripts/07_test_unified_auth.py
python scripts/05_cleanup.py
AgentCore Gateway turns MCP server sprawl into an infrastructure concern rather than an application concern. Teams own their MCP servers. The platform team manages the gateway. Agents connect to one endpoint with one token. As you add server 3, 4, 5, and beyond — zero agent code changes.
The core insight: AgentCore Gateway is to MCP servers what API Gateway is to REST APIs — centralised routing, auth, discovery, and management. Without it, every agent is its own integration layer.
More recent articles
- OpenUSD: Advanced Patterns and Common Gotchas. - 28th March 2026
- OpenUSD Mastery: From Composition to Pipeline — A SO-101 Arm Journey - 25th March 2026