visbridge-mcp
Health Pass
- License — License: Apache-2.0
- Description — Repository has a description
- Active repo — Last push 0 days ago
- Community trust — 55 GitHub stars
Code Warn
- fs module — File system access in package.json
- process.env — Environment variable access in src/config.ts
Permissions Pass
- Permissions — No dangerous permissions requested
No AI report is available for this listing yet.
A token-efficient, provider-agnostic vision layer for reasoning models via MCP.
visbridge-mcp
Status: stable beta. The core pipeline (media resolution, SSRF hardening, provider
adapters, local preprocessing) is covered by automated tests and releases are automated,
but real-world mileage is still limited. For local / personal use it is production-grade;
for critical workflows, pin your version and smoke-test withvisbridge-mcp doctorfirst.
Token-efficient vision for MCP clients. An MCP server
(stdio + Streamable HTTP) exposing a single analyze_image tool that lets any MCP client see
images — describe, OCR, or inspect — through an OpenAI-compatible vision provider.
It exists for one reason: to give a model without native vision (or any client that wants to
spend fewer tokens) the ability to understand images. The client sends only an image reference
plus a mode; the server resolves the image, sends it to the vision model, and returns compact plain
text.
┌──────────────────┐ JSON-RPC (stdio) ┌──────────────────┐ HTTP (chat) ┌──────────────────┐
│ MCP client │ ────────────────────▶ │ visbridge-mcp │ ──────────────────▶ │ vision provider │
│ (non-vision │ ◀──────────────────── │ (this server) │ ◀────────────────── │ (OpenAI-compat, │
│ model) │ compact text │ │ /chat/completions │ e.g. gemini) │
└──────────────────┘ └──────────────────┘ └──────────────────┘
Features
- Two transports — stdio for local clients (Claude Code, Cursor), Streamable HTTP (
POST /mcp)
for local clients that connect over HTTP. Both run on the user's own machine — this server is
not designed for remote/public hosting. - One tool, three modes —
describe(summarize),ocr(extract text),inspect(answer a
specific question viaprompt). detail: lowby default — cheaper and measurably better OCR thanhigh.- Fence-free OCR — every OCR answer is stripped of markdown code fences defensively.
- Four image sources — local
path,url,base64(data URL or raw), andresource
(resolved against a configured directory). - Hardened — SSRF protection (private/loopback/link-local IPs blocked), DNS resolved before
fetch, MIME sniffing (PNG/JPEG/WebP/GIF) + size limits, at most one redirect. - Local preprocessing — oversized images are downscaled before the provider call
(VISION_MAX_DIMENSION, default 2048, never upscales; animated GIFs use the first frame).
Cheaper, faster, and immune to provider-side image limits. Awarningsentry notes every resize. - Structured errors — 10-code taxonomy, MCP-safe messages (no keys/URLs leak).
- Observability — token/cost estimation and one JSON log line per call, written to stderr
(never stdout, which is reserved for the JSON-RPC channel).
Requirements
- Node.js >= 20
- A vision provider exposing an OpenAI-compatible
POST /chat/completionsendpoint + an API key.
Setup
npm install
cp .env.example .env # then fill in VISION_BASE_URL / VISION_API_KEY / VISION_MODEL
The server reads process.env directly. If you register it in an MCP client, the client populates
the environment from its own config — a .env file is optional.
Configuration
| Variable | Required | Default | Description |
|---|---|---|---|
VISION_BASE_URL |
✅ | — | Base URL of the OpenAI-compatible endpoint (http/https). |
VISION_API_KEY |
✅ | — | Bearer token sent to the provider. |
VISION_MODEL |
✅ | — | Vision model id, e.g. ag/gemini-3.6-flash-medium. |
VISION_PROVIDER |
— | openai-compatible |
Adapter: openai-compatible (chat completions) or mock (offline deterministic adapter for tests/demos). The openai Responses API adapter is not implemented yet. |
VISION_TRANSPORT |
— | stdio |
Transport: stdio (default) or http (Streamable HTTP). |
VISION_HTTP_HOST |
— | 127.0.0.1 |
Bind address for the HTTP transport. |
VISION_HTTP_PORT |
— | 3000 |
Port for the HTTP transport. |
VISION_DESCRIBE_OUTPUT_BUDGET |
— | 256 |
Max output tokens for describe. |
VISION_INSPECT_OUTPUT_BUDGET |
— | 384 |
Max output tokens for inspect. |
VISION_OCR_OUTPUT_BUDGET |
— | 1024 |
Max output tokens for ocr. |
VISION_TIMEOUT_MS |
— | 60000 |
Provider request timeout (ms). |
VISION_MAX_RETRIES |
— | 2 |
Retries for transient errors (429 / 5xx / network); honours retry-after. 0 disables. |
VISION_MAX_IMAGE_BYTES |
— | 20971520 |
Max decoded image size (20 MB). |
VISION_MAX_DIMENSION |
— | 2048 |
Downscale locally so the longest edge fits (never upscales; 0 disables). |
VISION_MAX_REDIRECTS |
— | 3 |
Max redirects for url images; every hop is SSRF-revalidated. |
VISION_SSRF_ALLOW_HOSTS |
— | (none) | Comma-separated hostnames allowed to reach private networks. |
VISION_RESOURCE_DIR |
— | (none) | Base dir for resolving resource image references. |
Usage
Install from npm (npx)
The package is published to npm — anyone can run it without a local checkout:
{
"mcpServers": {
"visbridge": {
"command": "npx",
"args": ["-y", "visbridge-mcp"],
"env": {
"VISION_BASE_URL": "https://...",
"VISION_API_KEY": "sk-...",
"VISION_MODEL": "ag/gemini-3.6-flash-medium"
}
}
}
}
With npx there is no local .env or launcher script, so credentials come from the client'senv block (above) rather than a .env file.
Register in Claude Code
Published package, stdio (credentials via -e flags):
claude mcp add visbridge \
-e VISION_BASE_URL=https://... \
-e VISION_API_KEY=sk-... \
-e VISION_MODEL=ag/gemini-3.6-flash-medium \
-- npx -y visbridge-mcp
HTTP transport (local clients that connect over HTTP):
claude mcp add --transport http visbridge http://127.0.0.1:3000/mcp
Local development (from a checkout): point at the launcher instead, so the API key stays in
the repo's.envrather than the client config:claude mcp add visbridge -- /path/to/visbridge-mcp/scripts/visbridge.sh
Verify with claude mcp list, then in a session ask the (non-vision) model to use the tool:
Use analyze_image to describe fixtures/images/dense-ui.png
Use analyze_image in ocr mode to read all the text in this image
Use analyze_image: what is the background color of the hero section, and how many CTAs are there?
Register in Cursor
Create .cursor/mcp.json:
{
"mcpServers": {
"visbridge": {
"command": "npx",
"args": ["-y", "visbridge-mcp"],
"env": {
"VISION_BASE_URL": "https://...",
"VISION_API_KEY": "sk-...",
"VISION_MODEL": "ag/gemini-3.6-flash-medium"
}
}
}
}
For the HTTP transport, replace command/args/env with"url": "http://127.0.0.1:3000/mcp".
Register in Kilo Code
Kilo Code stores MCP servers in ~/.config/kilo/kilo.jsonc (JSONC), under a top-level mcp key.
Its schema is not the standard mcpServers shape — command is the full argument array (no
separate args) and enabled is required:
{
"mcp": {
"visbridge": {
"type": "local",
"command": ["npx", "-y", "visbridge-mcp"],
"environment": {
"VISION_BASE_URL": "https://...",
"VISION_API_KEY": "sk-...",
"VISION_MODEL": "ag/gemini-3.6-flash-medium"
},
"enabled": true
}
}
}
For the HTTP transport, replace type/command/environment with "type": "http" and"url": "http://127.0.0.1:3000/mcp" — pointing at a server running on the same machine.
Other clients (Windsurf, VS Code, Cline, …)
They accept the standard mcpServers shape from the Cursor example above. Two notes:
- HTTP transport — replace
command/args/envwith"url": "http://127.0.0.1:3000/mcp"
(a server running on the same machine). - Windows — wrap the stdio command:
"command": "cmd", "args": ["/c", "npx", "-y", "visbridge-mcp"].
The three VISION_* variables are the only required config; everything else has safe defaults.
Invoke the tool directly
analyze_image accepts:
{
"image": { "kind": "path", "value": "fixtures/images/dense-ui.png" }, // path | url | base64 | resource
"mode": "auto", // auto | describe | ocr | inspect
"prompt": "…", // optional; for inspect mode
"context": "…", // optional; the user's original request so the vision model understands intent
"detail": "auto" // auto | low | high
}
Mode resolution: auto → describe when prompt is empty, otherwise inspect. Detail resolution:auto → low.
context — when set, the user's original question or intent is injected into the vision model's
prompt alongside the mode-specific instruction. This lets the vision model focus on what the user
actually asked about (e.g. "analyze the layout", "find the error code") even when the driving
model only describes the mode generically. Always pass the full user request as context when the
driving model's own instructions risk stripping the intent.
The result is a structured object plus a human-readable text block:
{
"answer": "…", // always present
"text": "…", // for ocr (plain text, fence-free)
"warnings": ["…"], // optional
"truncated": false // true if the provider hit the output budget
}
Run over HTTP (Streamable HTTP)
Some local clients and web UIs prefer to connect over HTTP instead of stdio. Run the server over
Streamable HTTP on the same machine:
VISION_TRANSPORT=http VISION_HTTP_PORT=3000 node --import tsx src/index.ts
The server listens on VISION_HTTP_HOST:VISION_HTTP_PORT (default 127.0.0.1:3000):
POST /mcp— the MCP endpoint (stateless, JSON responses).GET /health— liveness probe.
Register it in a client that supports HTTP MCP servers:
claude mcp add --transport http visbridge http://127.0.0.1:3000/mcp
Local-only by design. This server is meant to run on the user's own machine and has no
built-in authentication. Do not bindVISION_HTTP_HOST=0.0.0.0or expose the port beyond
localhost — the HTTP transport carries the provider'sVISION_API_KEY, accepts arbitrary
localpathimage references, and would let anyone who can reach the port use your provider
account. If you ever need remote access, front it with an authenticated TLS gateway.
Discoverability — making non-vision models use it reliably
The tool's description is written to signal when to use it, and most models will reach foranalyze_image on their own. But non-vision models have a trained-in reflex to refuse image
requests ("I can't see images"), and some paste attachments arrive as clipboard-*.png
filenames that invite wrong guesses. To make the behavior deterministic, add the ready-made
global rules from docs/agents-image-rules.md to your
client's instructions:
- Claude Code, global →
~/.claude/CLAUDE.md - Claude Code, per-project →
CLAUDE.md - Claude Code, per-agent →
.claude/agents/<name>.md - Cursor → a rule under
.cursor/rules/ - opencode →
~/.config/opencode/AGENTS.md(global) orAGENTS.mdin the project
The rules cover: calling the tool in the same turn (no refusals, no hedged openers), mapping
pasted data URLs to kind: "base64" instead of inventing paths, mode selection, and error
reporting. They were validated against GLM 5.3 and DeepSeek V4 (text-only) driving opencode.
CLI
visbridge-mcp start the stdio MCP server (default)
visbridge-mcp init print a ready-to-paste client config
visbridge-mcp config print effective (redacted) configuration
visbridge-mcp doctor [img] run a live describe + ocr against an image
Run without building (dev):
npx tsx src/index.ts doctor # live smoke test with your real credentials
npm run demo # MCP-client demo: listTools + describe + ocr
Development
npm run typecheck # tsc --noEmit
npm test # 93 unit + integration tests (mock provider + local HTTP fakes; no network)
npm run build # compile to dist/ (bin: visbridge-mcp)
npm run dev # start the stdio server via tsx
Try the full pipeline with no credentials at all:
VISION_PROVIDER=mock npx tsx src/index.ts doctor # offline smoke test of the pipeline
Security
- SSRF: URL images resolve DNS first, validate every address, block private, loopback,
link-local, and unspecified ranges (IPv4 + IPv6), and pin the connection to the validated
addresses (defeats DNS rebinding). Redirects are bounded (VISION_MAX_REDIRECTS) and each hop
is re-validated. Downloads are streamed with a hard byte cap — a lyingcontent-lengthcannot
buffer an unbounded response. Override per-host viaVISION_SSRF_ALLOW_HOSTS. - No secret leakage: API keys are masked in
configoutput and never appear in logs or error
messages. The launcher script keeps the key out of client config files. - Stdout is the protocol channel — all logging goes to stderr.
Publishing to npm
Releases are automated with npm trusted publishing
(GitHub Actions OIDC — no npm token). To cut a release:
npm version patch # or minor / major — bumps version, commits, and tags
git push --follow-tags
Pushing the v* tag triggers .github/workflows/publish.yml, which runs the tests, builds, and
publishes to npm. The tarball ships only package.json, README.md, LICENSE, and dist/ — nosrc/, fixtures, or .env.
Roadmap
resourcewired to a live MCP resource list.openai.tsResponses-API adapter (native OpenAI path, replacingopenai-compatiblewhere desired).- Richer
doctoroutput (surfaced per-call cost).
This project is local-only — there are no plans for remote/multi-tenant hosting, so HTTP
authorization is explicitly out of scope.
Reviews (0)
Sign in to leave a review.
Leave a reviewNo results found