Skip to main content
Glama
YugMakhecha17

Finance MCP Aggregator

README.md
# Finance MCP Aggregator

**A Scoped Aggregator pattern MCP server: 43 real finance tools, but the model
only ever sees the 4–7 that matter for the current turn.**

Built to solve a real, measured problem: MCP servers that statically register
every tool up front degrade as the tool count grows past ~15. This project
implements the fix — dynamic, intent-routed tool exposure — for a domain
(personal finance / investing analytics) where 40+ tools is the realistic
end state, not a contrived example.

---

## 1. The problem, with numbers

Give an LLM 40+ tool schemas in its system prompt on every single turn and
four failure modes compound:

| Failure mode | Mechanism | Effect |
|---|---|---|
| **Attention dilution** | Tool definitions in the middle of a long prompt get less attention ("lost in the middle") | Wrong tool picked, or the right tool picked with the wrong arguments |
| **Name/description collisions** | `get_stock_price` vs `get_after_hours_price`, `calculate_var` vs `calculate_volatility` — semantic overlap is inevitable at 40+ tools | Coin-flip tool selection |
| **Parameter hallucination** | Model has 40+ schemas competing for context; it blends fields from one schema into a call to another | Invalid API calls, retries, silent wrong answers |
| **Context/cost bloat** | Every tool schema is JSON sent on *every* turn, whether used or not | Fewer tokens left for actual conversation, higher per-call cost |

This project measures — not just claims — the first and last of those for its
own tool catalog. Run `benchmarks/benchmark_routing.py` yourself; here's what
it produced on this repo's 43-tool registry:

```
TOKEN COST: static merge (43 tools) vs scoped aggregator
Architecture                Tokens/turn    Tokens/10 turns
Static merge (all 43)       1754           17540
Scoped aggregator           338            3380
Reduction in tool-schema tokens per turn: 80.7%

ROUTING ACCURACY: expected tool in top-6 results
Top-6 accuracy: 25/26 (96.2%) on a 26-query evaluation set spanning all 6 domains
```

(Token counts are a ~4-chars/token estimate, order-of-magnitude accurate —
swap in `tiktoken` or Anthropic's tokenizer for exact figures. The
methodology is transparent in the script, not a black box.)

**What this means in practice:** at $3/M input tokens and 500 tool-augmented
turns/day, the static-merge architecture spends roughly $0.79/day on tool
schemas alone that never get called; the scoped aggregator spends about
$0.15/day for the same volume — and, more importantly, the accuracy numbers
above are what actually protects you from a misrouted `execute_trade`-style
call in a finance context, where a wrong tool call isn't just an annoyance.

---

## 2. Why finance, specifically

Personal finance / investing assistants are one of the few domains where
40+ distinct tools is the honest requirement, not an artificially inflated
demo:

- **Market data** (quotes, history, news, earnings dates, dividends, splits)
- **Portfolio analytics** (value, returns, Sharpe ratio, diversification, rebalancing, CAGR, drawdown, benchmarking)
- **Risk analytics** (VaR, beta, volatility, stress testing, correlation, Monte Carlo simulation)
- **Technical analysis** (SMA, EMA, RSI, MACD, Bollinger Bands, support/resistance, ATR, Fibonacci)
- **Fundamental analysis** (P/E, EPS, balance sheet, income statement, cash flow, DCF valuation)
- **Currency & crypto** (FX conversion, live rates, crypto pricing, gas fees)

A real product covering all six domains genuinely needs on the order of 40+
tools. Trying to force that into a single flat MCP tool list is exactly the
scenario the Scoped Aggregator pattern is meant for — this repo is that
pattern applied to a domain where it's load-bearing, not decorative.

---

## 3. Architecture

```
                          ┌─────────────────────────┐
                          │   LLM / MCP Client       │
                          │ (Claude Desktop, etc.)   │
                          └────────────┬─────────────┘
                                       │  sees only 4 meta-tools,
                                       │  not all 43
                   ┌───────────────────▼────────────────────┐
                   │        finance_mcp/server.py            │
                   │  list_finance_categories()               │
                   │  search_finance_tools(query, top_k)  ────┼──┐
                   │  get_tool_schema(tool_name)              │  │ 1. TF-IDF cosine
                   │  execute_finance_tool(tool_name, params) │  │    similarity ranks
                   └───────────────────┬────────────────────┘  │    query against all
                                       │                        │    43 tool docs
                          ┌────────────▼────────────┐  ◄────────┘
                          │   router.py (IntentRouter)│
                          │   ranks registry.py's      │
                          │   43 ToolSpecs by relevance │
                          └────────────┬────────────┘
                                       │  2. top-k names resolved
                          ┌────────────▼────────────┐
                          │   executor.py             │
                          │   lazily imports + calls   │
                          │   the winning tool          │
                          └────────────┬────────────┘
                                       │
                   ┌───────────────────▼────────────────────┐
                   │  tools/market_data.py                    │
                   │  tools/portfolio_analysis.py             │
                   │  tools/risk_analysis.py                  │  ← 43 tools total,
                   │  tools/technical_analysis.py             │    only imported when
                   │  tools/fundamental_analysis.py           │    actually called
                   │  tools/currency_crypto.py                │
                   └──────────────────────────────────────────┘
```

**The flow an LLM actually follows:**

1. `search_finance_tools("what's my portfolio's Sharpe ratio")` → returns
   the 5–6 relevant tools (not all 43), each with its exact parameter schema.
2. `execute_finance_tool("calculate_sharpe_ratio", {...})` → runs it.

No dependence on MCP clients supporting `tools/list_changed` notifications
(many don't, reliably) — the narrowing happens inside a normal tool call,
which every MCP client already supports. This is the same "search then act"
shape you'll recognize from how large codebases expose tool search to
coding agents, applied here to a finance tool catalog.

### Why TF-IDF instead of an LLM router or embeddings model

- **No extra LLM call** — a router that itself needs GPT-4 to pick a tool
  defeats the purpose (latency + cost on every turn).
- **No heavyweight download** — `scikit-learn` installs in seconds on a
  plain Windows Python 3.12 environment; no `torch`, no model weights.
- **Deterministic and debuggable** — you can inspect exactly which words
  matched.
- **Swappable** — `router.py` includes an `EmbeddingRouter` with the same
  interface for when a catalog grows past a few hundred tools and
  paraphrase-level matching starts to matter more than raw token overlap.
  See [Scaling further](#5-scaling-further-than-this-repo) below.

---

## 4. Project structure

```
finance-mcp-aggregator/
├── README.md
├── requirements.txt
├── pyproject.toml
├── .env.example
├── .gitignore
├── LICENSE
├── src/finance_mcp/
│   ├── server.py                 # MCP server: 4 meta-tools only
│   ├── router.py                 # IntentRouter (TF-IDF) + EmbeddingRouter
│   ├── registry.py               # Metadata for all 43 tools (no imports of impls)
│   ├── executor.py                # Lazy resolve + call by dotted path
│   └── tools/
│       ├── _data_source.py       # yfinance wrapper w/ offline synthetic fallback
│       ├── market_data.py        # 8 tools
│       ├── portfolio_analysis.py # 8 tools
│       ├── risk_analysis.py      # 7 tools
│       ├── technical_analysis.py # 8 tools
│       ├── fundamental_analysis.py # 6 tools
│       └── currency_crypto.py    # 6 tools
├── benchmarks/
│   └── benchmark_routing.py      # Token-cost + routing-accuracy measurements
├── tests/
│   └── test_router.py
└── examples/
    └── demo_client.py            # Runs the search→execute flow with no LLM/API key
```

---

## 5. Setup (Windows, Python 3.12.4)

```powershell
git clone https://github.com/<you>/finance-mcp-aggregator.git
cd finance-mcp-aggregator

python -m venv .venv
.venv\Scripts\activate

pip install -r requirements.txt
copy .env.example .env
```

### Run the demo (no MCP client, no API key needed)

```powershell
python examples\demo_client.py "what's my portfolio's value at risk"
```

### Run the benchmark yourself

```powershell
python benchmarks\benchmark_routing.py
```

### Run tests

```powershell
pytest tests\ -v
```

### Run the actual MCP server

Built on the standalone [`fastmcp`](https://github.com/jlowin/fastmcp)
package (not the trimmed-down `mcp.server.fastmcp` bundled inside the
official `mcp` SDK) — it tracks the MCP spec closer to real-time and adds a
dev inspector, auth providers, and an HTTP transport out of the box.

```powershell
# stdio (default) -- what Claude Desktop and most MCP clients expect
python -m finance_mcp.server

# or run it as a standalone HTTP server instead
$env:MCP_TRANSPORT="http"; $env:MCP_PORT="8000"; python -m finance_mcp.server
```

To use it from Claude Desktop, add to your MCP config
(`%APPDATA%\Claude\claude_desktop_config.json`):

```json
{
  "mcpServers": {
    "finance-aggregator": {
      "command": "C:\\path\\to\\finance-mcp-aggregator\\.venv\\Scripts\\python.exe",
      "args": ["-m", "finance_mcp.server"]
    }
  }
}
```

You can also inspect it live with FastMCP's built-in dev tool before wiring
it into any client:

```powershell
fastmcp dev src\finance_mcp\server.py
```

**Note on live data:** every tool falls back to deterministic synthetic
data when `yfinance` has no network access, so the whole test/benchmark
suite runs fully offline. With network access, `get_stock_price`,
`get_historical_prices`, `get_pe_ratio`, etc. pull real data via `yfinance`
(no API key required).

---

## 6. Scaling further than this repo

This repo's registry (43 tools, TF-IDF router) is sized to be genuinely
runnable end-to-end without any paid API or heavy install. If you fork this
for a catalog of hundreds of tools across many MCP servers, the pattern
extends cleanly:

- **Categorization → Intent routing → Dynamic ingestion**, exactly as
  described in the architecture above, generalizes past one server: put an
  aggregator server in front of *multiple* downstream MCP servers (each
  exposing its native tools), and have the aggregator's `search_tools`
  fan out across all of them.
- Swap `IntentRouter` for the included `EmbeddingRouter`
  (`pip install sentence-transformers`) once paraphrase matching
  ("how risky is my portfolio" ≈ "what's my VaR") starts mattering more
  than keyword overlap — this repo's benchmark script is the harness to
  validate that swap actually improves top-k accuracy before you ship it.
- For MCP clients that *do* support `notifications/tools/list_changed`,
  you can go further and only ever expose the narrowed tool list itself
  (not meta-tools) — trading universal compatibility for a marginally
  smaller prompt.

---

## 7. What this is not

- Not investment advice, and not wired to a brokerage — every calculation
  tool is a standalone analytics function you'd still validate before
  acting on.
- The offline synthetic price fallback exists so tests/CI/demos don't
  require network or an API key; it is clearly labeled `"source":
  "synthetic-offline"` in every response so it's never confused with real
  market data.
- The DCF, Monte Carlo, and VaR implementations are standard textbook
  formulations for demonstration; a production system would want a
  reviewed quant library and real-world backtesting before being trusted
  with real allocation decisions.

---

## Repository health and contribution model

This repository is structured so it can be pushed and maintained as a clean, open-source Python package:

- Source code lives under `src/finance_mcp/` and is buildable through `pyproject.toml`.
- Tool metadata lives in a registry rather than scattered implementation details.
- The public API is exposed through the server entry point and a stable set of search/execute tools.
- Contribution docs are available in `CONTRIBUTING.md`, `SECURITY.md`, and `CHANGELOG.md`.
- CI automation, issue templates, and pull request templates are available under `.github/`.

If you are preparing a public repository push, keep the branch history small and the release artifacts explicit.

---

## License

MIT — see [LICENSE](LICENSE).

TDQS

A4.4/5.0

Scored across 4 tools

Disambiguation5/5

Each tool has a distinct role: listing categories, searching for tools, fetching schemas, and executing. There is no functional overlap between any two tools.

Naming Consistency5/5

All tool names follow a consistent verb_noun pattern (list_finance_categories, search_finance_tools, get_tool_schema, execute_finance_tool), making the API predictable and easy to navigate.

Tool Count5/5

With only 4 tools, the server is well-scoped for its aggregator purpose. Instead of exposing 43 tools directly, it provides a lean meta-layer that is easy to learn and use.

Completeness5/5

The tool set covers the full discovery-to-execution workflow: list categories, search, get schema, and execute. There are no obvious missing operations for an aggregator that intentionally hides the underlying tool catalog.

Maintenance

ActivitySlowing
ResponsivenessNo issues