Skip to main content
Glama
README.md
# lft-reader-mcp

An [MCP](https://modelcontextprotocol.io) server that lets an AI assistant read
COVID-19 lateral flow tests from photographs.

It exposes a deployed computer-vision pipeline — YOLOv8-OBB for oriented
cassette detection, MobileNetV3-Small for classification, and a classical
OpenCV line counter for corroboration — as two tools any MCP client can call.

Research and demonstration use only. **Not a medical device.**

---

## Why this exists

The model already runs as a FastAPI service on Google Cloud Run
([**lft-reader-api**](https://github.com/Dareen-to/lft-reader-api) — source,
ONNX graphs and deployment), which means a human can upload a photo and read
the verdict. This server closes the last gap:
it lets an AI assistant do the same thing as part of a larger task, without a
person copying files between windows.

```
Claude / Cursor  ──MCP──►  this server  ──HTTPS──►  Cloud Run
                                                     ├── YOLOv8-OBB   (detect)
                                                     ├── MobileNetV3  (classify)
                                                     └── OpenCV       (count lines)
```

No model weights live here. The server is a thin, well-behaved client: it
validates input before spending a network call, gives the cold start enough
room to finish, and returns "no cassette found" as a *result* rather than an
error, because a well-formed request that finds nothing is not a failure.

## Install

**Windows (PowerShell)**
```powershell
py -m pip install -r requirements.txt
py test_server.py
```

**macOS / Linux**
```bash
python3 -m pip install -r requirements.txt
python3 test_server.py
```

`py` is the Windows Python launcher. Use it rather than `python3`, which on
Windows is usually intercepted by a Microsoft Store shortcut.

### SDK versions

Works with both major versions of the Python MCP SDK. Version 2.0 renamed
`FastMCP` to `MCPServer` and `Tool.inputSchema` to `Tool.input_schema`; the
server resolves the import at load time and the tests read either attribute,
so there is no need to pin an old major version. Verified against **mcp 2.2.0**
and **mcp 1.30.0**.

## Wire it into a client

**Claude Desktop, Windows** — `%APPDATA%\Claude\claude_desktop_config.json`.
Backslashes must be doubled inside JSON:

```json
{
  "mcpServers": {
    "lft-reader": {
      "command": "py",
      "args": ["C:\\Users\\YOURNAME\\lft-mcp\\server.py"]
    }
  }
}
```

**Claude Desktop, macOS** — `~/Library/Application Support/Claude/claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "lft-reader": {
      "command": "python3",
      "args": ["/absolute/path/to/lft-mcp/server.py"]
    }
  }
}
```

**Cursor** — the same block in `.cursor/mcp.json` in your project.

Example files for both are in this repo.

Restart the client. Then ask it, in plain language:

> Read the lateral flow test in ~/Desktop/test.jpg

## Tools

### `read_lateral_flow_test(image_path)`

Reads a test from a local image and returns the verdict.

```json
{
  "detected": true,
  "result": "positive",
  "confidence": 0.9811,
  "decision_source": "cnn+lines agree (positive)",
  "detection_confidence": 0.9084,
  "processing_ms": 523.3
}
```

Send a photo of the test **in a scene** — on a table, in a hand. The detector
was trained to find a cassette within a wider frame, so a tight crop of the
cassette alone returns `detected: false` with a note saying so.

### `check_reader_health()`

Reports whether the service is up and its models are loaded. Useful before a
batch, and to tell a service problem apart from a problem with one photo.

## Configuration

| Variable | Default |
|---|---|
| `LFT_API_BASE` | `https://lft-reader-xufdqxj7ja-uc.a.run.app` |

Point it at a local instance:

```powershell
$env:LFT_API_BASE="http://localhost:8080"; py server.py    # Windows
```
```bash
LFT_API_BASE=http://localhost:8080 python3 server.py       # macOS / Linux
```

## Known limitations

- **Cold starts.** The service scales to zero to stay inside the free tier, so
  the first call after ~15 minutes idle takes a few seconds while the models
  load. Warm calls are around 500 ms. The client allows 90 seconds before
  giving up.
- **Pre-cropped images are not detected.** See above — this is a property of
  how the detector was trained, not a bug.
- **`invalid` is the weakest class.** It had only 5 held-out examples and 50
  training images.
- Accuracy figures for the underlying models: detector mAP50 0.993
  (mAP50-95 0.857); classifier 96.3% on 81 held-out images — 60 positive,
  16 negative, 5 invalid, macro-F1 0.898. The classifier number should always
  be read with that class imbalance in mind.

## Tests

```powershell
py test_server.py        # Windows
python3 test_server.py   # macOS / Linux
```

Checks that the server loads, declares both tools with usable descriptions and
schemas, and rejects missing files, unsupported formats and empty files before
making any network call.

## The service behind it

[**lft-reader-api**](https://github.com/Dareen-to/lft-reader-api) is the
pipeline this server calls: the ONNX export and parity verification, the
NumPy reimplementation of Ultralytics' OBB decoding, the FastAPI service and
the Cloud Run deployment. Its README documents the measured accuracy numbers
quoted above, and the train/inference preprocessing bug found while verifying
the export.

Run this server against your own instance of it by setting `LFT_API_BASE`.

## Licence

MIT for this server — see [LICENSE](LICENSE). It contains no model weights and
links no AGPL code; it is an HTTP client that happens to call an AGPL-licensed
service, which does not make it a derivative work of one.

The models and the service are governed by their own licences: the detector is
YOLOv8, which is AGPL-3.0, so
[lft-reader-api](https://github.com/Dareen-to/lft-reader-api) is AGPL-3.0 too.

(Not legal advice — if this matters commercially, check with someone qualified.)