mcp-local-vision

mcp-local-vision

Lets a text-only LLM read images and PDFs by delegating to a vision model running on your own GPU.

Category
访问服务器

README

mcp-local-vision

Lets a text-only LLM read images and PDFs by delegating to a vision model running on your own GPU, through llama.cpp.

Built for the case where your coding model has no vision at all — GLM on the Z.AI coding plan, DeepSeek, most local models. The coding model stays where it is; this server becomes its eyes.

  • No API keys, no cloud. Images never leave the machine.
  • Free PDF text. Pages with a text layer are extracted by poppler — zero model calls, exact output, instant.
  • Scanned pages fall back to vision. Rendered at 200 DPI and read by the VLM automatically.
  • Gives the GPU back. llama-server is started on demand and stopped after an idle period, so a shared card is not held hostage between screenshots.
  • Verbatim-first prompting. Tuned to transcribe exactly, not to summarize helpfully.

Why this exists

A model can only see an image if the provider serves a vision-capable model. On the Z.AI coding plan every model reports attachment: false:

zai-coding-plan:  glm-5.2, glm-5.2-highspeed, glm-4.7, glm-5-turbo   → text only

No prompt trick fixes that. Something with eyes has to do the looking, and on a 12GB card that something can be local.

Requirements

GPU 8GB VRAM or more (developed on an RTX 3060 12GB)
llama.cpp built with your GPU backend, providing llama-server
Model a vision GGUF plus its mmproj projector
poppler sudo apt-get install -y poppler-utils — needed only for scanned PDFs
Node 20+

Building llama.cpp with CUDA

llama.cpp ships no Linux CUDA prebuilt (the CUDA release assets are Windows-only), so build it:

sudo apt-get install -y cmake ninja-build gcc-13 g++-13 nvidia-cuda-toolkit
git clone --depth 1 https://github.com/ggml-org/llama.cpp ~/app/llama.cpp
cd ~/app/llama.cpp
cmake -S . -B build -G Ninja \
  -DCMAKE_BUILD_TYPE=Release \
  -DGGML_CUDA=ON \
  -DCMAKE_CUDA_ARCHITECTURES=86 \          # 86 = RTX 30xx; 89 = 40xx; 120 = 50xx
  -DCMAKE_CUDA_HOST_COMPILER=/usr/bin/g++-13 \
  -DLLAMA_BUILD_TESTS=OFF -DLLAMA_BUILD_EXAMPLES=OFF
cmake --build build --target llama-server -j $(nproc)

gcc-13 matters: Ubuntu's CUDA 12.4 nvcc rejects GCC 15, which is the system default on recent Ubuntu. Pointing CMAKE_CUDA_HOST_COMPILER at 13 is the whole fix.

Getting the model

mkdir -p ~/models && cd ~/models
B=https://huggingface.co/unsloth/Qwen3-VL-4B-Instruct-GGUF/resolve/main
curl -L -O $B/Qwen3-VL-4B-Instruct-UD-Q4_K_XL.gguf         # 2.5GB — the language model
curl -L -o mmproj-4B-F16.gguf $B/mmproj-F16.gguf           # 0.8GB — the vision encoder

Both files are required. Without mmproj, llama-server loads fine and is simply blind — it will answer about an image it never saw. The projector stays at F16 on purpose: it is small, and quantizing the vision tower is what costs you fine print.

The 4B is the default because ~3.4GB of weights leaves the card usable for other work. Swap in the 8B (unsloth/Qwen3-VL-8B-Instruct-GGUF, 5.1GB + 1.2GB) via VISION_MODEL_PATH if the GPU is yours alone; the OCR gain is small (DocVQA 96.1 vs 95.3).

Install

git clone https://github.com/jshsakura/mcp-local-vision ~/app/mcp-local-vision
cd ~/app/mcp-local-vision && npm install
npm test        # builds fixtures, then exercises every tool over real MCP

Register with opencode

In ~/.config/opencode/opencode.json (global) or ./opencode.json (per project):

{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "local-vision": {
      "type": "local",
      "command": ["/home/you/app/mcp-local-vision/bin/launch.sh"],
      "enabled": true,
      "timeout": 15000
    }
  }
}

Use bin/launch.sh rather than node src/index.mjs: MCP clients spawn servers with a minimal environment, and if your node lives under nvm it will not be on PATH. The launcher finds it and survives nvm upgrades.

Register with Claude Code

claude mcp add local-vision -s user -- /home/you/app/mcp-local-vision/bin/launch.sh

Optional: a chat UI on the same model

Because llama-server speaks the OpenAI API, anything that talks to OpenAI can share it — including Open WebUI, which gives you a browser chat window with image upload against the same local model your coding agent uses.

This only works with the idle-sleep setup below, not the managed-process fallback: a chat UI needs something listening even when nothing has been asked yet.

docker run -d --network=host \
  -e PORT=8090 \
  -e OPENAI_API_BASE_URL=http://127.0.0.1:8080/v1 \
  -e OPENAI_API_KEY=sk-local \
  -e ENABLE_OLLAMA_API=False \
  -v open-webui:/app/backend/data \
  --name open-webui --restart unless-stopped \
  ghcr.io/open-webui/open-webui:main

--network=host is required so the container can reach a llama-server bound to 127.0.0.1; PORT=8090 moves Open WebUI off its default 8080, which llama-server already holds. Open http://localhost:8090 and create the admin account on first visit.

The model appears as qwen3-vl-4b-instruct with capabilities: ["completion","multimodal"], so the image attach button works. Idle polling from the UI does not wake the GPU.

Serving the tools to Open WebUI as well

Open WebUI (v0.6.31+) speaks MCP, but only over Streamable HTTP — it cannot spawn a stdio server. Run this one in HTTP mode alongside the stdio one:

VISION_MANAGED=0 mcp-local-vision --http 38765

Then register it in Admin Settings → External Tools → + Add Server:

  • Connection Type: MCP (Streamable HTTP) — this part matters. The field defaults to OpenAPI, and pointing an OpenAPI connection at an MCP server gives you a crash or an infinite loading spinner rather than a useful error.
  • URL: http://127.0.0.1:38765/mcp

Only administrators can add MCP servers; the permission that lets ordinary users add OpenAPI tools does not extend to these.

To check the server rather than the UI, run the handshake by hand — a healthy server answers 200 with an SSE event: message frame:

curl -i -X POST http://127.0.0.1:38765/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"probe","version":"1"}}}'

Know what this does and does not give you. read_image takes a path on the server's filesystem. An image you drag into the chat window is an upload, not a path, so the tools cannot see it — that case is what the direct model connection above is for. The tools earn their keep on files already on disk ("read ~/docs/contract.pdf"), which is the same job they do in a coding agent. The two are complements, not alternatives.

Pass absolute paths over HTTP. In stdio mode the client spawns the server, so a relative path resolves against the client's working directory. Over HTTP the server is a long-lived process with a working directory of its own — under systemd, your home — and a relative path quietly resolves there instead. The tool descriptions say so, but it is worth knowing when a file "is not found" that plainly exists.

The HTTP endpoint has no authentication of its own and these tools read the filesystem, so it binds to 127.0.0.1 by default. If you move it off loopback with VISION_HTTP_HOST, set VISION_ALLOWED_ROOTS too.

Tools

read_image(path, question?)

Reads png, jpg, webp, gif, bmp, tiff, avif, heic. EXIF rotation is applied, transparency is flattened onto white (dark-mode screenshots with alpha otherwise read as black-on-black), and the long edge is capped at 1568px.

Omit question for a full transcription. Pass one when you only need a single detail — the model writes a short answer instead of a whole document, which keeps both latency and returned context small.

read_pdf(path, pages?, question?, mode?)

  • pages — "1-5", "2,7,9", "3". Omit for the whole document.
  • mode — auto (default: text layer where present, vision for scanned pages), text (never call the model), vision (render every page — use when the text layer is garbled or you need to read figures).

Text-layer pages are not capped; a 200-page report comes back in about a second. Only pages that need the vision model are capped (VISION_MAX_PAGES, default 5), and any page dropped by the cap is named in the output so the model knows to ask again with pages narrowed.

vision_doctor()

Reports the binary/model/projector paths and whether each exists, server state, and whether poppler is installed. Call it first when something fails.

GPU lifecycle

There are two ways to keep the GPU free between uses. Prefer the first.

llama.cpp's own idle sleep (recommended)

Recent llama-server takes --sleep-idle-seconds N: after N seconds with no task it unloads the model and KV cache, and the next request reloads it. The process stays up, so the endpoint answers the whole time — which is what lets a chat UI sit connected without pinning the card.

GET /health, /props and /models are explicitly exempt: they neither wake it nor reset the idle timer. A UI polling for the model list therefore costs nothing.

Measured on a 3060 12GB with the 4B model:

State llama-server VRAM
Loaded 4676 MiB
Asleep 126 MiB (CUDA context only)

Waking costs ~5.7s on the next call. Run it under systemd and point this server at it with VISION_MANAGED=0:

# ~/.config/systemd/user/llama-vision.service
[Service]
Type=exec
ExecStart=/home/you/app/llama.cpp/build/bin/llama-server \
  --model /home/you/models/Qwen3-VL-4B-Instruct-UD-Q4_K_XL.gguf \
  --mmproj /home/you/models/mmproj-4B-F16.gguf \
  --alias qwen3-vl-4b-instruct \
  --host 127.0.0.1 --port 8080 --ctx-size 8192 \
  --sleep-idle-seconds 300 --no-warmup
Restart=on-failure

[Install]
WantedBy=default.target
systemctl --user daemon-reload && systemctl --user enable --now llama-vision
sudo loginctl enable-linger $USER    # survive logout

Process lifecycle managed here (fallback)

If your llama-server predates --sleep-idle-seconds, leave VISION_MANAGED=1 (the default). This process then starts llama-server on the first vision call and kills it after VISION_IDLE_MS. It frees VRAM just as well, but the endpoint is gone while idle — fine for MCP, useless for a chat UI that expects something listening.

Either way the idle timer resets on every call, so a burst of reads never expires mid-flow.

Two things keep the managed mode from crashing on a busy card:

  • -ngl is not passed by default. Forcing -ngl 99 disables llama.cpp's automatic layer fitting — it logs n_gpu_layers already set by user to 99, abort and then dies with a CUDA OOM rather than offloading the overflow to CPU. Left unset, it sizes itself to whatever VRAM is actually free.
  • Startup failures explain themselves. llama-server's stderr is captured, and an OOM is reported as an OOM with the tail of its log, not as exited with code 1.

If you would rather run llama-server yourself under systemd or in a tmux pane, set VISION_MANAGED=0 and this process will only ever health-check it.

Configuration

All optional, all via environment variables.

Variable Default Purpose
VISION_SERVER_URL http://127.0.0.1:8080 llama-server base URL
VISION_MANAGED 1 Start/stop llama-server here. 0 = you run it
VISION_IDLE_MS 300000 Release the GPU after this idle time. 0 = never
VISION_SERVER_BIN ~/app/llama.cpp/build/bin/llama-server Binary to launch
VISION_MODEL_PATH ~/models/Qwen3-VL-4B-Instruct-UD-Q4_K_XL.gguf Language model GGUF
VISION_MMPROJ_PATH ~/models/mmproj-4B-F16.gguf Vision projector GGUF
VISION_GPU_LAYERS unset -ngl. Leave unset — setting it disables auto-fit
VISION_CONTEXT 8192 -c context size
VISION_EXTRA_ARGS — Extra llama-server flags, space separated
VISION_MAX_EDGE 1568 Long-edge cap in px. Raise for dense small print
VISION_MAX_TOKENS 4096 Output cap per call
VISION_PDF_DPI 200 Rasterization DPI for scanned pages
VISION_MAX_PAGES 5 Cap on vision pages per call
VISION_TIMEOUT_MS 300000 Per-request timeout
VISION_STARTUP_TIMEOUT_MS 180000 How long to wait for a cold load
VISION_MAX_FILE_BYTES 209715200 Reject larger inputs
VISION_TEXT_PAGE_MIN_CHARS 50 Below this, a PDF page counts as scanned
VISION_ALLOWED_ROOTS unset Colon-separated directory allowlist. Unset = unrestricted
VISION_VERBOSE 0 Pass llama-server stderr through
VISION_API_KEY unset Bearer token, if llama-server runs with --api-key
VISION_HTTP_PORT unset Serve MCP over Streamable HTTP on this port instead of stdio
VISION_HTTP_HOST 127.0.0.1 Bind address for HTTP mode
VISION_HTTP_TOKEN unset Bearer token required on /mcp. Mandatory off loopback
VISION_NODE unset Explicit node path for bin/launch.sh

Model choice

Any vision GGUF with a matching mmproj works. Qwen3-VL is the default because its OCR is the strongest of the small models (DocVQA 96.1 at 8B) and it handles CJK well.

Model Size Notes
Qwen3-VL-4B-Instruct UD-Q4_K_XL 2.5GB + 0.8GB mmproj Default. DocVQA 95.3
Qwen3-VL-8B-Instruct UD-Q4_K_XL 5.1GB + 1.2GB mmproj DocVQA 96.1, if the card is free
MiniCPM-V 4.5 ~6GB Similar profile

Use the Instruct build, not the Thinking one. Thinking builds spend thousands of tokens reasoning before they transcribe — far slower for what is fundamentally a transcription job.

Limits

  • One image per call; multi-image comparison is not exposed.
  • Video is not supported even though the model accepts it.
  • Handwriting is materially worse than print.
  • The server reads any file the user running it can read unless VISION_ALLOWED_ROOTS is set.

License

MIT

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选