Skip to main content
Glama
zacharymcclernon

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.

Maintenance

ActivityMaintained
ResponsivenessNo issues