Skip to main content
Glama
venkatacloud01

local-mcp-postgres

README.md
# πŸš€ Operation LOCAL-MCP

A fully local Postgres-backed MCP (Model Context Protocol) server, connected two ways:
first to **Claude Desktop** (which speaks MCP natively), then to a **fully local Ollama
model** via a hand-built bridge β€” since Ollama has no native MCP client. Both paths sit
behind a FastAPI + Streamlit chat console.

Built as a diagnostic exercise as much as a build exercise: the core question was
*"where exactly does local-model tool-calling reliability break down?"*, and this
project isolates that question with a small, controlled two-tool schema.

## What it does

Ask questions in plain English about a local inventory/orders database:

- "What's in the inventory?"
- "What's John's total order amount?"
- "How many Widget B do we have?"

The model decides which tool to call, the tool runs a safe parameterized query
against Postgres, and the result gets turned back into a natural-language answer.

## Architecture

```
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     stdio (MCP)     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚Claude Desktop│◄───────────────────►│                  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                     β”‚  server.py       │──► Postgres
                                     β”‚  (FastMCP)        β”‚    (Docker)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   hand-built bridge  β”‚                  β”‚
β”‚  Ollama      │◄───────────────────►│                  β”‚
β”‚ (qwen2.5:7b) β”‚      (bridge.py)    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚
       β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   HTTP   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  FastAPI     │◄────────►│  Streamlit   β”‚
β”‚  (main.py)   β”‚          β”‚  (app.py)    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
```

`server.py` is the single source of truth for the tools β€” both Claude Desktop and
the Ollama bridge talk to the exact same FastMCP server. This was deliberate: it
turns "does Claude Desktop succeed where Ollama fails?" into a clean A/B test,
since the only variable that changes is the model, not the tool definitions.

## Key finding

The starting hypothesis (from prior local-LLM tooling work) was that 7B–12B
models are broadly unreliable at emitting well-formed tool calls. That held for
dense, high-overhead schemas (e.g. Claude Code's tool surface), but **did not
hold here**: `qwen2.5:7b`, given this project's small two-tool schema, correctly
selected the right tool on every test β€” including inferring a sensible default
parameter (`min_quantity: 1`) that wasn't stated in the prompt β€” and never once
emitted malformed tool-call JSON.

Takeaway: the earlier reliability problem looks like it's driven more by
**schema size / context overhead** than by a hard local-model capability
ceiling. Worth revisiting the `claude-code-router` / hosted-API tradeoff with
that in mind β€” schema simplification may solve more of the problem than
switching providers does.

## Setup

```bash
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

docker compose up -d              # Postgres, seeded via init.sql
ollama pull qwen2.5:7b            # or llama3.1:8b β€” confirm `tools` under
ollama show qwen2.5:7b            # Capabilities before relying on it
```

## Running it

**Path 1 β€” Claude Desktop (MCP native):**
Add an entry to `claude_desktop_config.json` pointing at `src/server.py` using
your venv's Python (see repo history / commit log for the exact config that
worked after a couple of path-resolution false starts β€” see Gotchas below).

**Path 2 β€” Local Ollama console:**
```bash
# terminal 1
uvicorn src.main:app --reload

# terminal 2
streamlit run src/app.py
```
Then open the Streamlit URL and ask questions in the chat box.

## Gotchas hit along the way (kept for the next person, including future me)

- **`python -m src.server` works from a terminal but not from Claude Desktop's
  launched subprocess** β€” its `cwd` handling doesn't reliably put the project
  root on Python's module search path for `-m` resolution. Fix: point
  `claude_desktop_config.json` directly at the script file
  (`args: ["/abs/path/src/server.py"]`) instead of using `-m`.
- **`tool.inputSchema` vs `tool.input_schema`** β€” the wire-protocol field name
  and the installed `mcp` SDK's Python attribute name don't match; the SDK
  uses `snake_case`. Worth checking installed SDK attribute names directly
  rather than assuming they mirror the protocol spec.
- **`init.sql` only runs against Postgres on a *fresh* Docker volume.** If
  you've already got a `pgdata` volume from a prior run, editing `init.sql`
  and re-running `docker compose up` silently does nothing β€” you need
  `docker compose down -v` first to force re-seeding.
- **Model function-call arguments arrive as either a dict or a JSON string**
  depending on the model/quantization β€” `bridge.py`'s loop handles both.

## Known limitations

- **No multi-turn memory.** Each `/chat` request (and each `run_tool_call_loop`
  call) starts a fresh conversation with no history from prior turns. The
  Streamlit UI *displays* prior turns for readability, but doesn't send them
  back to the backend β€” a deliberate scope cut for this iteration, not an
  oversight.
- Each request respawns the MCP server subprocess rather than keeping one
  long-lived session β€” simpler and correct for this scale, but adds latency
  you'd want to remove for anything beyond a local demo.
- Read-only by design: both tools run fixed, parameterized `SELECT`s. No
  write/update tools exist, intentionally, to keep the safety surface small.

## Stack

Python 3.12 Β· FastMCP Β· MCP SDK Β· Ollama (`qwen2.5:7b`) Β· PostgreSQL (Docker) Β·
FastAPI Β· Streamlit Β· psycopg2

## Repo layout

```
local-mcp-postgres/
β”œβ”€β”€ README.md
β”œβ”€β”€ MISSION_PLAN.md        # phase-by-phase build log / checklist
β”œβ”€β”€ docker-compose.yml
β”œβ”€β”€ init.sql
β”œβ”€β”€ requirements.txt
└── src/
    β”œβ”€β”€ server.py           # FastMCP server: fetch_inventory, fetch_order_total
    β”œβ”€β”€ bridge.py            # Ollama-MCP bridge + tool-call loop
    β”œβ”€β”€ main.py               # FastAPI backend
    └── app.py                 # Streamlit console
```