Multi-Agent Research Pipeline · Jan 2026 to present
FinRobot
Type a stock ticker, get a formatted equity research report. Behind it are three MCP servers running three different model providers, each picked for the one thing it's genuinely best at.
- Role
- Lead contributor, MCP tooling & services
- Team
- 6 contributors, RIT AI Club
- Stack
- Python · MCP · FastAPI · React
- Code
- GitHub ↗
What it does
This is the explanation I use when demoing it, because it's the one that actually lands with people who don't write software:
You type in a stock ticker. One bot goes out and gathers the research. It hands that to the main bot, which decides what's needed and passes the work along: to a graph maker that draws the charts, and to a final bot that formats everything into a clean document. What comes back is a PDF you could actually hand to someone.
Three phases, each handled by something specialised. That framing survives contact with a non-technical audience, and it also happens to be an accurate description of the architecture rather than a simplification of it.
Why MCP, and not just API calls
The honest answer to "why not call the APIs directly in Python" is that we could have; for a single-model pipeline it would have been less code. The reason MCP earns its place here is that this isn't a single-model pipeline.
Research, charting, and formatting are genuinely different problems, and the best tool for each is different. The Model Context Protocol lets each one run as its own server, exposing its capabilities through a consistent interface, so the orchestrator can call them without knowing or caring what's behind them. Swapping the model inside one server doesn't touch the other two.
mcp_servers:
- name: "research" # Perplexity: search-native retrieval
- name: "chart_generation" # Matplotlib: deterministic, no model
- name: "formatting" # Claude: document layout & styling
orchestrator: Gemini # tool calling and coordination
Model routing by task
- Perplexity for research. It's search-native, built to retrieve and cite current information. Asking a general-purpose model to recall market data from training weights is the wrong shape of question entirely.
- Gemini as orchestrator. It holds the conversation, decides which tools to invoke, and sequences the phases.
- Claude for formatting. Turning a pile of research and charts into well-structured HTML that survives conversion to PDF is a writing-and-layout problem, and that's where it's strongest. This server originally ran on Gemini; moving it was a straight quality decision, and the superseded implementation has since been deleted rather than left in the tree to rot.
- Matplotlib for charts, deliberately not a model. Charts have exactly one correct answer given the data. A deterministic library gets it right every time for free; an LLM would be slower, cost money, and occasionally invent a data point. The interesting judgment in an AI project is often knowing which parts shouldn't be AI.
The trade-off
Three servers and three providers means three sets of credentials, three failure modes, and a process-management story that a single script wouldn't have. In exchange each phase is independently swappable and testable, and we could replace the formatting model without regression risk to research. For a project whose stated purpose is learning how agent architectures fit together, that's the right side of the trade.
Things that only show up when you run it
Rate limits are an architecture problem
The orchestrator sits in the middle of every request, which makes it the first thing to hit a rate limit. Gemini returning HTTP 429 mid-pipeline doesn't just fail one call; it strands a report halfway through, after the research phase has already been paid for.
Sends now retry with exponential backoff: five attempts, starting at two seconds and doubling. It's a small piece of code that turns a class of hard failure into a slow success, and it only became obviously necessary once several people were demoing simultaneously.
PDF conversion is its own problem
Getting a report to look right in a browser is not the same as getting it to look right on a page. Export runs through Playwright, driving a real browser engine to render the HTML and print it, because that's the only reliable way to get output that matches what you saw on screen.
The charts can't go to the model
The formatting server has to place charts inside a document written by Claude, and the obvious way to do that is to show Claude the charts. That doesn't survive contact with the numbers: a single chart PNG is roughly 67,000 input tokens. Two of them and the request budget is gone before any research text is attached, and a model handed base64 has a habit of trying to hand it back.
So the images never leave the Python side. Claude receives only the chart
captions and is told to write CHART_PLACEHOLDER_0,
CHART_PLACEHOLDER_1 and so on where each chart belongs; the
server swaps those tokens for real <img> tags after
generation returns. The model does layout, which is what it's good at, and
never touches the bytes.
The failure mode is worth naming: if the model omits a placeholder, nothing errors. You get a valid report with a chart-shaped hole in it, so a missing placeholder now logs a warning rather than passing silently.
It won't chart numbers it doesn't have
Price data comes from yfinance. Early on, a failed fetch fell
back to a random walk so the pipeline could keep running, which meant a
broken data source produced a report that looked exactly like a working one:
confident, formatted, and made up.
That default is now inverted. A failed price fetch stops the
run. Synthetic prices require an explicit
--allow-synthetic-prices flag, and when they're used the chart
is captioned as simulated inside the report itself, not just in a
log line nobody reads. A financial document that is silently wrong is worse
than no document.
The cleanup nobody demos
After the feature work slowed down I spent a pass on the parts that don't show up in a demo, and it turned up more than I expected.
- A directory name silently disabled a feature. The formatting server sends Claude a gallery of reference layouts to style against. It was looking for
reference_images/; the folder was calledtest_images/. No error, no warning, just every report generated without the design references it was supposed to be using. Renaming it fixed the feature and made its real cost visible: twelve PNGs, about 770 KB, attached to every single call. - Configuration lived in two places. Keys and model settings were split across files with conflicting defaults left over from the old Gemini formatter. It's one
config/config.ymlnow, resolved from each server's own__file__rather than the working directory, because MCP launches them as subprocesses whose cwd isn't guaranteed. Environment variables override it, so a deployment never has to write secrets to disk. - The dependency list was fiction. It claimed
openai>=1.0while running 3.8 andfastmcp>=2.0while running 3.2, listed a package that doesn't exist, and omitted five that are imported directly. Every floor is now the version actually in use, capped below the next major. - An SDK upgrade removed a parameter I was still passing. Sampling parameters are gone from the Anthropic 1.x request signatures, and the current models reject them outright. Configuration that looks harmless is worse than configuration that errors, so the key is gone rather than quietly ignored.
None of this is interesting to watch. It's also the difference between a project that runs on my machine and one another club member can clone on Monday and have working.
Presenting it
FinRobot was presented at ImagineRIT 2026 to roughly 200–300 attendees across the day, alongside demos at club meetings and tabling events.
The audience at ImagineRIT is deliberately general: students, faculty, industry visitors, and members of the public with no technical background. Explaining a multi-agent MCP pipeline to someone who has never heard of any of those words is a genuinely useful constraint, and it's where the "one bot gathers, one draws, one writes" framing came from. Being forced to describe a system without jargon is a fast way to find out which parts of your own design you don't actually understand.
Where it's going
FinRobot started as an exploration project, a way for the AI Club to learn how agent architectures, MCP, and multi-model pipelines actually behave when you build something real with them. That's still its primary purpose.
The current direction is evolving it toward a practical use case for investors: output someone would genuinely use for research rather than a demonstration that it can be produced. It won't be commercialised. It's a club project, built by club members for learning, and it stays that way.
Six people have contributed to it so far. My work has centred on the MCP tooling and the server-side logic behind it, the layer that has to turn inconsistent, rate-limited, occasionally-disagreeing external sources into something predictable enough for a model to reason over.