diff --git a/docs/integrations/mcp.mdx b/docs/integrations/mcp.mdx index 6b9945c9..bf24ee3f 100644 --- a/docs/integrations/mcp.mdx +++ b/docs/integrations/mcp.mdx @@ -8,6 +8,7 @@ Strix can connect to [Model Context Protocol (MCP)](https://modelcontextprotocol A few things it pays off for: - **A database server.** The agent can read the schema and access policies and see tables left readable without them, rather than guessing from responses. +- **A web search / research server (e.g. [Parallel Search](https://search.parallel.ai/mcp)).** The agent can look up current CVEs, security advisories, and documentation in real time without needing a Perplexity API key. - **A hosting or infrastructure server.** Deployments, domains and environment variable names tell it what is really running, so it tests what exists instead of what it discovered by crawling. - **An issue tracker.** Known and accepted risks stop the agent re-reporting findings you already triaged. - **A logging server.** Reading logs lets it confirm an exploit attempt actually landed instead of inferring it from a status code. @@ -22,7 +23,7 @@ Create the directory if it does not exist, then write the file: mkdir -p ~/.strix ``` -Paste the servers you want into `~/.strix/mcp-servers.json`. The example below shows one of each transport: a local filesystem server over `stdio` and a remote GitHub server over `http` with a bearer token: +Paste the servers you want into `~/.strix/mcp-servers.json`. The example below shows a local filesystem server over `stdio`, a keyless remote web search server (Parallel Search) over `http`, and a remote GitHub server with a bearer token: ```json [ @@ -32,6 +33,12 @@ Paste the servers you want into `~/.strix/mcp-servers.json`. The example below s "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/project"] }, + { + "name": "parallel_search", + "transport": "http", + "url": "https://search.parallel.ai/mcp", + "notes": "Web search for CVEs, advisories, and documentation via Parallel Search" + }, { "name": "github", "transport": "http", diff --git a/tests/test_mcp_client.py b/tests/test_mcp_client.py index c75efa4d..f5f9c7d9 100644 --- a/tests/test_mcp_client.py +++ b/tests/test_mcp_client.py @@ -754,6 +754,36 @@ def test_loader_skips_bad_entry_but_keeps_good_ones(tmp_path: Path) -> None: assert [c.name for c in configs] == ["local_fs"] +def test_loader_parses_parallel_search_http_config(tmp_path: Path) -> None: + config_file = tmp_path / "mcp-servers.json" + config_file.write_text( + json.dumps( + [ + { + "name": "parallel_search", + "transport": "http", + "url": "https://search.parallel.ai/mcp", + "notes": ( + "Web search for CVEs, advisories, and documentation via Parallel Search" + ), + } + ] + ), + encoding="utf-8", + ) + + configs = load_user_mcp_configs(config_file) + + assert len(configs) == 1 + assert configs[0].name == "parallel_search" + assert configs[0].transport == "http" + assert configs[0].url == "https://search.parallel.ai/mcp" + assert configs[0].auth is None + assert configs[0].notes == ( + "Web search for CVEs, advisories, and documentation via Parallel Search" + ) + + def test_loader_returns_empty_when_file_absent(tmp_path: Path) -> None: assert load_user_mcp_configs(tmp_path / "does-not-exist.json") == [] @@ -1599,3 +1629,49 @@ def test_registry_statuses_report_the_live_dead_flag_and_provider() -> None: assert statuses["a"].provider == "supabase" assert statuses["b"].dead is True assert statuses["b"].provider is None + + +@pytest.mark.asyncio +async def test_parallel_search_mcp_discovery_and_invocation() -> None: + search_tool = MCPTool( + name="search", + description="Search the web for cybersecurity advisories, CVEs, and documentation", + inputSchema={ + "type": "object", + "properties": {"query": {"type": "string"}}, + "required": ["query"], + }, + ) + server = FakeMCPServer("parallel_search", [search_tool]) + session = SupervisedMcpSession.adopt(server, name="parallel_search") + registry = McpRegistry() + registry.add( + name="parallel_search", + session=session, + tool_count=1, + purpose="Web search for CVEs, advisories, and documentation via Parallel Search", + ) + + listed = await list_mcps.on_invoke_tool(_ctx(registry), "{}") + assert any(conn["name"] == "parallel_search" for conn in listed["connections"]) + + described = await describe_mcp.on_invoke_tool( + _ctx(registry), json.dumps({"connection": "parallel_search"}) + ) + assert "MCP connection 'parallel_search' offers 1 tool(s):" in described + assert "search: Search the web" in described + + out = await call_mcp.on_invoke_tool( + _ctx(registry), + json.dumps( + { + "connection": "parallel_search", + "tool": "search", + "arguments": {"query": "CVE-2024-1234 exploit PoC"}, + } + ), + ) + assert out == {"type": "text", "text": "routed:search"} + assert server.calls == [("search", {"query": "CVE-2024-1234 exploit PoC"})] + + await session.aclose()