Skip to main content
Glama
octorohan
by octorohan

financial-analyst-mcp

Natural language in, a real stock comparison chart out — via a Planner Agent (LLM), Pydantic-validated structured output, an optional Critic agent, and a real MCP client/server pair driving deterministic yfinance/pandas/Matplotlib tools. No Cursor, no arbitrary LLM-generated code execution.

This project is a from-scratch redesign of a reference "Multi-Agent Financial Analyst (MCP)" project that used CrewAI + 3 agents + Cursor-IDE-as-MCP-host. This version removes the Cursor dependency entirely (its MCP client couldn't be driven programmatically), removes arbitrary code generation/execution, and reduces the agent count from three to two (Planner + optional Critic), with the reasoning for each change documented in ARCHITECTURE.md.


What it does

"Compare Apple's stock performance with Microsoft's over the last 6 months
 and plot their normalized returns."
        │
        ▼
  Planner Agent (LLM) → tickers=[AAPL, MSFT], timeframe=last_6_months, metric=normalized_returns
        │
        ▼
  Pydantic validation + deterministic date resolution
        │
        ▼
  Optional Critic pass (re-checks the extraction against the raw prompt)
        │
        ▼
  MCP Client → MCP Server → fetch (yfinance) → compute (pandas) → plot (Matplotlib)
        │
        ▼
  A real PNG chart + return/volatility/drawdown metrics, back in the UI

Related MCP server: Finance MCP Server

Why MCP, and why not Cursor

MCP's actual value here is host interoperability: backend/mcp_server.py is a standalone process exposing tools over a standard protocol, callable by this app's own client, by Claude Desktop, or by anything else that speaks MCP — without touching the tool implementations. For a single app in isolation, plain function calls would work identically; MCP is what makes the tool surface reusable outside this one app.

Cursor was dropped entirely. In the reference project, Cursor served as the MCP host — but Cursor's MCP client is built for a human typing into an IDE chat panel, not for a Python agent pipeline to invoke programmatically. There is no clean way for orchestrator.py to "call Cursor." This project ships its own ~150-line MCP client (backend/orchestrator.py) using the official mcp SDK instead, so the whole system runs standalone, with zero IDE dependency, for anyone who clones it. Full reasoning in ARCHITECTURE.md.

How the Planner works

The Planner (backend/planner.py) sends the user's prompt plus today's date to an LLM and asks it to emit a JSON object matching the AnalysisRequest Pydantic schema (backend/schemas.py): tickers, a normalized timeframe pattern (e.g. "last_6_months", "year:2025", "since:2024-01-01" — never raw date arithmetic), a metric, and an analysis type. Pydantic then deterministically resolves that pattern into concrete start_date/ end_date via backend/date_utils.pydate math is never left to the LLM. If the LLM can't confidently identify a ticker or timeframe, the request comes back with needs_clarification=true instead of a guess.

How MCP client/server communication works

backend/mcp_server.py registers four tools (fetch_stock_data, calculate_returns, compute_comparison, plot_comparison) using the official mcp SDK's FastMCP API, each wrapping a deterministic function in backend/tools/. backend/orchestrator.py's MCPClient launches that server as a subprocess over stdio, calls list_tools() to discover what's available, and call_tool(name, arguments) to invoke one, with a timeout and structured error translation (OrchestratorError) at every step.

Available tools

Tool

Purpose

fetch_stock_data(ticker, start_date, end_date)

Raw OHLCV + headline metrics for one ticker

calculate_returns(ticker, start_date, end_date, kind)

Daily or cumulative return series for one ticker

compute_comparison(tickers, start_date, end_date, metric)

Multi-ticker aligned comparison data, no chart

plot_comparison(tickers, start_date, end_date, metric, chart_title)

The primary end-to-end tool: fetch + compute + render a PNG in one call


Installation

git clone <this repo>
cd financial-analyst-mcp
python3 -m venv .venv
source .venv/bin/activate       # Windows: .venv\Scripts\activate
pip install -r requirements.txt

Environment setup

cp .env.example .env

By default LLM_PROVIDER=mock, which needs no API key and no network — see "Offline mock mode" below. To use a real LLM, set LLM_PROVIDER=openai or LLM_PROVIDER=anthropic and fill in LLM_API_KEY in .env.

How to start the MCP server

You normally don't need to start this yourself — backend/orchestrator.py launches it automatically as a subprocess per request. To run/inspect it standalone:

macOS/Linux/WSL/Git Bash:

./scripts/run_mcp_server.sh
# or, with the official inspector UI:
mcp dev backend/mcp_server.py

Windows PowerShell (.sh scripts won't run natively in PowerShell — use the .ps1 equivalents, or run the commands directly):

.\scripts\run_mcp_server.ps1
# or directly:
python -m backend.mcp_server

If PowerShell blocks script execution, either run once per session: Set-ExecutionPolicy -Scope Process -ExecutionPolicy RemoteSigned, or just run the underlying command shown above directly instead of the .ps1 file.

How to start the FastAPI backend

macOS/Linux/WSL/Git Bash:

./scripts/run_backend.sh

Windows PowerShell:

.\scripts\run_backend.ps1
# or directly:
if (!(Test-Path ".env")) { Copy-Item ".env.example" ".env" }
uvicorn backend.main:app --host 127.0.0.1 --port 8000 --reload

Note: there is no route at / — visiting http://127.0.0.1:8000/ will correctly show {"detail":"Not Found"}. Use these instead:

  • http://127.0.0.1:8000/health — status check

  • http://127.0.0.1:8000/docs — interactive API docs, lets you try /analyze directly

How to start Streamlit

In a second terminal, with the backend already running:

macOS/Linux/WSL/Git Bash:

./scripts/run_frontend.sh

Windows PowerShell:

.\scripts\run_frontend.ps1
# or directly:
streamlit run frontend/app.py

Visit the URL Streamlit prints (typically http://localhost:8501).

How to run tests

macOS/Linux/WSL/Git Bash:

pip install -r requirements.txt   # needed for the full suite, see below
./scripts/run_tests.sh

Windows PowerShell:

pip install -r requirements.txt
.\scripts\run_tests.ps1
# or directly:
python -m pytest tests\ -v

Every test file also runs with plain stdlib unittest, no pytest required:

python -m unittest discover -s tests -v

How to run evaluations

python eval/run_eval.py                    # offline, uses LLM_PROVIDER=mock by default
python eval/run_eval.py --provider openai   # grade a real LLM provider instead
python eval/run_eval.py --verbose

Offline mock mode

LLM_PROVIDER=mock (the default) uses a deterministic, regex/keyword-based stand-in LLM (backend.llm_client.MockLLMClient) that requires no API key and no network access. This exists for three real reasons, not just as a test double:

  1. Zero-friction local development and CI — no API credits burned, no network flakiness.

  2. A genuinely usable offline demo — clone, pip install, run, and the whole pipeline (Planner → Critic → MCP → tools → chart) works end to end with zero external accounts, aside from yfinance itself needing network access to fetch real market data.

  3. It's what let this repository's own automated tests and evaluation dataset (eval/eval_prompts.json) be exercised without any external dependency.

It is intentionally limited: a fixed dictionary of ~13 well-known company names, and regex-based timeframe/metric detection. See "Known Limitations."


Example prompts

Compare Apple's stock performance with Microsoft's over the last 6 months and plot their normalized returns.
Show me Tesla's performance during 2025.
Compare AAPL, MSFT and NVDA over the past 6 months.
Plot normalized returns for Tesla and Apple since January 2024.
Compare Google and Amazon cumulative returns since 2023-01-01.

Expected output

A JSON response from POST /analyze shaped like:

{
  "success": true,
  "summary": "Analyzed AAPL, MSFT from 2026-02-21 to 2026-08-21 (normalized_returns). AAPL: total return +18.4%, ...",
  "chart_path": "output/comparison_ab12cd34ef.png",
  "request_id": "a1b2c3d4e5f6",
  "extracted_request": { "tickers": ["AAPL", "MSFT"], "start_date": "2026-02-21", "...": "..." },
  "metrics": { "AAPL": { "total_return": 0.184, "cagr": 0.41, "annualized_volatility": 0.28, "max_drawdown": -0.11 }, "...": "..." },
  "errors": [],
  "warnings": []
}

...and, in Streamlit, that same information rendered as a summary, a line chart, and a metrics table.


What Was and Wasn't Executed (read this before judging "does it work")

This codebase was written and tested in a sandboxed environment with no network access whatsoever (verified: pip install and raw HTTP requests both fail there) and only pandas/numpy/matplotlib pre-installed. Being upfront about exactly what that means:

Fully executed and verified in that sandbox (82 automated tests, all passing, plus real generated PNG files inspected):

  • backend/date_utils.py — 22 tests, including month-end edge cases

  • backend/tools/data.py — 18 tests using a fake yfinance module (dependency-injected), covering retries, empty results, malformed ticker rejection, and a real bug this caught and fixed (fetch_multiple was keying results by unnormalized ticker strings)

  • backend/tools/analysis.py — 19 tests of the financial math against hand-computed expected values

  • backend/tools/plotting.py — 6 tests that generate real PNG files and verify them

  • backend/llm_client.py's MockLLMClient — 9 tests, including two real bugs this caught and fixed (the bare-ticker regex was picking up "YTD" and "S"/"P" from "S&P" as fake stock tickers)

  • backend/config.py and backend/logging_config.py — 8 tests

Written completely (no TODOs, no stubs, no placeholders) against the documented, real APIs, but not executable in that no-network sandbox because they require pydantic, fastapi, mcp, or streamlit, none of which could be installed:

  • backend/schemas.py, backend/planner.py, backend/critic.py

  • backend/mcp_server.py, backend/orchestrator.py

  • backend/main.py (FastAPI), frontend/app.py (Streamlit)

  • tests/test_planner.py, tests/test_mcp.py, tests/test_api.py — these are written and ready to run; they were not executed here, but the logic they exercise (mock extraction, date resolution) was independently verified via the modules above

  • eval/run_eval.py — written and ready to run; the ground-truth expected values in eval_prompts.json were derived by actually running MockLLMClient against each prompt in the sandbox and then applying the separately-tested schema-resolution rules by hand, not guessed

To close this gap yourself: pip install -r requirements.txt (needs network) and run ./scripts/run_tests.sh. Everything above should pass; if anything doesn't, it's a real bug for you to report/fix, not an intentional gap — the design has been reasoned through carefully, but I have not had the chance to execute the pydantic/FastAPI/MCP layers myself.


Known limitations

  • MockLLMClient's company dictionary is small (~13 names). Uncommon company names will fail to resolve and correctly trigger needs_clarification rather than silently guessing — but this means the offline mode has real, documented gaps (see eval/eval_prompts.json case p23, included deliberately to demonstrate this rather than hide it). A real LLM provider does not have this limitation.

  • No support for index tickers (S&P 500, Dow, etc.) — yfinance can fetch these via tickers like ^GSPC, but nothing in this system maps natural-language index names to those symbols yet.

  • No quarter-based timeframes ("last quarter", "Q3 2024") are in the supported timeframe vocabulary yet — these correctly trigger clarification rather than a wrong guess, but adding quarter support to date_utils.py would be a small, contained change.

  • compute_comparison's inner-join alignment trims to the shortest overlapping trading history across tickers — a very recently-IPO'd company compared against a decades-old one will only compare over the newer company's whole lifetime, which is correct but not necessarily obvious from the output alone.

  • The MCP server and client run as a fresh subprocess per /analyze request (see orchestrator.py's async with MCPClient(...)) rather than a persistent long-lived connection, which is simpler and more robust for a single-instance demo but adds subprocess-startup latency to every request. A production version would likely pool a persistent MCP session instead.

Future improvements

  • Persistent MCP session pooling instead of per-request subprocess launch.

  • Quarter/half-year timeframe support in date_utils.py.

  • A larger, LLM-maintained company→ticker lookup (or a real symbol-search API) instead of the mock's fixed dictionary.

  • Multi-step analysis chains (e.g. "compare X and Y, then tell me which had lower volatility") requiring the Planner to sequence multiple tool calls rather than always calling plot_comparison once.

  • A measured comparison of Planner accuracy with vs. without the Critic step on a shared eval set, to quantify whether the Critic is actually earning its cost.

  • Response caching (CacheConfig is already wired into config.py but not yet consumed by tools/data.py — a natural next step, keyed on ticker+date range).

  • Observability/tracing across the full pipeline (currently: structured logs with request-ID correlation, but no span-level tracing).


Troubleshooting

Symptom

Likely cause

Fix

Could not reach the backend in Streamlit

FastAPI isn't running, or BACKEND_URL mismatch

Start it with ./scripts/run_backend.sh; check BACKEND_URL in .env matches

/health returns "status": "degraded"

The MCP server subprocess failed to start

Run ./scripts/run_mcp_server.sh directly to see the real error; check MCP_SERVER_COMMAND/MCP_SERVER_ARGS in .env

LLM_API_KEY is not set error

LLM_PROVIDER is openai/anthropic but no key in .env

Add a real key, or switch to LLM_PROVIDER=mock

No trading data found for ticker

Ticker doesn't exist, is delisted, or has no data in the requested range

Check spelling; try a shorter/more recent range

Tests in test_planner.py/test_mcp.py/test_api.py are skipped

Missing pydantic/mcp/fastapi

pip install -r requirements.txt

Chart doesn't render in Streamlit

Backend's OUTPUT_DIR isn't reachable at /output/...

Confirm output/ exists and the backend mounted it (it does, in main.py, at startup)

mcp still shows as skipped after pip install -r requirements.txt

Check pip show mcp — if not found, likely a version-compatibility gap between mcp and a very new Python version (e.g. 3.14)

pip install mcp directly and read the error; if it's a compatibility issue, create the venv with Python 3.12 or 3.13 instead: py -3.12 -m venv .venv

.sh scripts don't run in Windows PowerShell

PowerShell doesn't execute bash scripts natively

Use the .ps1 equivalents in scripts/ (e.g. .\scripts\run_backend.ps1), or run the underlying command shown inside each script directly

PowerShell refuses to run .ps1 scripts ("running scripts is disabled")

Default PowerShell execution policy

Run once per session: Set-ExecutionPolicy -Scope Process -ExecutionPolicy RemoteSigned, then retry

http://127.0.0.1:8000/ shows {"detail":"Not Found"}

Expected — there is no route at /

Use /health or /docs instead


License

Provided as-is for portfolio/educational use.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    F
    maintenance
    Enables comprehensive stock market analysis with portfolio management, technical indicators, dividend tracking, sector analysis, risk metrics, and price alerts. Provides real-time stock data, trend analysis, and investment insights through natural language interactions.
    -
  • A
    license
    Not graded
    quality
    Not graded
    maintenance
    Provides real-time financial data from Yahoo Finance, enabling stock price lookups, historical data analysis, company information retrieval, and multi-stock comparisons through natural language queries.
    -
  • F
    license
    Not graded
    quality
    D
    maintenance
    Provides tools for stock data retrieval, historical analysis, and market comparison using yfinance. It features robust guardrails to ensure secure interactions and blocks restricted content like investment advice.
    -
  • F
    license
    Not graded
    quality
    C
    maintenance
    Provides yfinance-based quant tools for stock price lookups, fundamentals, metric comparisons, price history statistics, and earnings dates, enabling natural-language stock questions.
    -