# Research agent

Research through MCP with model retries and fallback, then save a report.

Research an AI topic with a local web-search function and tools from your MCP server, then save a Markdown brief. This is the complete example shown on the home page, with setup instructions and a downloadable script.

## Setup

Use Python 3.11 or newer in a virtual environment. Install the SDK with its MCP and OpenAI extras, plus the search library:

~~~bash
python -m pip install "harnessx[mcp,openai]" ddgs
export ANTHROPIC_API_KEY="your-anthropic-key"
export OPENAI_API_KEY="your-openai-key"
export FALLBACK_MODEL="your-openai-model-id"
~~~

The script sets `provider="anthropic"` and no model, so the primary is the default `claude-sonnet-4-6`. Set `FALLBACK_MODEL` to an OpenAI model available to your account. The script reads exported environment variables; it does not load a `.env` file.

The local `search_web()` function uses [DDGS](https://github.com/deedy5/ddgs) to return up to five web results with titles, URLs, and excerpts. It runs in your Python process and needs no search API key. Availability depends on the upstream engines.

Replace `https://mcp.example.com/mcp` with your research MCP server's URL. Choose a server exposing the read-only search or reference tools needed for the task. The example allows calls to the server's tools with `permission="allow"`; choose a stricter permission for servers that expose other actions. Add authentication headers if your server requires them, as shown in the [MCP guide](../mcp/).

## Research and save a report

[Download research_report.py](../../examples/research_report.py), or copy this code into a file with that name:

~~~python
import asyncio
import os
from pathlib import Path

from ddgs import DDGS
import harnessx as harness

tools = harness.ToolRegistry()


# permission is optional; tools are allowed by default. ASK pauses for an
# approval callback, and DENY blocks the tool.
@tools.register(permission=harness.PermissionLevel.ALLOW)
def search_web(query: str) -> list[dict]:
    """Search the web for titles, URLs, and short excerpts."""
    return DDGS(timeout=10).text(query, max_results=5)


# Change this to whatever you want researched.
TOPIC = "running Python agents in a sandboxed container"

TASK = (
    f"Research {TOPIC} using the search and MCP tools available to you. "
    "Write a Markdown brief covering the steps to adopt it, where the limits "
    "are, and what you could not verify. End with links to the sources you "
    "actually used."
)


async def main():
    # Use Claude, and switch to an OpenAI model if Claude fails temporarily.
    config = harness.AgentConfig(
        provider="anthropic",
        fallbacks=[harness.Fallback("openai", model=os.environ["FALLBACK_MODEL"])],
        retry=harness.RetryPolicy(attempts=3),
    )

    async with harness.MCPManager() as mcp:
        research_server = harness.MCPServerConfig.http(
            "research",
            "https://mcp.example.com/mcp",  # Your MCP server
            permission="allow",  # Allow this server's tools
        )
        await mcp.connect(research_server)

        async with harness.Agent(
            tools=tools,
            mcp=mcp,
            config=config,
        ) as agent:

            # Print the brief as it is written.
            async with agent.run_stream(TASK) as stream:
                async for event in stream:
                    if event.type == harness.RunEventType.TEXT_DELTA:
                        print(event.data, end="", flush=True)
                result = await stream.result()

    result.raise_for_status()
    Path("research.md").write_text(result.output, encoding="utf-8")


if __name__ == "__main__":
    asyncio.run(main())
~~~

Run it from the folder where you want the report:

~~~bash
python research_report.py
~~~

The brief prints in the terminal as it is written. On a completed run, open `research.md`. It covers the steps to adopt the topic, where the limits are, what the agent could not verify, and links to the sources it used. Set `TOPIC` at the top of the file to research something else. Results depend on the model and the search responses, and a rerun replaces the file.

## How it works

1. **Register a local tool.** `@tools.register(...)` adds `search_web` to a `ToolRegistry`, and `tools=tools` passes that registry to the agent. HarnessX derives the input schema from the `query: str` annotation and the description from the docstring. Synchronous functions run in a worker thread.
2. **Set permissions on the tools.** `permission` is optional; a tool without one inherits the agent's default, which is `ALLOW`. The example sets `search_web` to `PermissionLevel.ALLOW` explicitly, and `permission="allow"` on `MCPServerConfig.http()` allows the MCP server's tools, which otherwise default to `ASK`. See [Permissions](../permissions/) for `ASK` approvals and `DENY`.
3. **Add MCP and research.** `MCPManager` connects to your server and discovers its tools, and `Agent(mcp=mcp)` bridges them into the agent. The task asks the agent to research `TOPIC` using both the local search tool and the MCP tools, cite the sources it used, and say what it could not verify.
4. **Recover from model failures.** `AgentConfig(provider="anthropic", fallbacks=[Fallback("openai", model=...)])` names the OpenAI model to try when the primary has a transient failure. The agent builds and closes that chain itself. `RetryPolicy(attempts=3)` allows up to three attempts through the chain. See [Retries & fallback models](../retries-and-fallbacks/) for timing, error classification, and tool replay rules.
5. **Stream and save the result.** `agent.run_stream(TASK)` yields events as the agent works; the loop prints each `TEXT_DELTA`, and `await stream.result()` returns the final `RunResult`. `raise_for_status()` raises if the run fails or stops before completion. Only then does Python write the report to the fixed local path. The agent does not need file or shell tools for this workflow.

Model retries are separate from local and MCP tool retries. This example leaves tool retry and replay settings at their defaults.

## Adapt it to your work

Set `TOPIC` to research a vendor shortlist, compare APIs, or prepare a source-backed brief on something else. Edit `TASK`, the full instruction passed to `agent.run_stream()`, to change what the brief covers or how it is laid out. Change `research.md` to choose the output file.

Point the MCP URL at your own analytics or project server and change the task to match its tools. Select the tools the task needs and choose their permissions. The [MCP guide](../mcp/) covers authentication, tool selection, and permissions.

Add [skills](../skills/) for writing instructions, [knowledge bundles](../knowledge/) for curated facts, or [tracing](../evaluations/) when you need to inspect a run.
