Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
21 changes: 14 additions & 7 deletions strix/agents/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -599,18 +599,21 @@ def _finish_tool_use_behavior(
list_sitemap,
view_sitemap_entry,
scope_rules,
list_mcps,
search_mcp_tools,
get_mcp_tool_schema,
describe_mcp,
call_mcp,
view_agent_graph,
send_message_to_agent,
wait_for_agents,
create_agent,
stop_agent,
)

_MCP_TOOLS: tuple[Tool, ...] = (
list_mcps,
search_mcp_tools,
get_mcp_tool_schema,
describe_mcp,
call_mcp,
)
Comment thread
tntrenjin marked this conversation as resolved.
Resolved


# Extra tools registered for scan agents. Mirrors
# ``strix.runtime.backends.register_backend``: register before the first
Expand Down Expand Up @@ -694,14 +697,18 @@ def build_strix_agent(
system_prompt_context=system_prompt_context,
)

base_tools: list[Tool] = list(_BASE_TOOLS)
if system_prompt_context and system_prompt_context.get("mcp_available"):
base_tools.extend(_MCP_TOOLS)

agent_tools = [*_EXTRA_TOOLS, *(extra_tools or [])]
if interactive:
# Yielding to the user is only meaningful when one is attached.
agent_tools.append(respond_to_user)
if is_root:
tools: list[Tool] = [*_BASE_TOOLS, *agent_tools, finish_scan]
tools: list[Tool] = [*base_tools, *agent_tools, finish_scan]
else:
tools = [*_BASE_TOOLS, *agent_tools, agent_finish]
tools = [*base_tools, *agent_tools, agent_finish]
_ensure_unique_tool_names(tools)
tools = [
_with_bounded_result(_with_strictness(_with_coerced_arguments(tool), strict_tool_schemas))
Expand Down
36 changes: 36 additions & 0 deletions tests/test_agent_factory_shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,39 @@ def test_function_tools_are_result_bounded() -> None:
by_name = {t.name: t for t in agent.tools}

assert getattr(by_name["think"], "_strix_bounded", False) is True


_MCP_TOOL_NAMES = {
"list_mcps",
"search_mcp_tools",
"get_mcp_tool_schema",
"describe_mcp",
"call_mcp",
}


def _tool_names(*, is_root: bool, **kwargs: Any) -> set[str]:
return {t.name for t in factory.build_strix_agent(is_root=is_root, **kwargs).tools}


@pytest.mark.parametrize("is_root", [True, False])
@pytest.mark.parametrize(
"system_prompt_context",
[None, {}, {"mcp_available": False}],
)
def test_mcp_tools_absent_without_mcp_configured(
is_root: bool, system_prompt_context: dict[str, Any] | None
) -> None:
names = _tool_names(is_root=is_root, system_prompt_context=system_prompt_context)
assert _MCP_TOOL_NAMES.isdisjoint(names)


@pytest.mark.parametrize("is_root", [True, False])
def test_mcp_tools_present_when_mcp_is_configured(is_root: bool) -> None:
names = _tool_names(is_root=is_root, system_prompt_context={"mcp_available": True})
assert names >= _MCP_TOOL_NAMES


def test_mcp_tools_do_not_leak_into_a_later_agent_without_mcp() -> None:
_tool_names(is_root=True, system_prompt_context={"mcp_available": True})
assert _MCP_TOOL_NAMES.isdisjoint(_tool_names(is_root=True))
9 changes: 6 additions & 3 deletions tests/test_mcp_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -890,8 +890,9 @@ async def test_call_mcp_flags_an_errored_result_failed_for_the_tui() -> None:
def test_agent_carries_exactly_the_generic_mcp_tools_regardless_of_connections() -> None:
"""No matter how many MCP connections a run makes, an agent's tool list gains
exactly the generic MCP tools and never a per-connection provider tool."""
root = factory.build_strix_agent(is_root=True)
child = factory.build_strix_agent(is_root=False)
context = {"mcp_available": True}
root = factory.build_strix_agent(is_root=True, system_prompt_context=context)
child = factory.build_strix_agent(is_root=False, system_prompt_context=context)

root_names = [t.name for t in root.tools]
child_names = [t.name for t in child.tools]
Expand Down Expand Up @@ -919,7 +920,9 @@ def test_agent_carries_exactly_the_generic_mcp_tools_regardless_of_connections()
# The tool list does not grow with connection count: it is the same set of
# names whether or not any connection exists, because connections never
# contribute tools.
assert root_names == [t.name for t in factory.build_strix_agent(is_root=True).tools]
assert root_names == [
t.name for t in factory.build_strix_agent(is_root=True, system_prompt_context=context).tools
]


# --- prompt guidance replaces the old per-connection inventory ---------------
Expand Down
11 changes: 10 additions & 1 deletion tests/test_runner_root_prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,12 @@ def _build_strix_agent(**kwargs: Any) -> object:
return object()

monkeypatch.setattr(runner, "build_strix_agent", _build_strix_agent)
monkeypatch.setattr(runner, "make_child_factory", lambda **_kwargs: lambda **_k: object())

def _make_child_factory(**kwargs: Any) -> Any:
captured["child_factory_kwargs"] = kwargs
return lambda **_k: object()

monkeypatch.setattr(runner, "make_child_factory", _make_child_factory)
monkeypatch.setattr(runner, "open_agent_session", lambda _root_id, _db: object())

async def _raise_rate_limit(*_args: Any, **kwargs: Any) -> None:
Expand Down Expand Up @@ -225,6 +230,8 @@ async def test_mcp_available_flag_set_when_a_connection_attaches(
assert kwargs["system_prompt_context"]["mcp_connections"] == [
{"name": "fs", "purpose": "local files", "tool_count": 0}
]
child_context = captured["child_factory_kwargs"]["system_prompt_context"]
assert child_context["mcp_available"] is True


@pytest.mark.asyncio
Expand All @@ -249,6 +256,8 @@ async def test_mcp_available_flag_absent_without_a_connection(
kwargs = captured["kwargs"]
assert "mcp_available" not in kwargs["system_prompt_context"]
assert "mcp_connections" not in kwargs["system_prompt_context"]
child_context = captured["child_factory_kwargs"]["system_prompt_context"]
assert "mcp_available" not in child_context


@pytest.mark.asyncio
Expand Down