An MCP (Model Context Protocol) server that gives AI agents the ability to see — analyze screenshots, UI mockups, diagrams, and any visual content through a local VLM (Vision Language Model) running on Ollama.
Designed for Kilo Code, but works with any MCP-compatible client.
┌──────────────┐ stdio/MCP ┌──────────────────┐ HTTP ┌─────────┐
│ AI Agent │ ◄──────────────── │ vision-sidecar │ ────────────► │ Ollama │
│ (Kilo/etc.) │ │ (this server) │ /api/chat │ + VLM │
└──────────────┘ └──────────────────┘ └─────────┘
- Agent captures a screenshot (e.g. via Playwright) or receives an image path
- Calls one of the MCP tools (
analyze_image,analyze_ui_screenshot,extract_text_from_image) - Server base64-encodes the image, sends it to Ollama VLM
- VLM returns structured text analysis → back to the agent
- 8 MCP tools:
analyze_image,analyze_ui_screenshot,extract_text_from_image,extract_table,analyze_structured,diagnose_error,get_telemetry,check_vlm_status - Hardened extraction-only system prompt — forbids hex-code invention, requires per-element color attribution, preserves icon glyphs (✦, →) verbatim, flags below-fold clipping
viewport_hintparameter (desktop/tablet/mobile/small_mobile) — viewport-aware analysis with explicit clipping checks for narrow viewports- Auto-upscale for narrow screenshots (≤400px wide) using Pillow Lanczos 2× — improves OCR on tight mobile layouts
- Structured JSON output via
analyze_structured— schema withglyphs_and_icons,below_fold_or_clipped, per-block color descriptors, and a fencedinterpretationblock - Error / broken-UI diagnosis via
diagnose_error— returns structured JSON witherror_type,affected_elements,suggested_fixes; optionalerror_contextfor cross-referencing console/log output - Structured logging with 7 GRACE BLOCK markers (
BLOCK_VALIDATE_IMAGE,BLOCK_UPSCALE,BLOCK_ENCODE_IMAGE,BLOCK_CALL_OLLAMA,BLOCK_PARSE_JSON,BLOCK_PROBE_OLLAMA,BLOCK_DISPATCH_TOOL) — opt-in viaVLM_LOG_LEVEL=INFO - Persistent local telemetry (default ON) — every tool call written as JSONL to
<server>/.telemetry/and (when image_path is inside a git repo) also to<repo>/.vision-sidecar-telemetry/. Aggregate snapshots with p50/p95/p99 latency and per-tool / per-status breakdown via theget_telemetrytool. Survives MCP daemon restarts. Disable viaVLM_TELEMETRY=0 - 44 module-level tests in
tests/(no Ollama required) —pytest tests/runs in ~0.5s - Mock mode for development without a running VLM
- Configurable model, temperature, timeout, num_predict, upscale threshold via environment variables
- 50MB image limit with format auto-detection (PNG, JPEG, GIF, WebP, BMP)
- Zero external state — pure proxy, no database, no caching
Note on
ui_diff: v0.3.0 deliberately does NOT ship a multi-image comparison tool. A gating smoke test (eval_results/smoke_multi_image/result.md) showed that qwen3-vl:8b fabricates cross-image elements (claims a desktop-only egg illustration is "partially clipped" in mobile shots where it's hidden by responsive rules) on 0/3 fixture pairs. Use twoanalyze_structuredcalls and diff the JSONs downstream instead.
- Python ≥ 3.11
- uv package manager
- Ollama with a vision model installed (e.g.
qwen3-vl,llava,minicpm-v) - Playwright (optional, for capturing screenshots)
git clone https://github.com/xronocode/vision-sidecar-mcp.git
cd vision-sidecar-mcp
uv sync# Check that Ollama is running and has a vision model
curl http://localhost:11434/api/tags | python3 -c \
"import sys,json; print([m['name'] for m in json.load(sys.stdin)['models']])"If you don't have a vision model yet:
ollama pull qwen3-vl:8bAdd to ~/.config/kilo/kilo.jsonc in the "mcp" section:
For remote Ollama instances, change OLLAMA_URL to the server address.
This server uses the standard MCP stdio transport. To run it directly:
OLLAMA_URL=http://localhost:11434 VLM_MODEL=qwen3-vl:8b uv run python3 -m vision_sidecar.serverRefer to your MCP client's documentation for registering local MCP servers.
General-purpose image analysis. Returns markdown description under the hardened extraction system prompt.
image_path: Absolute path to the image file.
prompt: Optional custom analysis prompt.
viewport_hint: Optional one of "desktop" | "tablet" | "mobile" | "small_mobile".
Specialized UI/UX analysis. Extracts component hierarchy, layout, qualitative color descriptors, glyph callouts, and clipping flags.
OCR-like text extraction. Transcribes all visible text preserving formatting and icon glyphs verbatim.
Dedicated table-extraction tool. Returns parsed JSON { present, row_count, col_count, header_row, rows }. Includes the row-label column when present (top-left cell as "").
Returns a structured JSON analysis with:
image_type,viewport_class(closed enums)visible_text[]with per-block role/x_position/color_descriptor/weightkey_ui_elements[],layout_structure,buttons_and_links[]numbers_dates_amounts[],table_content,glyphs_and_icons[]uncertain_items[],below_fold_or_clipped(clipping detection)- Optional
interpretationblock (fenced from observation fields) confidence(overall/ocr/color/layout)
Analyzes a screenshot showing a broken UI / error / clipping issue and proposes actionable fixes. Returns the same parse-tolerant JSON envelope as analyze_structured / extract_table (result holds the parsed diagnosis, or raw + parse_error if the model output didn't parse).
image_path: Absolute path to the screenshot.
error_context: Optional console/terminal/log text. When provided, it is appended
to the user prompt as a fenced block so the model cross-references
console output with what's visible.
viewport_hint: Optional viewport class.
Diagnosis schema: error_type (enum: layout_break/missing_element/overflow/clipping/error_message/crash/404/console_error/validation_error/z_index/other), description, affected_elements[] (with location), visible_error_text (verbatim if present, else null), suggested_fixes[] (2–3 concrete CSS/HTML/responsive recommendations), looks_correct (bool — true only if no error visible), confidence.
Sample real-call output is captured in eval_results/diagnose_error_smoke/.
Returns a snapshot of persistent local telemetry — counters, per-tool breakdown, latency / response / image-size histograms with p50/p95/p99.
project_path: Optional path inside a git repo. When given, returns the
per-project snapshot from <repo-root>/.vision-sidecar-telemetry/
instead of the server-global snapshot.
raw: If true, returns the most recent `raw_limit` events as a list
instead of the aggregate.
raw_limit: Cap on raw events (default 100, max 5000).
Snapshot schema includes:
totals—calls,by_tool,by_status,parse,errors,empty_responses,upscalesrates—upscale_rate,empty_response_rate,error_rateollama_wall_seconds,total_wall_seconds,response_chars,image_bytes— each with{ count, min, max, sum, p50, p95, p99 }
Telemetry is enabled by default. Set VLM_TELEMETRY=0 to disable.
Health check — verifies Ollama connectivity and reports server config (version, tools_available, URL, model, full sampling profile, prefill flag, upscale threshold) plus the list of models the Ollama backend reports.
npx playwright screenshot --full-page https://example.com /tmp/shot.pngThen call analyze_ui_screenshot("/tmp/shot.png") to get layout, components, and colors.
extract_text_from_image("/tmp/error.png") — get exact error text for code search.
analyze_image("/tmp/mockup.png", "Compare this design with the current implementation. List differences.")
Set VISION_MOCK=1 to get canned responses without calling Ollama. Useful for developing prompts or testing MCP integration.
| Variable | Default | Description |
|---|---|---|
OLLAMA_URL |
http://localhost:11434 |
Ollama API URL |
VLM_MODEL |
minicpm-v |
Vision model name in Ollama |
VLM_TIMEOUT |
120 |
Request timeout in seconds |
VLM_TEMPERATURE |
0.1 |
Sampling temperature (lower = more deterministic extraction) |
VLM_NUM_PREDICT |
8192 |
Max output tokens (raised to fit dense schema responses) |
VLM_NUM_CTX |
32768 |
Context window. Default 2048-4096 in Ollama is too small for image + schema + system prompt |
VLM_TOP_P |
0.9 |
Nucleus sampling cutoff |
VLM_TOP_K |
20 |
Top-k sampling cutoff |
VLM_REPEAT_PENALTY |
1.05 |
Penalty applied to repeated tokens |
VLM_SEED |
42 |
Pin random seed for reproducible outputs (set empty to use random) |
VLM_NUM_BATCH |
1024 |
Tokens processed per batch on the GPU during prompt eval |
VLM_NUM_KEEP |
0 |
Number of tokens to keep from the previous request (0 = clean state per request) |
VLM_PREFILL_THINKING_CLOSE |
1 |
Append assistant turn with </think>\n\n so reasoning VLMs (qwen3-vl, etc.) skip thinking and emit response directly. Set 0 only for non-reasoning models. Required for multi-image too — without it, two-image calls also return empty content |
VLM_LOG_LEVEL |
WARNING |
Logger level for vision_sidecar (Python logging standard names). Set to INFO to surface BLOCK markers ([VisionSidecar][module][fn][BLOCK_NAME] k=v) — useful for tracing tool dispatch, ollama calls, parse fallbacks. DEBUG adds full preprocess + sampling dumps |
VLM_TELEMETRY |
1 |
Persistent local telemetry. 1 = on (default), 0 = off |
VLM_TELEMETRY_DIR |
<server>/.telemetry |
Override the server-side telemetry directory. Per-project directory (<repo>/.vision-sidecar-telemetry/) is always inferred from image_path and not configurable |
VLM_TELEMETRY_AUTO_GITIGNORE |
0 |
When 1, the sidecar idempotently appends .vision-sidecar-telemetry/ to your project's .gitignore on first hit. Default OFF — opt-in only, since this mutates a file outside the sidecar's repo. Add the entry to your project's .gitignore manually if you don't enable this |
VISION_UPSCALE_BELOW_WIDTH |
400 |
Auto-upscale images whose width is ≤ this (px) |
VISION_UPSCALE_FACTOR |
2 |
Upscale multiplier when triggered (Lanczos resampling) |
VISION_MOCK |
1 |
1 = mock responses, 0 = real VLM calls |
| Model | Size | Notes |
|---|---|---|
qwen3-vl:8b |
~6GB | Best balance of speed and quality |
llava:7b |
~4.7GB | Good general-purpose VLM |
minicpm-v |
~5.5GB | Fast, decent quality |
All models require an Ollama server with sufficient RAM/VRAM.
- Max image size: 50MB
- VLM latency: ~30-60 seconds per request (depends on model and hardware)
- Text output quality depends on the VLM model — larger models produce better analysis
- No streaming — full response is returned after VLM completes
uv sync --extra dev # install pytest
pytest tests/ # 44 module-level checks, no Ollama required, ~0.5stests/ contains deterministic mock-mode checks for the JSON parser, Lanczos upscale, env-var resolution, log markers, and tool dispatch. Run before any pull request.
vision-sidecar-mcp/
├── pyproject.toml # Dependencies and entry point
├── README.md # This file
├── USAGE.md # Detailed workflows and tips
├── LICENSE # MIT
├── vision_sidecar/
│ ├── __init__.py
│ └── server.py # FastMCP server with 7 tools
├── tests/ # pytest mock-mode checks
│ ├── conftest.py
│ ├── test_parse.py
│ ├── test_image.py
│ ├── test_config.py
│ ├── test_logging.py
│ └── test_diagnose.py
├── eval_scripts/
│ └── run_multi_image_smoke.py # VF-006 gating runner
├── eval_images/kombo/ # 10-shot eval dataset
└── eval_results/
├── kombo/ # cycle-1 baseline
├── kombo_after_tuning/ # cycle-2 (v0.2.2)
├── opus_groundtruth/ # frontier ground truth
├── zai/ # cloud GLM 4-way comparison
├── smoke_multi_image/ # ui_diff gate decision
├── diagnose_error_smoke/ # diagnose_error real-call evidence
└── v030_release_evidence.md # v0.3.0 ship checklist
The eval_results/ tree records a multi-stack comparison on a 10-screenshot dataset captured from a real marketing site (eval_images/kombo/). eval_results/zai/aggregate_4way_comparison.md summarizes the four columns:
- Opus 4.7 (frontier ceiling) — composite 5.0
- local-tuned (v0.2.2/v0.3.0) — composite 4.1 (~82% of frontier)
- zai cloud (GLM) — composite 3.9 (~78% of frontier)
- local-baseline (pre-tuning) — composite 3.1 (~62% of frontier)
The tuned local model slightly outperforms the GLM-backed cloud VLM on aggregate. Residual gap to frontier sits in three known places: subtle color discrimination (e.g. distinguishing a deliberately greyed eyebrow from a purple one), exact wrap-line counting on mobile H1s, and design-intent inference. v0.3.0 is purely additive to v0.2.2 and does not change these numbers — see eval_results/v030_release_evidence.md for the no-regression rationale.