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:
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 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.
Research and save a report
Download research_report.py, or copy this code into a file with that name:
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:
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
- Register a local tool.
@tools.register(...)addssearch_webto aToolRegistry, andtools=toolspasses that registry to the agent. HarnessX derives the input schema from thequery: strannotation and the description from the docstring. Synchronous functions run in a worker thread. - Set permissions on the tools.
permissionis optional; a tool without one inherits the agent's default, which isALLOW. The example setssearch_webtoPermissionLevel.ALLOWexplicitly, andpermission="allow"onMCPServerConfig.http()allows the MCP server's tools, which otherwise default toASK. See Permissions forASKapprovals andDENY. - Add MCP and research.
MCPManagerconnects to your server and discovers its tools, andAgent(mcp=mcp)bridges them into the agent. The task asks the agent to researchTOPICusing both the local search tool and the MCP tools, cite the sources it used, and say what it could not verify. - 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 for timing, error classification, and tool replay rules. - Stream and save the result.
agent.run_stream(TASK)yields events as the agent works; the loop prints eachTEXT_DELTA, andawait stream.result()returns the finalRunResult.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 covers authentication, tool selection, and permissions.
Add skills for writing instructions, knowledge bundles for curated facts, or tracing when you need to inspect a run.