Skip to main content
Glama
dev1nagaich

MCP Banking Intelligence Server

by dev1nagaich
README.md
# MCP Banking Intelligence Layer (Lab 1 + Lab 2)

This project turns a pile of financial documents into AI-generated **investment
briefs**. It is built in two stages:

- **Lab 1 — RAG + Multi-Agent (AutoGen):** a `ResearchAgent` writes a brief from
  data retrieved out of a ChromaDB vector store, and a `CriticAgent` reviews it
  until it meets a quality checklist.
- **Lab 2 — MCP Banking Intelligence Layer:** we take Lab 1's tangled retrieval
  logic and expose it as a clean **MCP server** of reusable "banking tools."
  A LangChain + Ollama agent then acts as the **MCP client**, discovering and
  calling those tools at runtime to build the brief — and reuses the Lab 1
  `CriticAgent` for the final review.

Lab 2 **reuses the Lab 1 data** (`chroma_db/`). You do **not** re-run `ingest.py`.

---

## 1. What is MCP, and why does it matter?

**MCP (Model Context Protocol)** is an open standard from Anthropic that defines
*how an AI application talks to external tools and data*. Think of it as a
universal adapter — like USB-C for AI tools.

Without MCP, every agent hardcodes its own data access. In Lab 1, the agent
reaches **directly** into ChromaDB. If a new team wants a loan advisor or a fraud
analyst, they must copy and modify the whole codebase.

With MCP, the data/tool logic lives behind a **standard server interface**:

| Concept | What it is | In this lab |
| --- | --- | --- |
| **MCP Server** | Exposes *tools* (typed functions) over a standard protocol | `server.py` — 4 banking tools + 1 bonus |
| **MCP Client** | Discovers and calls those tools at runtime | `client.py` — LangChain/Ollama agent |
| **Transport** | The channel between client and server | `stdio` (client launches server as a subprocess) |
| **Tool** | A typed, documented function the LLM can call | `search_financials`, `get_company_profile`, ... |

**Why this is powerful:** the agent code never names a database. Tomorrow you can
swap ChromaDB for a live Bloomberg feed by editing only `server.py` — the agent
in `client.py` does not change. Tools become *modular and shareable* across any
MCP-compatible client (Claude Desktop, Cursor, your own agent, etc.).

### Key idea: who thinks vs. who fetches

- The **server tools are "dumb" on purpose.** They only retrieve and process
  data and return JSON. They **never call an LLM**.
- The **client agent does the thinking.** The LLM looks at the available tools,
  decides which to call (this is *not* hardcoded), reads the JSON results, and
  writes the brief.

---

## 2. Architecture

### Lab 1 pipeline (RAG + AutoGen)

```
data/financial/*.txt
        │  ingest.py (run ONCE)
        ▼
   ┌──────────────┐     retriever.py        ┌──────────────┐   APPROVED?   ┌──────────┐
   │  ChromaDB    │ ───────────────────────►│ ResearchAgent│ ◄───────────► │ Critic   │
   │ financial_docs│   (semantic retrieval)  │  (drafts)    │   feedback    │ Agent    │
   └──────────────┘                          └──────────────┘               └──────────┘
                                                     │
                                                     ▼
                                          output/brief_{company}.txt
```

### Lab 2 pipeline (MCP)

```
                    ┌──────────────────────── client.py (MCP CLIENT) ───────────────────────┐
                    │                                                                        │
   --company Apple ─┤  LangChain ReAct agent (Ollama: llama3.1)                              │
                    │        │  "which tool should I call?"  (LLM decides, not hardcoded)     │
                    │        ▼                                                                │
                    │   langchain-mcp-adapters  ──── stdio ───►  server.py (MCP SERVER)       │
                    │        ▲                                       │  @mcp.tool() functions  │
                    │        │   JSON tool results                   ▼                         │
                    │        └───────────────────────────────  reads chroma_db/financial_docs │
                    │        │                                  (reuses retriever.py)          │
                    │        ▼                                                                 │
                    │   draft brief ──► Lab 1 CriticAgent (one review round) ──► revise        │
                    │                                                                          │
                    └──────────────────────────┬───────────────────────────────────────────┘
                                               ▼
                                  output/mcp_brief_{company}.txt
```

---

## 3. The MCP tools (`server.py`)

All tools are defined with `@mcp.tool()`, have typed parameters, return
JSON-serializable dicts, and return `{"error": ...}` instead of crashing on bad
input.

| Tool | Signature | What it does |
| --- | --- | --- |
| `search_financials` | `(query: str, company: str, max_results: int)` | Semantic search over `financial_docs` for a company. |
| `get_company_profile` | `(company: str)` | Aggregated metadata: doc count, available years, doc types. |
| `compare_companies` | `(companies: list[str])` | Side-by-side financial summary of two companies. |
| `generate_risk_flags` | `(company: str)` | Scans risk-related docs and returns flagged risk keywords. |
| `get_market_news` *(bonus)* | `(company: str)` | Up to 3 recent headlines via DuckDuckGo (network, not LLM). |

Missing company → `{"error": "company not found", "company": "..."}`.

> Note: `stdio` uses stdout as the protocol channel, so `server.py` logs only to
> **stderr** — it never prints to stdout.

---

## 4. Project structure

```
MCP/
├── data/financial/*.txt     # Lab 1 source documents
├── ingest.py                # Lab 1 — builds chroma_db (run ONCE, do not re-run)
├── chroma_db/               # Lab 1 vector store (reused by Lab 2)
├── retriever.py             # Lab 1 — semantic retrieval (reused by server.py)
├── agents.py                # Lab 1 — ResearchAgent + CriticAgent (CriticAgent reused)
├── main.py                  # Lab 1 — AutoGen pipeline entry point
├── server.py                # Lab 2 — NEW: MCP server (4 tools + bonus)
├── client.py                # Lab 2 — NEW: LangChain MCP client + CriticAgent
├── output/                  # briefs are written here
├── requirements.txt
└── README.md                # this file
```

---

## 5. Setup

**Prerequisites**

1. Conda env `agentic` activated: `conda activate agentic`.
2. [Ollama](https://ollama.com) running locally with the models pulled:
   ```bash
   ollama pull llama3.1:latest
   ollama pull nomic-embed-text:latest
   ```
3. Lab 1 already ingested (`chroma_db/` exists). If not, run `python ingest.py` **once**.

**Install Lab 2 dependencies**

```bash
pip install mcp langchain langchain-community langchain-mcp-adapters langchain-ollama langgraph python-dotenv
# or simply:
pip install -r requirements.txt
```

---

## 6. How to run

**Lab 1 (AutoGen):**
```bash
python main.py --company Apple --year 2024
```

**Lab 2 (MCP):**
```bash
python client.py --company Apple
```
`client.py` automatically launches `server.py` as a subprocess — you do not start
the server yourself.

**Quick server smoke test** (optional — it will wait for a client on stdin):
```bash
python server.py     # Ctrl+C to stop
```

Output is written to `output/mcp_brief_{company}.txt`.

Companies available in the dataset: **Apple, Amazon, Microsoft, Nvidia, Tesla**
(years 2023–2024).

---

## 7. How the constraints are satisfied

- **Server and client are separate files** — `server.py` and `client.py`.
- **Tools never call the LLM** — they only query/aggregate ChromaDB and return JSON.
- **Tool routing is not hardcoded** — the LLM in the ReAct agent decides which
  MCP tools to call.
- **Reuses the existing `financial_docs` collection** — no new ChromaDB is created;
  `compare_companies` even reuses Lab 1's `retriever.py`.
- **CriticAgent runs before saving** — `client.py` calls the Lab 1 `CriticAgent`
  for one review round on every brief.
- **Errors return JSON** — invalid input yields `{"error": ...}`, never an
  unhandled exception.

---

## 8. Troubleshooting

**`ModuleNotFoundError: No module named 'autogen'`**
The Lab 1 code uses the *classic* AutoGen API. The PyPI `pyautogen` name now
redirects to Microsoft's new `autogen-agentchat` packages, which do **not** ship
the `autogen` module. Install the classic-API fork instead:
```bash
pip install ag2
python -c "import autogen; from autogen import AssistantAgent; print('classic AutoGen OK')"
```

**The brief is full of numbers that aren't in our data (hallucination)**
This means the LLM did *not* actually call the MCP tools — it described the calls
in text and made up the answer. `client.py` now prints
`MCP tools actually called by the LLM: ...` and warns if the list is empty.

Confirm whether Ollama tool-calling works at all:
```bash
python diagnose_tools.py
```
- If `TOOL_CALLS` is empty, your Ollama server and/or `langchain-ollama` are too
  old to emit structured tool calls. Upgrade both:
  ```bash
  pip install -U langchain-ollama langchain-core langgraph
  # update the Ollama server itself (Linux):
  curl -fsSL https://ollama.com/install.sh | sh
  ollama --version          # need a recent version with tool-call support
  ```
- Tool calling needs a tool-capable model. `llama3.1` supports it; if it is still
  flaky, try `ollama pull qwen2.5:7b-instruct` and set `MODEL` in `client.py`.

**`create_react_agent ... LangGraphDeprecatedSinceV10`**
Harmless warning — it still works. (It will move to
`from langchain.agents import create_agent` in a future major version.)

---

## 9. Bonus / extensions

- ✅ `get_market_news()` using DuckDuckGo (implemented).
- 💡 Expose the server over **SSE** transport instead of stdio.
- 💡 Add a `portfolio_summary` tool that calls `compare_companies` +
  `generate_risk_flags` in sequence for a list of companies.