Skip to main content
Glama
README.md
# mata-kadalz 🦎

Lizard Eyes — local **vision MCP server** backed by **Qwen3-VL-4B** running on `llama-server`.
Gives any MCP client (opencode, Claude, Codex, ...) one tool: `vision.inspect(image_path, task)`.

```
client -> vision.inspect -> mata-kadalz server.py (stdio or streamable HTTP)
                            -> HTTP POST http://<llama-server>:9931/v1/chat/completions
                            -> llama-server (Qwen3-VL-4B GGUF + mmproj)
```

`mata-kadalz` is a thin MCP layer. The inference runtime (`llama-server` from llama.cpp) and the
model are **external dependencies you install yourself**; this repo never downloads or bundles them.

## Quick start

> **Install from source.** This project is not yet published to PyPI, so the package is
> installed from this repository (see [Setup guides](#supported-setups) for your platform).

1. Install Python **>= 3.11**.
2. Install **llama-server** (llama.cpp) and the **Qwen3-VL-4B** model + mmproj — external,
   from official sources (links in the [Model](#model) and [Setup guides](#supported-setups) sections).
3. Start `llama-server` and confirm it is healthy: `curl http://localhost:9931/health` → `{"status":"ok"}`.
4. Install `mata-kadalz` from this repo into a venv:
   ```bash
   git clone https://github.com/kadalzbaiq/mata-kadalz.git
   cd mata-kadalz
   bash scripts/install.sh          # POSIX (Linux/macOS/WSL); Windows: see docs/SETUP-windows.md
   ```
5. Confirm the MCP server can reach llama-server: `.venv/bin/mata-kadalz --health`.
6. Register `mata-kadalz` in your MCP client — see [Client registration](#client-registration).

## Supported setups

| Host | llama-server runs on | Setup doc |
|---|---|---|
| Windows (native) | Windows | [docs/SETUP-windows.md](docs/SETUP-windows.md) |
| Linux (native) | Linux | [docs/SETUP-linux.md](docs/SETUP-linux.md) |
| macOS (native) | macOS | [docs/SETUP-macos.md](docs/SETUP-macos.md) |
| WSL2 on Windows | Windows host (native) | [docs/SETUP-hybrid-wsl.md](docs/SETUP-hybrid-wsl.md) |
| Any / remote / custom | anywhere reachable over HTTP | [docs/SETUP-modular.md](docs/SETUP-modular.md) |

## Model

- Model: `Qwen3VL-4B-Instruct-Q4_K_M.gguf` (LLM, ~2.5 GB)
- Vision encoder: `mmproj-Qwen3VL-4B-Instruct-F16.gguf` (~800 MB)
- Source: `Qwen/Qwen3-VL-4B-Instruct-GGUF`
  - https://huggingface.co/Qwen/Qwen3-VL-4B-Instruct-GGUF/resolve/main/Qwen3VL-4B-Instruct-Q4_K_M.gguf
  - https://huggingface.co/Qwen/Qwen3-VL-4B-Instruct-GGUF/resolve/main/mmproj-Qwen3VL-4B-Instruct-F16.gguf

Do not change the quant or mmproj combo without re-validating; this is the verified working setup.

## Layout

```
server.py                 # MCP server (stdio + streamable HTTP), single file
config/config.json        # optional overrides (empty = auto)
scripts/install.sh        # install the mata-kadalz package only
docs/                     # per-host setup guides
tests/                    # pytest (no llama-server needed)
runtime/                  # logs + image cache (gitignored)
```

## Client registration

Replace `/path/to/mata-kadalz/.venv/bin/mata-kadalz` with the real path on your machine.

stdio (local — same machine as the images):

```bash
# claude
claude mcp add vision -- /path/to/mata-kadalz/.venv/bin/mata-kadalz
# codex
codex mcp add vision -- /path/to/mata-kadalz/.venv/bin/mata-kadalz
```

streamable HTTP (remote — server machine may differ from client):

```bash
# claude
claude mcp add --transport http vision http://127.0.0.1:9932/mcp
# codex
codex mcp add vision --url http://127.0.0.1:9932/mcp
```

opencode (config in `opencode.json`):

```jsonc
{
  "mcp": {
    "vision": {
      "type": "local",                 // stdio
      "command": ["/path/to/mata-kadalz/.venv/bin/mata-kadalz"]
    }
    // or "type": "remote", "url": "http://127.0.0.1:9932/mcp"  // streamable HTTP
  }
}
```

## Usage

One tool: **`vision.inspect`** — takes `image_path` (absolute path on the machine where the MCP server runs) and `task` (what to analyze). Returns structured JSON:

```json
{ "success": true, "summary": "...", "details": "", "warnings": [], "cache_hit": true }
```

On failure it returns `is_error: true` with a machine-readable code, e.g. `IMAGE_NOT_FOUND`, `IMAGE_NOT_SUPPORTED`, `IMAGE_PATH_NOT_ALLOWED`, `LLAMA_SERVER_URL_NOT_SET`, `LLAMA_SERVER_TIMEOUT`, `LLAMA_BUSY`, `INVALID_VISION_RESPONSE`.

The server also embeds a **system prompt** (`SYSTEM_PROMPT`) instructing the vision model how to structure its output; the prompt is sent on every inference request.

### HTTP vs stdio — where files must live

- **stdio (local):** the MCP client and the server share one machine, so `image_path` is a path on that machine.
- **streamable HTTP (remote):** the client and the server may be on different machines — `image_path` is resolved **on the server machine**, not the client's. Set `VISION_IMAGE_ROOTS` to restrict which directories the server will read from (strongly recommended for a network-exposed server).

### Security for HTTP deployments

If you expose the server over the network:

- **Set `VISION_IMAGE_ROOTS`** so the server can only read from the directories you choose. With it unset, the server can read any path on the host.
- **Bind to a safe interface.** `--host 127.0.0.1` (the default) only accepts local connections. For LAN/remote access, prefer a VPN or a firewall rule over binding `0.0.0.0` on a public interface.
- **No authentication is built in.** Put the endpoint behind an authenticated reverse proxy or your VPN. The HTTP transport speaks raw MCP; there is no token/user layer.
- Allow inbound traffic only on the ports you use: **9931** (llama-server) and **9932** (mata-kadalz HTTP transport).

## Configuration

Config precedence: `DEFAULTS` < `config/config.json` (empty values skipped) < environment variables.

| Key | Default | Notes |
|---|---|---|
| `LLAMA_SERVER_URL` | `http://<gateway-ip>:9931` | Auto-detects WSL gateway IP; set explicitly to override. If WSL gateway detection fails and no URL is set, inference returns `LLAMA_SERVER_URL_NOT_SET` |
| `VISION_RUNTIME_DIR` | `<repo>/runtime/vision` | Relative paths resolve against repo root |
| `VISION_CACHE_DIR` | `<repo>/runtime/vision/cache` | |
| `VISION_LOG_DIR` | `<repo>/runtime/vision/logs` | |
| `VISION_TIMEOUT_SECONDS` | `180` | CPU inference takes 10–130 s per call |
| `VISION_MAX_IMAGE_SIZE` | `20971520` | 20 MB |
| `VISION_MODEL_ID` | `qwen3-vl` | |
| `VISION_IMAGE_ROOTS` | *(empty = any path)* | Restrict readable image dirs. JSON array or comma-separated, relative to repo root. Symlinks and `..` escapes resolve and are rejected |
| `VISION_MAX_QUEUE` | `4` | Bounded inference queue; beyond this, calls fail fast with `LLAMA_BUSY` |

Example override in `config/config.json`:

```json
{ "LLAMA_SERVER_URL": "http://192.168.64.1:9931" }
```

Restrict a network-exposed server to one directory:

```json
{ "VISION_IMAGE_ROOTS": ["/srv/shared-images"] }
```

## Health check

Confirm the MCP can reach llama-server before wiring up a client:

```bash
.venv/bin/mata-kadalz --health
```

Prints platform, resolved `LLAMA_SERVER_URL`, and a `reachable: true/false` health probe; exits 0 when reachable.

## Caching

Requests are deduplicated by `sha256(image) + task + model`. A cache hit returns instantly without touching llama-server. Failed requests are never cached. Inference is serialized with a process-wide lock (one concurrent call at a time); the queue beyond the lock is bounded by `VISION_MAX_QUEUE` and returns `LLAMA_BUSY` when full.

**Cache invalidation:** changing `VISION_MODEL_ID` invalidates the cache automatically, because the model id is part of the cache key — stale answers from an older model are never served.

**Cancellation:** if the MCP client cancels a request mid-inference, the server releases its lock and queue slot immediately, discards the in-flight result (never caches a partial one), and the error propagates without crashing the server. The underlying llama-server call keeps running in the background; its result is ignored.

## Self-check

```bash
echo '{"image_path":"/path/to/img.png","task":"describe"}' | .venv/bin/mata-kadalz --once
```

Reads one JSON request from stdin, runs it, prints the result, and exits: **0** on success or cache hit, **1** on any error (invalid input, missing file, unreachable llama-server, ...). Useful for scripting and cron-style smoke checks.

## Test

```bash
.venv/bin/python -m pytest
```

No llama-server required — tests cover config, file validation, magic bytes, cache logic, image-root policy, cancellation, `--once` exit codes, WSL gateway detection, bounded queue, and a real streamable-HTTP session over uvicorn.

## HTTP transport dependency

`uvicorn` is only needed for `--transport http`. It already ships transitively with the MCP SDK, but it is also declared as an explicit optional extra so the intent is unambiguous:

```bash
# from a source checkout (until PyPI publication)
pip install -e '.[http]'          # or: bash scripts/install.sh then pip install -e '.[http]'
```

> Until the package is published to PyPI, `pip install mata-kadalz` and
> `pip install 'mata-kadalz[http]'` will **not** work. Use a source checkout.

## License

[MIT](LICENSE)

TDQS

A4.3/5.0

Scored across 1 tool

Disambiguation5/5

With only one tool, there is no possibility of confusion between tools. The tool's purpose is clearly defined and unique.

Naming Consistency5/5

The tool name uses a consistent dot-separated namespace (vision.inspect), which is a clear naming convention. Even with a single tool, the pattern is coherent.

Tool Count3/5

The server has a single tool, which is on the low end. While it serves a specific vision inspection purpose, a single tool feels thin and offers limited functionality.

Completeness5/5

For the stated purpose of vision inspection, the tool provides a complete analysis capability. There are no obvious missing operations within the scope of inspecting a local image.

Maintenance

ActivitySlowing
ResponsivenessUnresponsive