Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 2 additions & 7 deletions examples/06_mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ mcp_clients:
prefix: calc # tools: calc_add, calc_multiply, calc_percentage
```

`calculator_server.py` is an ordinary `FastMCP` script. The MCP client spawns it
`calculator_server.py` is an ordinary `MCPServer` script. The MCP client spawns it
on first use and tears it down with the agent, so its whole lifetime is handled
for you.

Expand Down Expand Up @@ -60,12 +60,7 @@ AWS publicly hosts a Knowledge MCP server at `https://knowledge-mcp.global.api.a
No API key is needed.

This is the mode to use in production: deploy your MCP server independently
(container, VM, or behind a gateway) and point agents at its URL. To try it
locally, run the example server over HTTP and swap `command:` for `url:`:

```bash
uv run python examples/06_mcp/calculator_server.py --http
```
(container, VM, or behind a gateway) and point agents at its URL.

### Attaching both clients to one agent

Expand Down
17 changes: 5 additions & 12 deletions examples/06_mcp/calculator_server.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,15 @@
"""A standalone MCP server for the 06_mcp example.

This is a plain ``FastMCP`` server.
Compatible with mcp>=2.0 (MCPServer replaces FastMCP).
The config launches it as a stdio subprocess via ``command:``, so you never
start it by hand.

To run it directly over HTTP instead (and connect with ``url:``)::

uv run python examples/06_mcp/calculator_server.py --http
"""

from __future__ import annotations

import sys

from mcp.server.fastmcp import FastMCP
from mcp.server import MCPServer

mcp = FastMCP("calculator")
mcp = MCPServer("calculator")


@mcp.tool()
Expand Down Expand Up @@ -61,6 +55,5 @@ def percentage(value: float, percent: float) -> float:


if __name__ == "__main__":
# stdio is the default: the MCP client spawns this file as a subprocess.
# --http serves over Streamable HTTP for use with a url: client instead.
mcp.run(transport="streamable-http" if "--http" in sys.argv else "stdio")
# The MCP client spawns this file as a stdio subprocess.
mcp.run(transport="stdio")
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ dependencies = [
"strands-agents>=1.52.0,<2.0.0",
"pydantic>=2.12.5",
"pyyaml>=6.0.0",
"mcp>=1.24.0",
"mcp>=2.0.0",
]

[project.scripts]
Expand Down
96 changes: 27 additions & 69 deletions src/strands_compose/mcp/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def create_mcp_client(
``httpx_client_factory``

**streamable-http**: ``headers``, ``http_client`` (pre-configured
``httpx.AsyncClient``), ``terminate_on_close``
``httpx2.AsyncClient``), ``terminate_on_close``

**kwargs: Additional kwargs forwarded to strands MCPClient
(startup_timeout, tool_filters, prefix, elicitation_callback,
Expand All @@ -62,7 +62,8 @@ def create_mcp_client(
A strands MCPClient instance.

Raises:
ValueError: If connection parameters are ambiguous.
ValueError: If connection parameters are ambiguous, or if ``transport``
is not an HTTP transport while ``url`` is given.
"""
modes = sum(x is not None for x in [url, command])
if modes != 1:
Expand All @@ -71,74 +72,31 @@ def create_mcp_client(
"url=str for an HTTP MCP server, command=list[str] for subprocess stdio."
)

from strands.tools.mcp import MCPClient as _MCPClient

opts = transport_options or {}
transport_callable: Any

if url is not None:
transport_callable = _transport_for_http(url, transport, opts)
else:
if url is None:
# command is guaranteed non-None by the modes == 1 check above.
transport_callable = stdio_transport(command, **opts) # ty: ignore

return _make_strands_client(transport_callable=transport_callable, **kwargs)


def _make_strands_client(**kwargs: Any) -> MCPClient:
"""Create a strands MCPClient instance.

Args:
**kwargs: Arguments forwarded to strands MCPClient constructor.

Returns:
A strands MCPClient instance.
"""
from strands.tools.mcp import MCPClient as _MCPClient

return _MCPClient(**kwargs)


def _transport_for_http(
url: str,
transport: MCP_TRANSPORT | None,
opts: dict[str, Any] | None = None,
) -> Any:
"""Build a transport callable for an HTTP-based MCP connection.

Args:
url: The MCP server URL.
transport: Explicit transport override, or ``None`` to detect it from
the URL path.
opts: Transport-specific options forwarded to the transport factory.

Returns:
A transport callable for strands MCPClient.

Raises:
ValueError: If the transport type is unsupported for an HTTP URL.
"""
opts = opts or {}
effective = transport or _detect_transport(url)
if effective == "streamable-http":
return streamable_http_transport(url, **opts)
if effective == "sse":
return sse_transport(url, **opts)
raise ValueError(
f"HTTP-based connection requires 'sse' or 'streamable-http' transport, got: {effective}.\n"
"Use command=list[str] for a stdio subprocess server."
)


def _detect_transport(url: str) -> str:
"""Auto-detect transport type from URL.

Default: streamable-http (the modern MCP transport).

Args:
url: The MCP server URL.

Returns:
The detected transport type string.
"""
path = urlparse(url).path.rstrip("/")
if path.endswith("/sse") or path == "/sse":
return "sse"
return "streamable-http"
else:
# Detect from the URL path when not given explicitly: /sse selects SSE,
# anything else selects streamable-http (the modern MCP transport).
effective = transport
if effective is None:
path = urlparse(url).path.rstrip("/")
effective = "sse" if path.endswith("/sse") else "streamable-http"

if effective == "streamable-http":
transport_callable = streamable_http_transport(url, **opts)
elif effective == "sse":
transport_callable = sse_transport(url, **opts)
else:
raise ValueError(
f"HTTP-based connection requires 'sse' or 'streamable-http' "
f"transport, got: {effective}.\n"
"Use command=list[str] for a stdio subprocess server."
)

return _MCPClient(transport_callable=transport_callable, **kwargs)
8 changes: 4 additions & 4 deletions src/strands_compose/mcp/transports.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,13 +133,13 @@ def streamable_http_transport(
"""Create a streamable HTTP transport callable.

For full control (auth, timeouts, custom TLS, etc.), pass a pre-configured
``httpx.AsyncClient`` via ``http_client``. When ``http_client`` is provided,
``httpx2.AsyncClient`` via ``http_client``. When ``http_client`` is provided,
``headers`` is ignored (configure headers on the client directly).

Args:
url: HTTP endpoint URL (e.g., "http://localhost:8000/mcp").
headers: Optional HTTP headers. Ignored when ``http_client`` is provided.
http_client: Optional pre-configured ``httpx.AsyncClient``.
http_client: Optional pre-configured ``httpx2.AsyncClient``.
terminate_on_close: Send DELETE to close session (default: True).

Returns:
Expand All @@ -165,9 +165,9 @@ def factory() -> Any:
terminate_on_close=captured_terminate_on_close,
)
if captured_headers:
import httpx
import httpx2

client = httpx.AsyncClient(headers=captured_headers)
client = httpx2.AsyncClient(headers=captured_headers)
return streamable_http_client(
url=url,
http_client=client,
Expand Down
Loading
Loading