mcp-finance-agent
README.md
# Finance Agent
A LangGraph agent that routes natural-language stock questions between a custom MCP server (yfinance quant tools) and a news tool, validated by a hand-rolled eval set.
Built to close two skill gaps: designing MCP servers (tool surface, descriptions-as-routing-logic) and agent orchestration with routing judgment.
---
## Architecture
```
User Query
│
▼
┌─────────┐
│ route │ 3-way classifier: quant / news / both
└────┬────┘
│
├──────────────────────┬─────────────────────
▼ ▼
┌─────────┐ ┌──────────┐
│ quant │ │ news │
│ (ReAct) │ │ (ReAct) │
└────┬────┘ └────┬─────┘
│ │
└──────────┬───────────┘
│
┌───────┴────────┐
│ │
▼ ▼
┌──────────────┐ ┌───────────┐
│finish_single │ │ synthesize│
│ _source │ │ │
└──────┬───────┘ └─────┬─────┘
│ │
└────────┬─────────┘
▼
Answer
```
**Nodes:**
- `route` — classifies the query; no tool calls, just a decision
- `quant` — ReAct agent with 5 MCP tools; picks the right tool(s) for the question
- `news` — ReAct agent with `get_recent_news`; handles news/context questions
- `finish_single_source` — pass-through for single-path queries; promotes result to `answer`
- `synthesize` — reconciles quant + news outputs into one coherent answer for "both" queries
For `"both"` queries, `quant` and `news` run concurrently; LangGraph holds until both complete before running `synthesize`.
---
## MCP Server — Quant Tools
Five tools, each scoped to a single question shape:
| Tool | Use case |
|---|---|
| `get_stock_price(ticker, date?)` | Point-in-time price lookup |
| `get_fundamentals(ticker)` | P/E, EPS, market cap, revenue growth, margins |
| `compare_metric(tickers[], metric)` | Side-by-side fundamentals comparison across companies |
| `get_price_history_stats(ticker, period)` | Return, volatility, vs-average over a lookback period |
| `get_earnings_date(ticker)` | Next/most recent earnings date |
Tool descriptions include explicit negative cases ("not for X, which belongs to tool Y") — required because several tools share similar parameter shapes and would otherwise be ambiguous to the router.
---
## Setup
```bash
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env # add your ANTHROPIC_API_KEY
```
---
## Usage
```bash
python -m agent.graph "What's Tesla's P/E ratio?"
python -m agent.graph "Why did NVDA drop this week?"
python -m agent.graph "Has AMZN's stock kept pace with its revenue growth?"
```
---
## Evals
```bash
pytest evals/test_agent.py -v
```
18 queries across four categories: quant, news, both, and adversarial. Adversarial cases include keyword traps (news language wrapping quant questions) and false-premise queries (assertions about price movement that must be verified before explaining).
**Result: 15/18 → 18/18** after correcting two brittle ticker-string assertions and adding a false-premise classification rule to the router prompt.
---
## Key Design Decisions
**1. Tool descriptions as routing logic.** Every tool description explicitly states what it does *not* handle. Without negative cases, tools with overlapping parameter shapes (e.g. `get_stock_price` and `get_price_history_stats` both take ticker + date-ish params) are ambiguous to the LLM router.
**2. News as a plain LangGraph tool, not a second MCP server.** The instinct was "second MCP server = reusable building block." Pushed on it: reusable for what? No concrete second consumer existed. A second MCP server means a second subprocess to launch and manage; a plain `@tool` is a single in-process function. The process-boundary overhead had no matching benefit.
**3. Router classifies intent, agents pick tools.** The router does 3-way classification only — it does not pick which quant tool to call. LLM tool-selection (matching a query to one of 5 tool descriptions) is flexible/non-deterministic work that belongs inside the quant ReAct loop, not duplicated into the router prompt.
**4. Synthesis must reconcile, not concatenate.** The synthesis prompt explicitly instructs the model to connect quant data and news context into a single coherent answer. "Do not just list both separately — connect them." Without this, the "both" path produces two paragraphs stapled together rather than an actual answer.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues