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
10 changes: 5 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"strands-agents>=1.32,<1.34",
"strands-agents>=1.32,<1.35",
"pydantic>=2.12.5",
"pyyaml>=6.0.0",
"mcp>=1.24.0",
Expand All @@ -35,13 +35,13 @@ agentcore-memory = [
"bedrock-agentcore>=1.4.0",
]
ollama = [
"strands-agents[ollama]>=1.32,<1.34",
"strands-agents[ollama]>=1.32,<1.35",
]
openai = [
"strands-agents[openai]>=1.32,<1.34",
"strands-agents[openai]>=1.32,<1.35",
]
gemini = [
"strands-agents[gemini]>=1.32,<1.34",
"strands-agents[gemini]>=1.32,<1.35",
]

[project.urls]
Expand All @@ -53,7 +53,7 @@ Changelog = "https://github.com/strands-compose/sdk-python/blob/main/CHANGELOG.m

[dependency-groups]
dev = [
"ty==0.0.24",
"ty>=0.0.29",
"bandit>=1.9.2",
"coverage>=7.12.0",
"pytest-asyncio>=1.2.0",
Expand Down
60 changes: 22 additions & 38 deletions src/strands_compose/mcp/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def create_mcp_client(
Exactly one of server, url, or command must be provided.

Args:
server: A managed MCPServer instance (connects via its URL or stdio).
server: A managed MCPServer instance (connects via its URL).
url: External MCP server URL (for SSE or streamable-http).
command: Command to start an MCP server subprocess (stdio transport).
transport: Override transport type ("stdio", "sse", "streamable-http").
Expand Down Expand Up @@ -75,12 +75,12 @@ def create_mcp_client(
opts = transport_options or {}

if server is not None:
transport_callable = _transport_for_server(server, transport, opts)
transport_callable = _transport_for_http(server.url, transport, opts, allow_stdio=False)
elif url is not None:
transport_callable = _transport_for_url(url, transport, opts)
transport_callable = _transport_for_http(url, transport, opts, allow_stdio=True)
else:
# command is guaranteed non-None by the modes == 1 check above.
transport_callable = stdio_transport(command, **opts) # type: ignore[arg-type]
transport_callable = stdio_transport(command, **opts) # ty: ignore

return _make_strands_client(transport_callable=transport_callable, **kwargs)

Expand All @@ -99,57 +99,41 @@ def _make_strands_client(**kwargs: Any) -> MCPClient:
return _MCPClient(**kwargs)


def _transport_for_server(
server: MCPServer, transport: str | None, opts: dict[str, Any] | None = None
def _transport_for_http(
url: str,
transport: str | None,
opts: dict[str, Any] | None = None,
*,
allow_stdio: bool = True,
) -> Any:
"""Build transport callable for a managed MCPServer.

Args:
server: The managed MCPServer instance.
transport: Optional transport override.
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 managed servers.
"""
opts = opts or {}
effective = transport or "streamable-http"
if effective == "streamable-http":
return streamable_http_transport(server.url, **opts)
if effective == "sse":
return sse_transport(server.url, **opts)
if effective == "stdio":
raise ValueError(
"stdio transport not supported for managed servers. Use url or command instead."
)
raise ValueError(f"Unknown transport: {effective}")


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

Args:
url: The external MCP server URL.
transport: Optional transport override.
url: The MCP server URL.
transport: Optional transport override. Auto-detected from URL when omitted.
opts: Transport-specific options forwarded to the transport factory.
allow_stdio: When False, raises ValueError if stdio is requested.
Set to False for managed servers where stdio makes no sense.

Returns:
A transport callable for strands MCPClient.

Raises:
ValueError: If the transport type is unsupported for URL connections.
ValueError: If the transport type is unsupported or stdio is requested
when allow_stdio is False.
"""
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)
if effective == "stdio" and not allow_stdio:
raise ValueError(
"stdio transport not supported for managed servers. Use url or command instead."
)
raise ValueError(
f"URL-based connection requires 'sse' or 'streamable-http' transport, got: {effective}."
f"HTTP-based connection requires 'sse' or 'streamable-http' transport, got: {effective}."
)


Expand Down
2 changes: 1 addition & 1 deletion src/strands_compose/mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ def start(self) -> None:

def _target() -> None:
try:
asyncio.run(self._uvicorn_server.serve()) # type: ignore[union-attr]
asyncio.run(self._uvicorn_server.serve()) # ty: ignore
except BaseException as exc:
self._error = exc
self._ready.set()
Expand Down
6 changes: 3 additions & 3 deletions tests/unit/config/loaders/test_helpers_extended.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ def test_delegate_connections_updated(self):
)
orch: dict = raw["orchestrations"]["main"]
assert orch["entry_name"] == "Parent_Agent"
conns: list = orch["connections"] # type: ignore[assignment]
conns: list = orch["connections"] # ty: ignore
assert conns[0]["agent"] == "Child_Agent"

def test_swarm_refs_updated(self):
Expand Down Expand Up @@ -174,8 +174,8 @@ def test_graph_refs_updated(self):
orch = raw["orchestrations"]["main"]
assert orch["entry_name"] == "Node_A"
edges = orch["edges"]
assert edges[0]["from"] == "Node_A" # type: ignore[index]
assert edges[0]["to"] == "Node_B" # type: ignore[index]
assert edges[0]["from"] == "Node_A" # ty: ignore
assert edges[0]["to"] == "Node_B" # ty: ignore

def test_non_dict_agent_def_skipped(self):
raw: dict = {"agents": {"a": "not-a-dict"}}
Expand Down
18 changes: 9 additions & 9 deletions tests/unit/config/resolvers/orchestrations/test_builders.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ def test_creates_graph_with_nodes_and_edges(self, mock_builder_cls: MagicMock) -
nodes: dict = {"a1": a1, "a2": a2}
config = GraphOrchestrationDef(
entry_name="a1",
edges=[GraphEdgeDef(from_agent="a1", to_agent="a2")], # type: ignore[call-arg]
edges=[GraphEdgeDef(from_agent="a1", to_agent="a2")], # ty: ignore
)

build_graph("test_graph", config, nodes, "a1")
Expand All @@ -200,7 +200,7 @@ def test_graph_accepts_orchestration_node(self, mock_builder_cls: MagicMock) ->
config = GraphOrchestrationDef(
entry_name="agent1",
edges=[
GraphEdgeDef(from_agent="agent1", to_agent="nested_swarm"), # type: ignore[call-arg]
GraphEdgeDef(from_agent="agent1", to_agent="nested_swarm"), # ty: ignore
],
)

Expand All @@ -226,7 +226,7 @@ def test_unknown_config_type_raises_configuration_error(self) -> None:
configs = {"bad": unknown_cfg}

with pytest.raises(ConfigurationError, match="Unknown orchestration config type"):
OrchestrationBuilder(configs, {"a1": a1}, {}, {}, {}).build_all() # type: ignore[arg-type]
OrchestrationBuilder(configs, {"a1": a1}, {}, {}, {}).build_all() # ty: ignore


class TestOrchestrationBuilder:
Expand All @@ -249,7 +249,7 @@ def test_delegate_returns_new_agent(self, mock_build: MagicMock) -> None:
),
}

built = OrchestrationBuilder(configs, agents, agent_defs, {}, {}).build_all() # type: ignore[arg-type]
built = OrchestrationBuilder(configs, agents, agent_defs, {}, {}).build_all() # ty: ignore

assert built["orch"] is new_agent
assert built["orch"] is not original
Expand All @@ -265,7 +265,7 @@ def test_builds_swarm_in_topological_order(self, mock_swarm_cls: MagicMock) -> N
"my_swarm": SwarmOrchestrationDef(entry_name="a1", agents=["a1", "a2"]),
}

built = OrchestrationBuilder(configs, agents, {}, {}, {}).build_all() # type: ignore[arg-type]
built = OrchestrationBuilder(configs, agents, {}, {}, {}).build_all() # ty: ignore

assert "my_swarm" in built
mock_swarm_cls.assert_called_once()
Expand All @@ -289,12 +289,12 @@ def test_node_pool_grows_for_downstream_orchestrations(
"pipeline": GraphOrchestrationDef(
entry_name="research_swarm",
edges=[
GraphEdgeDef(from_agent="research_swarm", to_agent="reviewer"), # type: ignore[call-arg]
GraphEdgeDef(from_agent="research_swarm", to_agent="reviewer"), # ty: ignore
],
),
}

built = OrchestrationBuilder(configs, agents, {}, {}, {}).build_all() # type: ignore[arg-type]
built = OrchestrationBuilder(configs, agents, {}, {}, {}).build_all() # ty: ignore

assert "research_swarm" in built
assert "pipeline" in built
Expand All @@ -312,7 +312,7 @@ def test_session_manager_forwarded_to_swarm(self, mock_swarm_cls: MagicMock) ->
"my_swarm": SwarmOrchestrationDef(entry_name="a1", agents=["a1", "a2"]),
}

OrchestrationBuilder(configs, agents, {}, {}, {}, sm).build_all() # type: ignore[arg-type]
OrchestrationBuilder(configs, agents, {}, {}, {}, sm).build_all() # ty: ignore

assert mock_swarm_cls.call_args.kwargs["session_manager"] is sm

Expand All @@ -325,4 +325,4 @@ def test_invalid_entry_name_raises_configuration_error(self) -> None:
}

with pytest.raises(ConfigurationError, match="entry_name 'nonexistent' is not defined"):
OrchestrationBuilder(configs, agents, {}, {}, {}).build_all() # type: ignore[arg-type]
OrchestrationBuilder(configs, agents, {}, {}, {}).build_all() # ty: ignore
22 changes: 11 additions & 11 deletions tests/unit/config/resolvers/orchestrations/test_planner.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ def test_graph_collects_edge_endpoints(self) -> None:
config = GraphOrchestrationDef(
entry_name="a",
edges=[
GraphEdgeDef(from_agent="a", to_agent="b"), # type: ignore[call-arg]
GraphEdgeDef(from_agent="b", to_agent="c"), # type: ignore[call-arg]
GraphEdgeDef(from_agent="a", to_agent="b"), # ty: ignore
GraphEdgeDef(from_agent="b", to_agent="c"), # ty: ignore
],
)
assert collect_node_refs(config) == {"a", "b", "c"}
Expand All @@ -66,7 +66,7 @@ def test_independent_orchestrations_both_present(self) -> None:
"orch_a": SwarmOrchestrationDef(entry_name="a1", agents=["a1", "a2"]),
"orch_b": SwarmOrchestrationDef(entry_name="b1", agents=["b1", "b2"]),
}
order = topological_sort(configs) # type: ignore[arg-type]
order = topological_sort(configs) # ty: ignore
assert set(order) == {"orch_a", "orch_b"}

def test_dependency_appears_before_dependent(self) -> None:
Expand All @@ -76,40 +76,40 @@ def test_dependency_appears_before_dependent(self) -> None:
"orch_b": GraphOrchestrationDef(
entry_name="orch_a",
edges=[
GraphEdgeDef(from_agent="orch_a", to_agent="reviewer"), # type: ignore[call-arg]
GraphEdgeDef(from_agent="orch_a", to_agent="reviewer"), # ty: ignore
],
),
}
order = topological_sort(configs) # type: ignore[arg-type]
order = topological_sort(configs) # ty: ignore
assert order.index("orch_a") < order.index("orch_b")

def test_circular_dependency_raises_configuration_error(self) -> None:
"""Mutual references between orchestrations raise ConfigurationError."""
configs = {
"orch_a": GraphOrchestrationDef(
entry_name="orch_b",
edges=[GraphEdgeDef(from_agent="orch_b", to_agent="x")], # type: ignore[call-arg]
edges=[GraphEdgeDef(from_agent="orch_b", to_agent="x")], # ty: ignore
),
"orch_b": GraphOrchestrationDef(
entry_name="orch_a",
edges=[GraphEdgeDef(from_agent="orch_a", to_agent="y")], # type: ignore[call-arg]
edges=[GraphEdgeDef(from_agent="orch_a", to_agent="y")], # ty: ignore
),
}
with pytest.raises(ConfigurationError, match="Circular dependency"):
topological_sort(configs) # type: ignore[arg-type]
topological_sort(configs) # ty: ignore

def test_three_level_chain_correct_order(self) -> None:
"""A -> B -> C chain: C built first, then B, then A."""
configs = {
"A": GraphOrchestrationDef(
entry_name="B",
edges=[GraphEdgeDef(from_agent="B", to_agent="agent1")], # type: ignore[call-arg]
edges=[GraphEdgeDef(from_agent="B", to_agent="agent1")], # ty: ignore
),
"B": GraphOrchestrationDef(
entry_name="C",
edges=[GraphEdgeDef(from_agent="C", to_agent="agent2")], # type: ignore[call-arg]
edges=[GraphEdgeDef(from_agent="C", to_agent="agent2")], # ty: ignore
),
"C": SwarmOrchestrationDef(entry_name="agent3", agents=["agent3", "agent4"]),
}
order = topological_sort(configs) # type: ignore[arg-type]
order = topological_sort(configs) # ty: ignore
assert order.index("C") < order.index("B") < order.index("A")
Loading
Loading