Skip to main content
Glama
bvenkatesulu909

Mini CRM MCP

README.md
# Mini CRM MCP

A complete, end-to-end **Model Context Protocol** server over `crm/leads.db` (40 sales leads). An
MCP client or LLM can ask "give me the funnel by stage", "what's in Priya's pipeline", or "what's
our weighted forecast" and get structured answers without ever writing SQL.

This is a *practical* server (like the sibling `Mcp_venki` is a *teaching* one). It replaced an
earlier StudentAnalytics prototype — same architecture, pointed at real CRM data.

Pipeline stages: **New → Contacted → Qualified → Proposal → Won / Lost**.

## What it exposes

### Tools (model-callable)
| Tool | Purpose |
|------|---------|
| `pipeline_summary()` | The funnel: count + total value per stage |
| `list_owners()` | Every rep with lead count and total pipeline value |
| `list_leads(status?, owner?, source?, min_value?, limit=25)` | Filtered lead list (all filters ANDed) |
| `search_leads(query, limit=25)` | Free-text search across name / company / email |
| `get_lead(lead_id)` | One full lead record + close probability |
| `owner_leaderboard()` | Per-rep won / open / weighted-forecast value |
| `forecast()` | Weighted revenue forecast; streams progress via `ctx` |

### Resources (loadable read-only data)
- `crm://schema` — the table schema + stage order
- `crm://lead/{lead_id}` — a single lead as a readable card (URI template)

### Prompt
- `pipeline_review(owner?)` — template that asks the model to write a pipeline review

### The weighted forecast
Each stage has a close probability (New 10%, Contacted 25%, Qualified 50%, Proposal 70%, Won 100%,
Lost 0%). `forecast()` and `owner_leaderboard()` multiply each lead's value by its stage probability
and sum — a standard sales-forecasting method. On the current data this yields ≈ **$396,755**
weighted against a **$942,310** raw pipeline.

## Safety

The SQLite connection is opened with `mode=ro`, so the server **physically cannot write** to the
DB — the engine rejects any mutation. Every query is a parameterized `SELECT`; tool arguments are
bound, never string-formatted, so they can't inject SQL. A test asserts the read-only guarantee.

## Run it

Use any Python (3.10+) that has the `mcp` SDK installed:

```
pip install -r requirements.txt

# 1. Drive it end-to-end with a hand-written client (no Claude, no config):
python client_demo.py

# 2. Run the protocol tests (launch the real server over stdio):
python -m pytest test_server.py -q
```

A demo database ships in `data/leads.db`, so the server runs out of the box.
`CRM_DB` (env var) overrides the database path to point at your own leads table.

## Registered in Claude

Added to the project `.mcp.json` as **`MiniCRM`**, pointing at the hermes-venv Python and the repo's
`crm/leads.db`. Restart the app / reconnect MCP servers to pick it up, then the tools appear as
`mcp__MiniCRM__*` and the prompt as `/mcp__MiniCRM__pipeline_review`.

## Files

- `server.py` — the MCP server (7 tools, 2 resources, 1 prompt)
- `client_demo.py` — hand-written stdio client that exercises every primitive
- `test_server.py` — 8 protocol-level tests (all passing)
- `data/leads.db` — bundled demo database (40 synthetic leads)
- `requirements.txt` / `pytest.ini`
- `docs/` — the illustrated deep-dive PDF and its HTML source

## Gotchas (carried over from building the sibling servers)

1. **FastMCP returns a `list` as one content-block per item**, and puts the whole value in
   `result.structuredContent` (list/scalar wrapped as `{"result": ...}`, dict returns as-is).
   Read `structuredContent`, not `content[0].text`, or you only see the first element.
2. **`session.read_resource(uri)` returns a result with `.contents`**, not a 2-tuple.
3. **Don't hold the MCP session in a `pytest_asyncio` async-generator fixture** — anyio's cancel
   scope is finalized in a different task and every test errors in teardown. Use `async with`
   inside the test body (see `test_server.py`).