mcp-local-vision
This server provides local vision capabilities to text-only LLMs via llama.cpp. It offers three MCP tools: read_image, read_pdf, and vision_doctor.
read_image: Handles PNG, JPG, WebP, GIF, BMP, TIFF, AVIF, and HEIC files, applying EXIF rotation, flattening transparency, and scaling the long edge to 1568px. Returns verbatim text transcriptions and structural descriptions of UI, charts, and diagrams. Optionally answer a specific question about the image.read_pdf: Uses a hybrid approach: pages with a text layer are extracted directly (via poppler), while scanned pages are rendered at 200 DPI and processed by the vision model. Supports page selectors (e.g., "1-5"), modes (auto, text, vision), and an optional question. Vision-only pages are capped at 5 by default, with dropped pages reported.vision_doctor: Checks the local setup: llama-server process, model/projector paths, and poppler installation.
The server runs entirely locally, ensuring privacy, and manages GPU usage by leveraging idle sleep or starting/stopping the llama-server on demand. It can be configured via environment variables for paths, timeouts, page caps, etc., and deployed as an MCP tool for coding agents or via Streamable HTTP for integration with UIs like Open WebUI.
Provides tools for reading images and PDFs by delegating to a vision model served by Ollama, enabling text-only LLMs to transcribe images, extract text from scanned PDFs, and diagnose the vision setup.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@mcp-local-visionExtract the text from /tmp/screenshot.png"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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-serveris 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 onlyNo prompt trick fixes that. Something with eyes has to do the looking, and on a 12GB card that something can be local.
Related MCP server: image-recognition-mcp
Requirements
GPU | 8GB VRAM or more (developed on an RTX 3060 12GB) |
| built with your GPU backend, providing |
Model | a vision GGUF plus its |
poppler |
|
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 encoderBoth 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 MCPRegister 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.shOptional: 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 38765Then 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.targetsystemctl --user daemon-reload && systemctl --user enable --now llama-vision
sudo loginctl enable-linger $USER # survive logoutProcess 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:
-nglis not passed by default. Forcing-ngl 99disables llama.cpp's automatic layer fitting — it logsn_gpu_layers already set by user to 99, abortand 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 |
|
| llama-server base URL |
|
| Start/stop llama-server here. |
|
| Release the GPU after this idle time. |
|
| Binary to launch |
|
| Language model GGUF |
|
| Vision projector GGUF |
| unset |
|
|
|
|
| — | Extra llama-server flags, space separated |
|
| Long-edge cap in px. Raise for dense small print |
|
| Output cap per call |
|
| Rasterization DPI for scanned pages |
|
| Cap on vision pages per call |
|
| Per-request timeout |
|
| How long to wait for a cold load |
|
| Reject larger inputs |
|
| Below this, a PDF page counts as scanned |
| unset | Colon-separated directory allowlist. Unset = unrestricted |
|
| Pass llama-server stderr through |
| unset | Bearer token, if llama-server runs with |
| unset | Serve MCP over Streamable HTTP on this port instead of stdio |
|
| Bind address for HTTP mode |
| unset | Bearer token required on |
| unset | Explicit node path for |
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_ROOTSis set.
License
MIT
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Tools
Related MCP Servers
- Alicense-qualityDmaintenanceEnables text-only LLMs to analyze images by routing them to an OpenAI-compatible vision backend, supporting local files, URLs, and data URLs.Last updated53MIT
- AlicenseAqualityBmaintenanceGives vision-less LLMs the ability to recognize clipboard screenshots and images by proxying to an OpenAI-compatible vision model.Last updated1332MIT
- AlicenseAqualityBmaintenanceGives text-only coding agents the ability to 'see' images, videos, and screenshots by routing them to a vision model and returning structured text.Last updated837MIT
- Flicense-qualityCmaintenanceEnables text-only language models to 'see' and describe images by calling multimodal APIs (OpenAI, Anthropic) for image analysis.Last updated
Related MCP Connectors
OCR, transcription, file extraction, and image generation for AI agents via MCP.
Let ChatGPT, Claude & Cursor use your Mac: email, calendar, iMessage, Teams, files. Local, free.
Connect AI assistants to your GitHub-hosted Obsidian vault to seamlessly access, search, and analy…
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/jshsakura/mcp-local-vision'
If you have feedback or need assistance with the MCP directory API, please join our Discord server