Skip to main content
Glama
finnhyun12

genicam-mcp

by finnhyun12
README.md
# genicam-mcp

**Vendor-neutral industrial camera control via GenICam GenTL.**

An MCP server that enumerates, connects to, configures, and captures from industrial
cameras through the **GenICam GenTL standard** — not through a single vendor's SDK.

Point it at any GenTL producer (`.cti`) and you get the camera's **entire GenICam
feature tree**: exposure, gain, trigger, ROI, pixel format, and everything else the
camera exposes.

## Why this exists

Existing camera MCP servers bind to one vendor's SDK (typically `pypylon`, Basler-only)
and expose a single "grab a frame" call. This one goes through the vendor-neutral GenTL
layer instead, so one code path is meant to drive any GenTL-compliant camera — Basler,
HIKrobot, Teledyne FLIR, Allied Vision, XIMEA — regardless of vendor. (Actually tested
producers are listed under [Verified](#verified) below.) It also exposes feature control,
not just capture.

**No vendor SDK is imported.** The only path to the hardware is `harvesters` → `.cti`.
That constraint is the point of the project.

## Install

Requires **Python 3.12** (the `genicam` wheels this depends on are version-specific).

```bash
git clone https://github.com/finnhyun12/genicam-mcp.git
cd genicam-mcp
uv venv --python 3.12
uv sync
```

## Claude Desktop setup

Add to your `claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "genicam": {
      "command": "uv",
      "args": ["--directory", "/absolute/path/to/genicam-mcp", "run", "genicam-mcp"]
    }
  }
}
```

If `uv` is not on your PATH, point at the venv interpreter directly:

```json
{
  "mcpServers": {
    "genicam": {
      "command": "/absolute/path/to/genicam-mcp/.venv/bin/python",
      "args": ["-m", "genicam_mcp.server"]
    }
  }
}
```

On Windows use `.venv\\Scripts\\python.exe` and escape backslashes in JSON.

## Tools

| Tool | Purpose |
|---|---|
| `add_producer(cti_path)` | Register a GenTL producer (`.cti`). Call this first. Multiple producers may be registered. |
| `discover_cameras()` | List cameras: `index`, `vendor`, `model`, `serial`, `tl_type`. |
| `connect(index)` | Open a camera. Required before feature access or capture. |
| `disconnect()` | Release the camera. |
| `list_features(category=None)` | Walk the node map. Returns name, type, readable/writable, current value, range or options. |
| `get_feature(name)` | Read one feature with its type and `min`/`max` (or enum options). |
| `set_feature(name, value)` | Write any GenICam feature — `ExposureTime`, `Gain`, `TriggerMode`, `PixelFormat`, `Width`… Values are cast to the node's type. |
| `grab(save_path=None)` | Capture one frame → PNG. Returns `width`, `height`, `pixel_format`, `saved_path`. |

Feature names are **case-sensitive** and vary by camera. Use `list_features()` to see
what a given camera actually offers rather than guessing.

## Finding a `.cti` producer

Vendor SDKs install their producers and register the directory in the
`GENICAM_GENTL64_PATH` environment variable:

```bash
# Linux / macOS
echo $GENICAM_GENTL64_PATH
```

```powershell
# Windows
$env:GENICAM_GENTL64_PATH
```

Common locations:

| Vendor | Producer |
|---|---|
| Allied Vision Vimba X | `.../Vimba X/cti/VimbaGigETL.cti`, `VimbaUSBTL.cti` |
| Basler pylon | `.../pylon 6/Runtime/x64/ProducerGEV.cti`, `ProducerU3V.cti` |
| HIKrobot MVS | `.../Common Files/MVS/Runtime/Win64_x64/MvProducerGEV.cti`, `MvProducerU3V.cti` |

If `add_producer` is given a bad path, the error message lists the `.cti` files it
found on your machine.

## Demo without hardware

Allied Vision **Vimba X** ships a *Camera Simulator TL* — a GenTL producer that
presents simulated cameras with real test-pattern frames. No camera required.

1. Install [Vimba X](https://www.alliedvision.com/en/support/software-downloads/vimba-x-sdk/) (free).
2. Register the simulator producer and grab a frame:

```
add_producer("C:\\Program Files\\Allied Vision\\Vimba X\\cti\\VimbaCameraSimulatorTL.cti")
discover_cameras()
connect(0)
set_feature("TestPattern", "GreyVerticalRamp")
grab("frame.png")
```

This is the exact producer path used for the end-to-end verification below. On that
machine it enumerated three simulated cameras; index 0 was
`Camera Simulator (G1-030_VSWIR)` at 656×520 `Mono8`. `TestPattern` accepts `Off`,
`GreyVerticalRamp`, `GreyVerticalRampMoving`, `ColorVerticalBar`, `ColorHorizontalBar`,
and `ColorVerticalBarMoving` — check `get_feature("TestPattern")` on your install rather
than assuming, since the simulator's model list can differ by Vimba X version.

> Note: a vendor's *virtual camera* feature is not the same thing as a simulator
> **transport layer**. Basler's pylon Camera Emulator (`PYLON_CAMEMU`) and HIKrobot's
> MVS Virtual Device both live above the GenTL layer and are visible only through
> their own vendor SDKs — GenTL consumers cannot see them. Vimba X's Camera Simulator
> is itself a `.cti`, which is why it works here.

## Verified

- ✅ End-to-end (enumerate → connect → feature R/W → grab → PNG):
     Allied Vision Vimba X — Camera Simulator TL (GenTL producer)
- ✅ Producer load & camera enumeration:
     Basler pylon ProducerU3V.cti

> genicam-mcp is built against the GenICam GenTL standard and is
> expected to work with any compliant producer (Basler, HIKrobot,
> Teledyne FLIR, Allied Vision, XIMEA, …). Only the producers listed
> above are actually tested; others are unverified — field reports welcome.

## Development

```bash
uv run pytest                    # fake-GenTL suite; no hardware, no SDK
```

The real-producer suite is gated on an env var so day-to-day runs stay fast:

```bash
GENTL_CTI=/path/to/Producer.cti uv run pytest -m real_producer -v -s
```

If `GENTL_CTI` is set but no camera is found, the suite **fails** rather than skipping —
a gate that silently passes is not a gate.

## Known quirks

These are documented because each one *looks* like a broken server when you hit it
cold. Knowing them in advance turns a bug report into a shrug.

- **First `add_producer` can be slow.** Basler's `ProducerU3V.cti` took over 60 s on its
  first load (USB bus enumeration). Nothing is wrong — it is enumerating the bus. Allow
  a generous tool timeout on the first call; a short client timeout will cancel the
  request and can tear down the MCP session with it.
- **A bad producer can take the process down.** `MvFGProducerCML.cti` segfaults
  (`0xC0000005`) when loaded without Camera Link hardware. This happens inside the
  native library, so no Python `try/except` can catch it — register only producers that
  match your hardware.
- **Trigger mode blocks capture.** If `TriggerMode` is `On`, `grab()` waits for a
  trigger and then times out. Set `set_feature("TriggerMode", "Off")` for free-run.

## Implementation notes

Three findings shaped this code. They are recorded here because none of them is
visible from the source alone, and each cost real time to track down.

**A vendor "virtual camera" is not a simulator transport layer.** Basler's pylon Camera
Emulator (`PYLON_CAMEMU`) and HIKrobot's MVS Virtual Device both look like the answer to
"test without hardware" — and both are invisible to GenTL. They live *above* the GenTL
boundary, inside the vendor SDK, so only that vendor's own tools can see them. Measured
on HIKrobot MVS: the vendor SDK enumerated the virtual camera (`MV_VIR_GIGE_DEVICE` → 1
device, and only when run elevated), while `MvProducerGEV.cti` reported 0 through the
same machine. Vimba X's Camera Simulator works because it *is* a `.cti`. The question to
ask of any "virtual camera" feature is not whether it exists but which layer it lives in.

**Import the native extensions at module load, never inside a tool call.** Deferring
`import harvesters` into the tool body seems tidy — it lets the module import cleanly on
machines with no SDK. But FastMCP runs tool functions off the event loop, and loading the
`genicam` native extension there **hangs forever**: the server accepts the
`CallToolRequest` and then simply never answers. A silent hang is far worse than an
immediate error, because the client only sees a timeout and the user has nothing to go
on. So the import happens at module load and the failure reason is stashed and raised, as
a readable message, at first use.

**A GenTL buffer is borrowed, not owned.** `ia.fetch()` hands out a buffer from the
acquisition queue; once it is returned, the memory is recycled. Keeping a numpy view past
that point does not raise — it silently yields whatever the driver wrote next, which in
practice means an all-black frame. This surfaced only against the real Vimba simulator,
never against the fakes, because a fake buffer is just a live numpy array. The fake now
zeroes its data on `__exit__` to model the real lifetime contract, so the failure is
reproducible without hardware, and `test_real_producer.py` asserts the frame is not
uniformly zero. **A capture test that only checks "a PNG exists" passes on a black
image.**

## Scope

One camera at a time; one frame per `grab()`. Simultaneous multi-camera acquisition,
async streaming, GUI, and web serving are deliberately out of scope.

## License

MIT — see [LICENSE](LICENSE).

TDQS

A4.2/5.0

Scored across 8 tools

Disambiguation5/5

Each tool has a clearly distinct purpose: registration (add_producer), discovery (discover_cameras), connection (connect/disconnect), feature inspection (list_features, get_feature), feature modification (set_feature), and image capture (grab). No overlap.

Naming Consistency5/5

All tool names follow a consistent lowercase snake_case verb_noun pattern (e.g., add_producer, discover_cameras, set_feature). No mixing of conventions.

Tool Count5/5

8 tools is well-scoped for a camera control server. Each tool serves a necessary role without redundancy or bloat.

Completeness4/5

Covers the full lifecycle: producer registration, camera discovery, connection, feature configuration, image capture, and disconnection. Minor gap: no built-in support for continuous or burst capture, but single-frame grab and trigger control via set_feature suffice for basic use.

Maintenance

ActivitySlowing
ResponsivenessNo issues