diff --git a/strix/agents/prompts/system_prompt.jinja b/strix/agents/prompts/system_prompt.jinja index 9ea8121a..b1a574bc 100644 --- a/strix/agents/prompts/system_prompt.jinja +++ b/strix/agents/prompts/system_prompt.jinja @@ -78,6 +78,13 @@ AUTHORIZED TARGETS: {% if system_prompt_context and system_prompt_context.mcp_available %} MCP CONNECTIONS (available this run): - The user connected one or more MCP (Model Context Protocol) servers — external tool providers you can reach on demand. Their individual tools do NOT appear in your tool list; three dispatch tools are the only way in. +{% if system_prompt_context.mcp_connections %} +- Connected this run (call describe_mcp on one to see its tools): +{% for connection in system_prompt_context.mcp_connections %} + - {{ connection.name }} ({{ connection.tool_count }} tools){% if connection.purpose %}: {{ connection.purpose }}{% endif %} +{% endfor %} +{% endif %} +- Reach for a connection whenever the target itself cannot give you information a connection could: its database schema and access policies, real deployment or infrastructure configuration, known issues or prior findings, or server logs. In those cases call list_mcps early to see what is available, and prefer a connection's authoritative data over inferring from the target's responses. Do not wait to be told a connection exists. 1. Call list_mcps() to discover the available connections. 2. Call describe_mcp(connection="") to inspect one connection's tools, each with its name, description, and JSON input schema. 3. Call call_mcp(connection="", tool="", arguments={...}) to run one, passing an arguments object that matches the schema (omit arguments for a tool that takes none). diff --git a/strix/core/runner.py b/strix/core/runner.py index fd347f24..6b79b91a 100644 --- a/strix/core/runner.py +++ b/strix/core/runner.py @@ -370,13 +370,21 @@ async def run_strix_scan( _record_mcp_connections(connections) if connections: report(_mcp_startup_summary(connections)) - # Flag that MCP is reachable so both the root context and the - # child factory's context (both derive from scope_context) - # render the static three-tool guidance. Set only when a - # connection exists, so a run with no MCP leaves the prompt - # context unchanged. The connections themselves are discovered - # at run time via list_mcps, not listed in the prompt. + # Name the connected servers in the prompt so every agent + # (root and children, both deriving from scope_context) sees + # what is available at the start; they can still re-list or + # inspect them at run time via list_mcps / describe_mcp. Set + # only when a connection exists, so a run with no MCP leaves + # the prompt context unchanged. scope_context["mcp_available"] = bool(mcp_registry) + scope_context["mcp_connections"] = [ + { + "name": summary.name, + "purpose": summary.purpose, + "tool_count": summary.tool_count, + } + for summary in mcp_registry.summaries() + ] except Exception: logger.exception("Failed to connect user MCP servers; continuing without them") diff --git a/tests/test_mcp_client.py b/tests/test_mcp_client.py index 7b7919e9..069b9604 100644 --- a/tests/test_mcp_client.py +++ b/tests/test_mcp_client.py @@ -648,21 +648,36 @@ def test_prompt_has_no_mcp_section_without_availability() -> None: assert "MCP CONNECTIONS" not in render_system_prompt(system_prompt_context={}) -def test_prompt_no_longer_renders_a_per_connection_inventory() -> None: - """The old inventory loop read ``mcp_connections`` and listed each connection - with its purpose. That data no longer drives the prompt; only the boolean - ``mcp_available`` does, so a legacy ``mcp_connections`` list renders nothing.""" +def test_prompt_renders_named_connection_inventory() -> None: + """With mcp_available set, the prompt names each connected server (name, tool + count, purpose) so every agent sees what is available at the start, alongside + the three dispatch tools for re-listing and inspecting them at run time.""" prompt = render_system_prompt( system_prompt_context={ + "mcp_available": True, "mcp_connections": [ - {"name": "secret-conn", "purpose": "should not appear", "tool_count": 3} - ] + {"name": "supabase", "purpose": "read the app's schema", "tool_count": 13} + ], + } + ) + + assert "MCP CONNECTIONS" in prompt + assert "supabase" in prompt + assert "13 tools" in prompt + assert "read the app's schema" in prompt + + +def test_prompt_inventory_is_gated_on_availability() -> None: + """The block is gated on ``mcp_available``; an ``mcp_connections`` payload + without it renders nothing, so a stale or spoofed list cannot leak names.""" + prompt = render_system_prompt( + system_prompt_context={ + "mcp_connections": [{"name": "secret-conn", "purpose": "x", "tool_count": 3}] } ) assert "MCP CONNECTIONS" not in prompt assert "secret-conn" not in prompt - assert "should not appear" not in prompt # --- loader ------------------------------------------------------------------ diff --git a/tests/test_runner_root_prompt.py b/tests/test_runner_root_prompt.py index e33bf254..1bea19c2 100644 --- a/tests/test_runner_root_prompt.py +++ b/tests/test_runner_root_prompt.py @@ -187,9 +187,10 @@ async def test_mcp_available_flag_set_when_a_connection_attaches( monkeypatch: pytest.MonkeyPatch, tmp_path: Any, ) -> None: - """When at least one MCP connection attaches, the runner sets the boolean - ``mcp_available`` (not a per-connection inventory) into the scan context that - reaches every agent.""" + """When at least one MCP connection attaches, the runner sets ``mcp_available`` + plus a named ``mcp_connections`` inventory into the scan context that reaches + every agent, so each agent sees which connections exist at the start while + still being able to re-list them at run time via list_mcps.""" scope_context: dict[str, Any] = {"scope": "built-in"} captured = _patch_engine_scaffold(monkeypatch, tmp_path, scope_context) @@ -217,8 +218,10 @@ async def test_mcp_available_flag_set_when_a_connection_attaches( kwargs = captured["kwargs"] assert kwargs["system_prompt_context"]["mcp_available"] is True - # The old per-connection inventory key is gone entirely. - assert "mcp_connections" not in kwargs["system_prompt_context"] + # The named inventory names each connected server for the prompt. + assert kwargs["system_prompt_context"]["mcp_connections"] == [ + {"name": "fs", "purpose": "local files", "tool_count": 2} + ] @pytest.mark.asyncio