Content
# visbridge-mcp
[](https://www.npmjs.com/package/visbridge-mcp)
[](https://www.npmjs.com/package/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 with `visbridge-mcp doctor` first.
Token-efficient vision for MCP clients. An [MCP](https://modelcontextprotocol.io) 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 via `prompt`).
- **`detail: low` by default** — cheaper *and* measurably better OCR than `high`.
- **Fence-free OCR** — every OCR answer is stripped of markdown code fences defensively.
- **Four image sources** — local `path`, `url`, `base64` (data URL or raw), and `resource`
(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. A `warnings` entry 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/completions` endpoint + an API key.
## Setup
```bash
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:
```jsonc
{
"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's
`env` block (above) rather than a `.env` file.
### Register in Claude Code
Published package, stdio (credentials via `-e` flags):
```bash
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):
```bash
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 `.env` rather 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`:
```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:
```jsonc
{
"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`/`env` with `"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:
```jsonc
{
"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:
```jsonc
{
"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:
```bash
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:
```bash
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 bind `VISION_HTTP_HOST=0.0.0.0` or expose the port beyond
> localhost — the HTTP transport carries the provider's `VISION_API_KEY`, accepts arbitrary
> local `path` image 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 for
`analyze_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`](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) or `AGENTS.md` in 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):
```bash
npx tsx src/index.ts doctor # live smoke test with your real credentials
npm run demo # MCP-client demo: listTools + describe + ocr
```
## Development
```bash
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:
```bash
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 lying `content-length` cannot
buffer an unbounded response. Override per-host via `VISION_SSRF_ALLOW_HOSTS`.
- **No secret leakage**: API keys are masked in `config` output 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](https://docs.npmjs.com/generating-provenance-statements)
(GitHub Actions OIDC — no npm token). To cut a release:
```bash
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/` — no
`src/`, fixtures, or `.env`.
## Roadmap
- `resource` wired to a live MCP resource list.
- `openai.ts` Responses-API adapter (native OpenAI path, replacing `openai-compatible` where desired).
- Richer `doctor` output (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.
Connection Info
You Might Also Like
No recommendations available