name the run's MCP connections in the prompt again, alongside list_mcps

This commit is contained in:
Jonathan Singer 2026-08-26 17:41:08 -04:00
parent c220e1fd31
commit eaf4853415
4 changed files with 51 additions and 18 deletions

View file

@ -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="<name>") to inspect one connection's tools, each with its name, description, and JSON input schema.
3. Call call_mcp(connection="<name>", tool="<tool>", arguments={...}) to run one, passing an arguments object that matches the schema (omit arguments for a tool that takes none).

View file

@ -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")

View file

@ -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 ------------------------------------------------------------------

View file

@ -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