From a80281cb491c56641dbd2f32bf442092a2784d7b Mon Sep 17 00:00:00 2001 From: Matt Harris Date: Thu, 28 May 2026 16:43:41 -0400 Subject: [PATCH 1/5] feat(mcp): add Parallel Web Search to MCP server registry + example config Adds Parallel Web Systems' hosted Search MCP server alongside the existing Context7, Brave, and Exa entries. - massgen/mcp_tools/server_registry.py: new `parallel_search` entry using `type: streamable-http` and `url: https://search.parallel.ai/mcp`. The server accepts unauthenticated requests by default, so the entry is registered with `requires_api_key: False` and an `optional_api_key_env_var: PARALLEL_API_KEY` for users who want higher rate limits via the `Authorization: Bearer ...` header. - massgen/configs/tools/web-search/parallel_search_example.yaml: full example config modelled on `exa_search_example.yaml`, with a system message that explains Parallel's `objective + search_queries` pattern and `web_search`/`web_fetch` tools, plus a commented-out `headers` block for opting into the API key. - Module docstring updated to list `parallel_search`. Docs: - https://docs.parallel.ai/integrations/mcp/search-mcp - https://docs.parallel.ai/api-reference/search/search - https://docs.parallel.ai/search/best-practices Tested with claude-sonnet-4-6 backend via the new example config. --- .../web-search/parallel_search_example.yaml | 80 +++++++++++++++++++ massgen/mcp_tools/server_registry.py | 26 ++++++ 2 files changed, 106 insertions(+) create mode 100644 massgen/configs/tools/web-search/parallel_search_example.yaml diff --git a/massgen/configs/tools/web-search/parallel_search_example.yaml b/massgen/configs/tools/web-search/parallel_search_example.yaml new file mode 100644 index 000000000..fd9570796 --- /dev/null +++ b/massgen/configs/tools/web-search/parallel_search_example.yaml @@ -0,0 +1,80 @@ +# MassGen Configuration: Parallel Web Search +# This configuration demonstrates Parallel's hosted Search MCP server for +# LLM-optimized web search and URL extraction. +# +# Prerequisites: +# - None required for free anonymous use (light/exploratory traffic). +# - For higher rate limits or production use: +# 1. Get an API key at https://platform.parallel.ai +# 2. Set PARALLEL_API_KEY in your .env file +# 3. Uncomment the `headers` block below +# +# Usage: +# uv run massgen --config @examples/tools/web-search/parallel_search_example.yaml "Research the latest advances in multi-agent AI systems" +# +# Features: +# - web_search: pass an `objective` (natural-language goal) plus 2-3 short +# keyword `search_queries`; returns ranked URLs with LLM-optimized +# excerpts in one call (replaces multi-step keyword search loops) +# - web_fetch: pass a list of URLs (and optional `objective`); returns the +# most relevant markdown content from each page, bounded for token +# efficiency +# - Excerpts are pre-ranked and compressed for reasoning utility, so they +# can be fed directly into the model context with minimal post-processing + +agents: + - id: "parallel_research_agent" + backend: + type: "claude_code" + model: "claude-sonnet-4-6" + cwd: "workspace" + mcp_servers: + - name: "parallel_search" + type: "streamable-http" + url: "https://search.parallel.ai/mcp" + # Uncomment to enable higher rate limits with a Parallel API key: + # headers: + # Authorization: "Bearer ${PARALLEL_API_KEY}" + security: + level: "moderate" + + system_message: | + You are a research assistant with access to Parallel Web Systems' + LLM-optimized search. + + The Parallel Search MCP exposes two tools: + + - web_search(objective, search_queries[, ...]): + * `objective` is a natural-language description of what you're + trying to answer (max 5000 chars). Include enough context to + make the search self-contained. + * `search_queries` is a list of 2-3 short keyword queries, each + 3-6 words. Vary entity names, synonyms, and angles — keep them + diverse. Do NOT write sentences or use `site:` operators here. + Maximum of 5 queries per call. + * Optional `mode`: "basic" for low-latency lookups, "advanced" + (default) for higher-quality multi-step retrieval on harder + questions. + * Returns ranked URLs with markdown excerpts that are already + relevance-ranked and compressed for direct LLM consumption. + + - web_fetch(urls[, objective]): + * Pass an array of URLs (up to 20) and an optional `objective` to + focus the excerpts. + * Returns markdown content (or full page bodies if requested) for + each URL. + + Best practices: + - Provide BOTH `objective` and `search_queries` for best results — one + well-formed call typically replaces several keyword searches. + - Use web_search for discovery; use web_fetch only when you already + know the URL or after a search step has surfaced one. + - Cite every claim with the source URL returned by the API; never + invent URLs. + + Provide comprehensive, well-sourced responses based on the search + results. + +ui: + display_type: "textual_terminal" + logging_enabled: true diff --git a/massgen/mcp_tools/server_registry.py b/massgen/mcp_tools/server_registry.py index 562a828f8..e35488b63 100644 --- a/massgen/mcp_tools/server_registry.py +++ b/massgen/mcp_tools/server_registry.py @@ -9,6 +9,8 @@ - Context7: Up-to-date documentation for libraries and frameworks - Brave Search: Web search via Brave API (requires API key) - Exa Search: AI-powered web search via Exa API (requires API key) +- Parallel Search: LLM-optimized web search via Parallel API (anonymous-friendly; + optional API key for higher rate limits) """ import os @@ -68,6 +70,30 @@ "level": "moderate", }, }, + "parallel_search": { + "name": "parallel_search", + "type": "streamable-http", + "url": "https://search.parallel.ai/mcp", + "description": ( + "Parallel's hosted Search MCP server. Provides web_search " + "(objective + 2-3 keyword search_queries -> ranked excerpts) " + "and web_fetch (URL -> markdown excerpts) tools optimized for " + "LLM consumption. Works without an API key for free anonymous " + "use; set PARALLEL_API_KEY for higher rate limits." + ), + "requires_api_key": False, + "optional_api_key_env_var": "PARALLEL_API_KEY", + "notes": ( + "Free anonymous access for light/exploratory use. For higher " + "rate limits in production, get a key at https://platform.parallel.ai " + "and add 'headers: {Authorization: \"Bearer ${PARALLEL_API_KEY}\"}' " + "to your mcp_servers entry. Docs: " + "https://docs.parallel.ai/integrations/mcp/search-mcp" + ), + "security": { + "level": "moderate", + }, + }, } From 34a765489ec8172608aa856201992618a1c7889f Mon Sep 17 00:00:00 2001 From: Matt Harris Date: Thu, 28 May 2026 16:55:55 -0400 Subject: [PATCH 2/5] fix(mcp): drop signature-style tool syntax from prompt-facing text Addresses CodeRabbit findings (Major) on PR #1108: - server_registry.py: registry description no longer encodes tool invocation shape (web_search(...) / web_fetch(URL ->...)). Reworded as a natural-language description of what the server provides; the actual call schemas come from the MCP server itself. - parallel_search_example.yaml: system message dropped the web_search(objective, search_queries[, ...]) and web_fetch(urls[, objective]) headers, and now leans on the MCP-supplied schemas for argument names. Usage guidance (objective + 2-3 keyword queries, mode preset, citation discipline) is preserved in plain prose. Tools remain referenced by their conceptual function ("web search", "URL extraction") rather than by hardcoded call signatures, so the prompt stays accurate if the underlying tool schema evolves. --- .../web-search/parallel_search_example.yaml | 57 ++++++++++--------- massgen/mcp_tools/server_registry.py | 8 +-- 2 files changed, 35 insertions(+), 30 deletions(-) diff --git a/massgen/configs/tools/web-search/parallel_search_example.yaml b/massgen/configs/tools/web-search/parallel_search_example.yaml index fd9570796..3b47c9b7f 100644 --- a/massgen/configs/tools/web-search/parallel_search_example.yaml +++ b/massgen/configs/tools/web-search/parallel_search_example.yaml @@ -42,35 +42,40 @@ agents: You are a research assistant with access to Parallel Web Systems' LLM-optimized search. - The Parallel Search MCP exposes two tools: + The Parallel Search MCP exposes two tools — the exact call schemas + will be provided to you by the MCP server itself. Rely on those + schemas for argument names and types; the notes below describe how + to use the tools well, not how to call them. - - web_search(objective, search_queries[, ...]): - * `objective` is a natural-language description of what you're - trying to answer (max 5000 chars). Include enough context to - make the search self-contained. - * `search_queries` is a list of 2-3 short keyword queries, each - 3-6 words. Vary entity names, synonyms, and angles — keep them - diverse. Do NOT write sentences or use `site:` operators here. - Maximum of 5 queries per call. - * Optional `mode`: "basic" for low-latency lookups, "advanced" - (default) for higher-quality multi-step retrieval on harder - questions. - * Returns ranked URLs with markdown excerpts that are already - relevance-ranked and compressed for direct LLM consumption. + Web search + - The objective should be a natural-language description of what + you're trying to answer (up to 5000 chars). Include enough context + to make the search self-contained. + - Provide 2-3 short keyword queries, each 3-6 words. Vary entity + names, synonyms, and angles — keep them diverse. Do NOT write + sentences or use `site:` operators in the keyword queries. + Maximum of 5 queries per call. + - When the tool supports a mode preset, prefer the default + ("advanced") for higher-quality multi-step retrieval on harder + questions and the lower-latency "basic" mode for simple lookups. + - Results come back as ranked URLs with markdown excerpts that are + already relevance-ranked and compressed for direct LLM + consumption. - - web_fetch(urls[, objective]): - * Pass an array of URLs (up to 20) and an optional `objective` to - focus the excerpts. - * Returns markdown content (or full page bodies if requested) for - each URL. + URL extraction + - Provide a small batch of URLs (up to 20) and, when useful, an + objective to focus the excerpts. + - Results come back as markdown content from each URL. - Best practices: - - Provide BOTH `objective` and `search_queries` for best results — one - well-formed call typically replaces several keyword searches. - - Use web_search for discovery; use web_fetch only when you already - know the URL or after a search step has surfaced one. - - Cite every claim with the source URL returned by the API; never - invent URLs. + Best practices + - Provide BOTH an objective and keyword queries for best results — + one well-formed search typically replaces several keyword + searches. + - Use the web search tool for discovery; use the URL extraction + tool only when you already know the URL or after a search step + has surfaced one. + - Cite every claim with the source URL returned by the tools; + never invent URLs. Provide comprehensive, well-sourced responses based on the search results. diff --git a/massgen/mcp_tools/server_registry.py b/massgen/mcp_tools/server_registry.py index e35488b63..1c6ec464a 100644 --- a/massgen/mcp_tools/server_registry.py +++ b/massgen/mcp_tools/server_registry.py @@ -75,10 +75,10 @@ "type": "streamable-http", "url": "https://search.parallel.ai/mcp", "description": ( - "Parallel's hosted Search MCP server. Provides web_search " - "(objective + 2-3 keyword search_queries -> ranked excerpts) " - "and web_fetch (URL -> markdown excerpts) tools optimized for " - "LLM consumption. Works without an API key for free anonymous " + "Parallel's hosted Search MCP server. Provides LLM-optimized " + "web search and URL extraction tools that return ranked, " + "compressed markdown excerpts suitable for direct model " + "consumption. Works without an API key for free anonymous " "use; set PARALLEL_API_KEY for higher rate limits." ), "requires_api_key": False, From 961680913d54e279443ee089bbb7d0f3b46431ee Mon Sep 17 00:00:00 2001 From: Matt Harris Date: Thu, 28 May 2026 16:58:51 -0400 Subject: [PATCH 3/5] fix(mcp): inject optional API key as Bearer header for streamable-http servers Codex review identified that the new parallel_search entry advertises PARALLEL_API_KEY support but get_server_config() only special-cased context7's --api-key CLI flag. As written, parallel_search would always connect anonymously even when PARALLEL_API_KEY was set, defeating the documented higher-rate-limit path. Generalize get_server_config() so that when a registry entry declares optional_api_key_env_var and the env var is set: - stdio context7 keeps its existing --api-key flag injection (unchanged behavior; explicit branch on server_name) - streamable-http entries get a default Authorization: Bearer ${key} header (using setdefault so a registry-declared header takes precedence) - everything else is unchanged Manually verified: - PARALLEL_API_KEY unset -> no headers added to parallel_search config - PARALLEL_API_KEY set -> headers.Authorization = "Bearer " - apply_api_key_logic=False -> no injection - context7 still injects --api-key when CONTEXT7_API_KEY is set --- massgen/mcp_tools/server_registry.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/massgen/mcp_tools/server_registry.py b/massgen/mcp_tools/server_registry.py index 1c6ec464a..03f862550 100644 --- a/massgen/mcp_tools/server_registry.py +++ b/massgen/mcp_tools/server_registry.py @@ -131,13 +131,21 @@ def get_server_config(server_name: str, apply_api_key_logic: bool = True) -> dic # Deep copy to avoid modifying the registry config = deepcopy(MCP_SERVER_REGISTRY[server_name]) - # Handle optional API key for Context7 - if apply_api_key_logic and server_name == "context7": + if apply_api_key_logic: optional_key_var = config.get("optional_api_key_env_var") if optional_key_var and is_api_key_available(optional_key_var): - # Add --api-key argument with the API key value api_key_value = os.environ.get(optional_key_var) - config["args"].extend(["--api-key", api_key_value]) + + # Server-specific injection of the optional API key. + if server_name == "context7": + # Context7 takes the key as a CLI flag on its stdio command. + config["args"].extend(["--api-key", api_key_value]) + elif config.get("type") == "streamable-http": + # Hosted HTTP MCPs take the key via the Authorization + # Bearer header. Don't overwrite a header the registry + # entry already declared. + headers = config.setdefault("headers", {}) + headers.setdefault("Authorization", f"Bearer {api_key_value}") return config From 43f137d3e72334e94386bd26da83626aeab031be Mon Sep 17 00:00:00 2001 From: Matt Harris Date: Thu, 28 May 2026 17:04:20 -0400 Subject: [PATCH 4/5] fix(mcp): make parallel_search key-gated for auto-discovery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex review on the prior commit flagged a P1 behavioral regression: marking parallel_search as `requires_api_key: False` put it in the always-on auto-discovery bucket alongside context7, so every config with `auto_discover_custom_tools: true` (e.g. subagent_checklist.yaml, log_analysis.yaml, many others) silently gained an outbound web-search tool — breaking deterministic/offline assumptions of existing workflows. Realign with the Brave/Exa convention: - `requires_api_key: True`, `api_key_env_var: "PARALLEL_API_KEY"` - Add a baked `headers: {"Authorization": "Bearer ${PARALLEL_API_KEY}"}` block; the MCP transport's existing env-var substitution unpacks it at connection time (mirrors how stdio servers consume `env:` like `BRAVE_API_KEY: "${BRAVE_API_KEY}"`). - Revert the temporary streamable-http bearer-injection branch added to `get_server_config()` last commit — no longer needed and made the registry entry's `optional_api_key_env_var` look like it was opt-in when it actually gated nothing. - Description/notes reworded: anonymous use is still possible by adding the server manually to mcp_servers without the headers block (the example YAML continues to show that path). Manually verified: - PARALLEL_API_KEY unset -> auto-discovery is just [context7] (no regression vs. main) - PARALLEL_API_KEY set -> auto-discovery is [context7, parallel_search] with the headers template ready for MCP substitution - Missing-key reporting now lists parallel_search alongside brave_search and exa_search when their env vars are unset --- massgen/mcp_tools/server_registry.py | 37 +++++++++++++--------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/massgen/mcp_tools/server_registry.py b/massgen/mcp_tools/server_registry.py index 03f862550..4c273a3ab 100644 --- a/massgen/mcp_tools/server_registry.py +++ b/massgen/mcp_tools/server_registry.py @@ -74,20 +74,25 @@ "name": "parallel_search", "type": "streamable-http", "url": "https://search.parallel.ai/mcp", + "headers": { + "Authorization": "Bearer ${PARALLEL_API_KEY}", + }, "description": ( "Parallel's hosted Search MCP server. Provides LLM-optimized " "web search and URL extraction tools that return ranked, " "compressed markdown excerpts suitable for direct model " - "consumption. Works without an API key for free anonymous " - "use; set PARALLEL_API_KEY for higher rate limits." + "consumption." ), - "requires_api_key": False, - "optional_api_key_env_var": "PARALLEL_API_KEY", + "requires_api_key": True, + "api_key_env_var": "PARALLEL_API_KEY", "notes": ( - "Free anonymous access for light/exploratory use. For higher " - "rate limits in production, get a key at https://platform.parallel.ai " - "and add 'headers: {Authorization: \"Bearer ${PARALLEL_API_KEY}\"}' " - "to your mcp_servers entry. Docs: " + "Get an API key at https://platform.parallel.ai. The " + "Authorization header is substituted from PARALLEL_API_KEY at " + "MCP connection time (mirrors how stdio servers consume env). " + "The endpoint search.parallel.ai/mcp also accepts unauthenticated " + "requests at lower rate limits — if you want anonymous use, add " + "the server manually to your mcp_servers config without the " + "headers block (see parallel_search_example.yaml). Docs: " "https://docs.parallel.ai/integrations/mcp/search-mcp" ), "security": { @@ -131,21 +136,13 @@ def get_server_config(server_name: str, apply_api_key_logic: bool = True) -> dic # Deep copy to avoid modifying the registry config = deepcopy(MCP_SERVER_REGISTRY[server_name]) - if apply_api_key_logic: + # Handle optional API key for Context7 + if apply_api_key_logic and server_name == "context7": optional_key_var = config.get("optional_api_key_env_var") if optional_key_var and is_api_key_available(optional_key_var): + # Add --api-key argument with the API key value api_key_value = os.environ.get(optional_key_var) - - # Server-specific injection of the optional API key. - if server_name == "context7": - # Context7 takes the key as a CLI flag on its stdio command. - config["args"].extend(["--api-key", api_key_value]) - elif config.get("type") == "streamable-http": - # Hosted HTTP MCPs take the key via the Authorization - # Bearer header. Don't overwrite a header the registry - # entry already declared. - headers = config.setdefault("headers", {}) - headers.setdefault("Authorization", f"Bearer {api_key_value}") + config["args"].extend(["--api-key", api_key_value]) return config From 1fb5b178ad96ece9b520f240f52f921fa9fa92b4 Mon Sep 17 00:00:00 2001 From: Matt Harris Date: Thu, 28 May 2026 17:11:20 -0400 Subject: [PATCH 5/5] fix(mcp): use 'claude' backend in parallel_search example for headers compat MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex review caught that the example was wired to backend.type: 'claude_code' but used the streamable-http 'headers:' field. ClaudeCodeBackend forwards MCP configs to ClaudeAgentOptions, where the equivalent auth field is a top-level 'authorization:' string (per massgen/backend/docs/MCP_IMPLEMENTATION_CLAUDE_BACKEND.md), not a nested 'headers' map — so users uncommenting the API-key block would have silently kept anonymous mode. Switch the example to backend.type: 'claude' (generic massgen MCP transport), matching the existing streamable_http_test configs whose 'headers:' shape lines up with the registry entry. Added an inline note pointing claude_code users at the 'authorization:' alternative. --- .../tools/web-search/parallel_search_example.yaml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/massgen/configs/tools/web-search/parallel_search_example.yaml b/massgen/configs/tools/web-search/parallel_search_example.yaml index 3b47c9b7f..49b8b7e33 100644 --- a/massgen/configs/tools/web-search/parallel_search_example.yaml +++ b/massgen/configs/tools/web-search/parallel_search_example.yaml @@ -25,14 +25,18 @@ agents: - id: "parallel_research_agent" backend: - type: "claude_code" - model: "claude-sonnet-4-6" - cwd: "workspace" + type: "claude" + model: "claude-sonnet-4-20250514" mcp_servers: - name: "parallel_search" type: "streamable-http" url: "https://search.parallel.ai/mcp" - # Uncomment to enable higher rate limits with a Parallel API key: + # Uncomment to enable higher rate limits with a Parallel API key. + # NOTE: this `headers` block is the generic massgen MCP transport + # shape (matches the streamable_http_test configs). When using the + # `claude_code` backend, the equivalent field is a top-level + # `authorization: "Bearer ${PARALLEL_API_KEY}"` instead (see + # massgen/backend/docs/MCP_IMPLEMENTATION_CLAUDE_BACKEND.md). # headers: # Authorization: "Bearer ${PARALLEL_API_KEY}" security: