Previously the depth parameter (shallow/balanced/deep) was passed only as a text hint inside the agent's user message, with no mechanical effect on iterations, token budget, or source count. The flag was effectively cosmetic — the LLM was expected to "interpret" it. Add DEPTH_PRESETS table and constraints_for_depth() helper in researchers.web.models: shallow: 2 iters, 5,000 tokens, 5 sources balanced: 5 iters, 20,000 tokens, 10 sources (= historical defaults) deep: 8 iters, 60,000 tokens, 20 sources Wired through the stack: - WebResearcher.research(): when constraints is None, builds from the depth preset instead of bare ResearchConstraints() - MCP server `research` tool: max_iterations and token_budget now default to None; constraints are built via constraints_for_depth with explicit values overriding the preset - CLI `ask` command: --max-iterations and --budget default to None; the CLI only forwards them to the MCP tool when set, so unset flags fall through to the depth preset balanced is unchanged from the historical defaults so existing callers see no behavior difference. Explicit --max-iterations / --budget always win over the preset. Tests cover each preset's values, balanced backward-compat, unknown depth fallback, full override, and partial override. 116/116 tests passing. Live-verified: --depth shallow on a simple question now caps at 2 iterations and stays under budget. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
98 lines
2.8 KiB
Python
98 lines
2.8 KiB
Python
"""MCP server for the web researcher.
|
|
|
|
Exposes a single tool `research` that delegates to WebResearcher.
|
|
Run with: python -m researchers.web.server
|
|
"""
|
|
|
|
import asyncio
|
|
import os
|
|
import sys
|
|
from typing import Optional
|
|
|
|
from mcp.server.fastmcp import FastMCP
|
|
|
|
from obs import configure_logging, get_logger
|
|
from researchers.web.agent import WebResearcher
|
|
from researchers.web.models import constraints_for_depth
|
|
|
|
log = get_logger("marchwarden.mcp")
|
|
|
|
mcp = FastMCP(
|
|
name="marchwarden-web-researcher",
|
|
instructions=(
|
|
"A Marchwarden web research specialist. "
|
|
"Call the research tool with a question to get a grounded, "
|
|
"evidence-based answer with citations, gaps, open questions, "
|
|
"and confidence scoring."
|
|
),
|
|
)
|
|
|
|
|
|
def _read_secret(key: str) -> str:
|
|
"""Read a secret from ~/secrets file."""
|
|
secrets_path = os.path.expanduser("~/secrets")
|
|
with open(secrets_path) as f:
|
|
for line in f:
|
|
if line.startswith(f"{key}="):
|
|
return line.split("=", 1)[1].strip()
|
|
raise ValueError(f"Key {key} not found in {secrets_path}")
|
|
|
|
|
|
def _get_researcher() -> WebResearcher:
|
|
"""Create a WebResearcher with keys from ~/secrets."""
|
|
return WebResearcher(
|
|
anthropic_api_key=_read_secret("ANTHROPIC_API_KEY"),
|
|
tavily_api_key=_read_secret("TAVILY_API_KEY"),
|
|
model_id=os.environ.get("MARCHWARDEN_MODEL", "claude-sonnet-4-6"),
|
|
)
|
|
|
|
|
|
@mcp.tool()
|
|
async def research(
|
|
question: str,
|
|
context: Optional[str] = None,
|
|
depth: str = "balanced",
|
|
max_iterations: Optional[int] = None,
|
|
token_budget: Optional[int] = None,
|
|
) -> str:
|
|
"""Research a question using web search and return a structured answer.
|
|
|
|
Args:
|
|
question: The question to investigate.
|
|
context: What the caller already knows (optional).
|
|
depth: Research depth — "shallow", "balanced", or "deep". Each
|
|
depth picks default max_iterations / token_budget / max_sources.
|
|
max_iterations: Override the depth preset for iterations (1-20).
|
|
token_budget: Override the depth preset for token budget.
|
|
|
|
Returns:
|
|
JSON string containing the full ResearchResult with answer,
|
|
citations, gaps, discovery_events, open_questions, confidence,
|
|
and cost_metadata.
|
|
"""
|
|
researcher = _get_researcher()
|
|
constraints = constraints_for_depth(
|
|
depth,
|
|
max_iterations=max_iterations,
|
|
token_budget=token_budget,
|
|
)
|
|
|
|
result = await researcher.research(
|
|
question=question,
|
|
context=context,
|
|
depth=depth,
|
|
constraints=constraints,
|
|
)
|
|
|
|
return result.model_dump_json(indent=2)
|
|
|
|
|
|
def main():
|
|
"""Run the MCP server on stdio."""
|
|
configure_logging()
|
|
log.info("mcp_server_starting", transport="stdio", server="marchwarden-web-researcher")
|
|
mcp.run(transport="stdio")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|