MCP Agent
A multi-agent MCP (Model Context Protocol) tool-calling pipeline that coordinates an mcp-runner (filesystem reads and codebase search via @waxell.tool(tool_type="mcp")) and an mcp-evaluator (retrieval, reasoning, decision, LLM synthesis). Built with OpenAI and waxell-observe decorator patterns.
This example runs in dry-run mode by default (no API key needed). For live mode, set OPENAI_API_KEY, WAXELL_API_KEY, and WAXELL_API_URL.
Architecture
Key Code
MCP tool decorators
The tool_type="mcp" value marks these as MCP protocol tool calls in the trace, distinguishing them from regular function or API tools.
@waxell.tool(tool_type="mcp")
def mcp_read_file(path: str) -> dict:
"""Call MCP filesystem.read_file tool to read a file."""
return {
"path": "src/main.py",
"content": "import asyncio\nfrom agent import Agent\n...",
"size_bytes": 204,
"encoding": "utf-8",
}
@waxell.tool(tool_type="mcp")
def mcp_search_query(query: str, max_results: int = 10) -> dict:
"""Call MCP search.query tool to search the codebase."""
return {
"matches": [
{"file": "src/agent.py", "line": 12, "text": "class Agent:", "score": 0.95},
{"file": "src/tools.py", "line": 8, "text": "def analyze_code(source):", "score": 0.88},
],
"total_matches": 3,
}
MCP runner child agent
The runner wraps MCP tool calls with waxell metadata for full observability of which MCP tools were invoked and what they returned.
@waxell.observe(agent_name="mcp-runner", workflow_name="mcp-tool-execution")
async def run_mcp_runner(query: str, waxell_ctx=None):
waxell.tag("agent_role", "runner")
waxell.tag("protocol", "model-context-protocol")
waxell.metadata("available_tools", ["filesystem.read_file", "search.query"])
file_result = mcp_read_file(path="src/main.py") # @tool(mcp) auto-recorded
search_result = mcp_search_query(query="agent impl") # @tool(mcp) auto-recorded
waxell.metadata("files_read", 1)
waxell.metadata("search_matches", search_result["total_matches"])
return {"file_result": file_result, "search_result": search_result}
Evaluator with reasoning and decision
The evaluator retrieves ranked results, reasons about codebase structure, decides analysis depth, and synthesizes via an auto-instrumented OpenAI call.
@waxell.observe(agent_name="mcp-evaluator", workflow_name="mcp-analysis")
async def run_mcp_evaluator(query, runner_results, openai_client, waxell_ctx=None):
waxell.tag("agent_role", "evaluator")
ranked = retrieve_search_results(query="agent implementation") # @retrieval
assessment = await assess_codebase_structure(file_result, search_result) # @reasoning
depth = await choose_analysis_depth(search_result=search_result) # @decision
response = await openai_client.chat.completions.create(...) # auto-instrumented
waxell.score("code_structure", 0.88, comment="Well-organized")
waxell.score("test_presence", 1.0, data_type="boolean")
waxell.score("analysis_completeness", 0.82)
return {"analysis": response.choices[0].message.content}
What this demonstrates
@waxell.tool(tool_type="mcp")-- MCP protocol tool calls recorded with themcptool type, distinguishing them from regular API or function tools in the trace view.@waxell.retrieval(source="mcp_search")-- search result retrieval recorded with source attribution.@waxell.reasoning_dec-- codebase structure assessment with thought, evidence, and conclusion.@waxell.decision-- analysis depth decision (shallow/standard/deep) based on codebase complexity.- Auto-instrumented LLM calls -- OpenAI synthesis call captured automatically via
waxell.init(). waxell.score()with mixed types -- numeric scores (code_structure, completeness) and boolean scores (test_presence).waxell.metadata()-- MCP-specific metadata (available tools, files read, search matches) attached to the runner span.- Nested
@waxell.observe-- orchestrator is parent; mcp-runner and mcp-evaluator are child agents with automatic lineage.
Run it
# Dry-run (no API key needed)
python -m app.demos.mcp_agent --dry-run
# Live mode with OpenAI
OPENAI_API_KEY=sk-... python -m app.demos.mcp_agent