Skip to content

Connect your agent to MCP tools

Connect your agent to tools that live outside your codebase: databases, SaaS APIs, and internal services exposed over the Model Context Protocol (MCP), an open standard for providing context to language models. Strands loads an MCP server’s tools and hands them to the agent like any other tool, in both Python and TypeScript.

from mcp import stdio_client, StdioServerParameters
from strands import Agent
from strands.tools.mcp import MCPClient
# Create MCP client with stdio transport
mcp_client = MCPClient(lambda: stdio_client(
StdioServerParameters(
command="uvx",
args=["awslabs.aws-documentation-mcp-server@latest"]
)
))
# Pass MCP client directly to agent - lifecycle managed automatically
agent = Agent(tools=[mcp_client])
agent("What is AWS Lambda?")

Managed Integration (Recommended)

The MCPClient implements the ToolProvider interface, enabling direct usage in the Agent constructor with automatic lifecycle management:

from mcp import stdio_client, StdioServerParameters
from strands import Agent
from strands.tools.mcp import MCPClient
mcp_client = MCPClient(lambda: stdio_client(
StdioServerParameters(
command="uvx",
args=["awslabs.aws-documentation-mcp-server@latest"]
)
))
# Direct usage - connection lifecycle managed automatically
agent = Agent(tools=[mcp_client])
response = agent("What is AWS Lambda?")

Manual Context Management

For cases requiring explicit control over the MCP session lifecycle, use context managers:

with mcp_client:
tools = mcp_client.list_tools_sync()
agent = Agent(tools=tools)
agent("What is AWS Lambda?") # Must be within context

The connection pattern above works for any transport: only the transport object you pass to the client changes. Strands supports three:

  • stdio for local processes and command-line servers that speak MCP over standard I/O.
  • Streamable HTTP for remote servers reachable over HTTP, including servers behind OAuth or AWS IAM authentication.
  • Server-Sent Events (SSE) for HTTP servers that use the older SSE transport.

For the configuration of each transport, including authentication, see MCP transports.

Combine tools from multiple MCP servers in a single agent:

from mcp import stdio_client, StdioServerParameters
from mcp.client.sse import sse_client
from strands import Agent
from strands.tools.mcp import MCPClient
# Create multiple clients
sse_mcp_client = MCPClient(lambda: sse_client("http://localhost:8000/sse"))
stdio_mcp_client = MCPClient(lambda: stdio_client(
StdioServerParameters(command="python", args=["path/to/mcp_server.py"])
))
# Manual approach - explicit context management
with sse_mcp_client, stdio_mcp_client:
tools = sse_mcp_client.list_tools_sync() + stdio_mcp_client.list_tools_sync()
agent = Agent(tools=tools)
# Managed approach
agent = Agent(tools=[sse_mcp_client, stdio_mcp_client])

Load from configuration

Use load_servers to create clients from a dictionary or JSON file. Transport types are detected from command or url, and environment placeholders keep credentials out of the configuration:

from strands import Agent
from strands.tools.mcp import MCPClient
clients = MCPClient.load_servers(
{
"mcpServers": {
"documentation": {
"command": "uvx",
"args": ["awslabs.aws-documentation-mcp-server@latest"],
},
"protected-api": {
"url": "https://api.example.com/mcp/",
"auth": {
"client_id": "${OAUTH_CLIENT_ID}",
"client_secret": "${OAUTH_CLIENT_SECRET}",
"scopes": ["mcp:tools"],
},
},
}
},
prefix_with_server_name=True,
)
agent = Agent(tools=clients)

Python’s MCPClient supports tool filtering and name prefixing to manage tools from multiple servers.

Tool Filtering

Control which tools are loaded using the tool_filters parameter:

from mcp import stdio_client, StdioServerParameters
from strands.tools.mcp import MCPClient
import re
# String matching - loads only specified tools
filtered_client = MCPClient(
lambda: stdio_client(StdioServerParameters(
command="uvx",
args=["awslabs.aws-documentation-mcp-server@latest"]
)),
tool_filters={"allowed": ["search_documentation", "read_documentation"]}
)
# Regex patterns
regex_client = MCPClient(
lambda: stdio_client(StdioServerParameters(
command="uvx",
args=["awslabs.aws-documentation-mcp-server@latest"]
)),
tool_filters={"allowed": [re.compile(r"^search_.*")]}
)
# Combined filters - applies allowed first, then rejected
combined_client = MCPClient(
lambda: stdio_client(StdioServerParameters(
command="uvx",
args=["awslabs.aws-documentation-mcp-server@latest"]
)),
tool_filters={
"allowed": [re.compile(r".*documentation$")],
"rejected": ["read_documentation"]
}
)

Tool Name Prefixing

Prevent name conflicts when using multiple MCP servers:

aws_docs_client = MCPClient(
lambda: stdio_client(StdioServerParameters(
command="uvx",
args=["awslabs.aws-documentation-mcp-server@latest"]
)),
prefix="aws_docs"
)
other_client = MCPClient(
lambda: stdio_client(StdioServerParameters(
command="uvx",
args=["other-mcp-server@latest"]
)),
prefix="other"
)
# Tools will be named: aws_docs_search_documentation, other_search, etc.
agent = Agent(tools=[aws_docs_client, other_client])

While tools are typically invoked by the agent based on user requests, MCP tools can also be called directly:

result = mcp_client.call_tool_sync(
tool_use_id="tool-123",
name="calculator",
arguments={"x": 10, "y": 20}
)
print(f"Result: {result['content'][0]['text']}")

Custom MCP servers can be created to extend agent capabilities:

from mcp.server import FastMCP
# Create an MCP server
mcp = FastMCP("Calculator Server")
# Define a tool
@mcp.tool(description="Calculator tool which performs calculations")
def calculator(x: int, y: int) -> int:
return x + y
# Run the server with SSE transport
mcp.run(transport="sse")

For more information on implementing MCP servers, see the MCP documentation.

An MCP server can pause a tool call to request additional input from the user. Configure an elicitation callback on the client to respond to these requests:

The server declares the schema it wants back, and the client returns a matching response:

server.py
from mcp.server import FastMCP
from pydantic import BaseModel, Field
class ApprovalSchema(BaseModel):
username: str = Field(description="Who is approving?")
server = FastMCP("mytools")
@server.tool()
async def delete_files(paths: list[str]) -> str:
result = await server.get_context().elicit(
message=f"Do you want to delete {paths}",
schema=ApprovalSchema,
)
if result.action != "accept":
return f"User {result.data.username} rejected deletion"
# Perform deletion...
return f"User {result.data.username} approved deletion"
server.run()
client.py
from mcp import stdio_client, StdioServerParameters
from mcp.types import ElicitResult
from strands import Agent
from strands.tools.mcp import MCPClient
async def elicitation_callback(context, params):
print(f"ELICITATION: {params.message}")
# Get user confirmation...
return ElicitResult(
action="accept",
content={"username": "myname"}
)
client = MCPClient(
lambda: stdio_client(
StdioServerParameters(command="python", args=["/path/to/server.py"])
),
elicitation_callback=elicitation_callback,
)
with client:
agent = Agent(tools=client.list_tools_sync())
result = agent("Delete 'a/b/c.txt' and share the name of the approver")

For more information on elicitation, see the MCP specification.

MCP servers can report incremental progress during long-running tool calls. Configure a progress_callback on the client to receive these updates:

from mcp import stdio_client, StdioServerParameters
from strands import Agent
from strands.tools.mcp import MCPClient
async def progress_callback(progress, total, message):
pct = f"{progress}/{total}" if total is not None else str(progress)
label = f" - {message}" if message else ""
print(f"Progress: {pct}{label}")
client = MCPClient(
lambda: stdio_client(
StdioServerParameters(command="python", args=["/path/to/server.py"])
),
progress_callback=progress_callback,
)
with client:
agent = Agent(tools=client.list_tools_sync())
agent("Run the long-running task")

The callback receives three arguments:

ArgumentTypeDescription
progressfloatCurrent progress value reported by the server
totalfloat | NoneTotal value (may be None if the server doesn’t report it)
messagestr | NoneOptional human-readable status message from the server

You can also pass a progress_callback directly to call_tool_sync or call_tool_async to override the instance-level callback for a single call:

result = client.call_tool_sync(
tool_use_id="tool-123",
name="long_running_tool",
arguments={"input": "data"},
progress_callback=my_one_off_callback,
)
  • Tool Descriptions: Provide clear descriptions for tools to help the agent understand when and how to use them
  • Error Handling: Return informative error messages when tools fail to execute properly
  • Security: Consider security implications when exposing tools via MCP, especially for network-accessible servers
  • Connection Management: In Python, always use context managers (with statements) to ensure proper cleanup of MCP connections
  • Timeouts: Set appropriate timeouts for tool calls to prevent hanging on long-running operations

Tools relying on an MCP connection must be used within a context manager. Operations will fail when the agent is used outside the with statement block.

# Correct
with mcp_client:
agent = Agent(tools=mcp_client.list_tools_sync())
response = agent("Your prompt") # Works
# Incorrect
with mcp_client:
agent = Agent(tools=mcp_client.list_tools_sync())
response = agent("Your prompt") # Fails - outside context

Connection failures occur when there are problems establishing a connection with the MCP server. Verify that:

  • The MCP server is running and accessible
  • Network connectivity is available and firewalls allow the connection
  • The URL or command is correct and properly formatted

If tools aren’t being discovered:

  • Confirm the MCP server implements the list_tools method correctly
  • Verify all tools are registered with the server

When tool execution fails:

  • Verify tool arguments match the expected schema
  • Check server logs for detailed error information