mcp-vision-web-bridge 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +18 -0
- package/CHANGELOG.md +14 -0
- package/LICENSE +21 -0
- package/README.md +201 -0
- package/SECURITY_REVIEW.md +37 -0
- package/package.json +32 -0
- package/scripts/check-secrets.mjs +73 -0
- package/src/model-client.mjs +757 -0
- package/src/server.mjs +201 -0
- package/src/web-reader.mjs +371 -0
package/.env.example
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# OpenAI-compatible model endpoint
|
|
2
|
+
MODEL_BASE_URL=https://api.example.com/v1
|
|
3
|
+
MODEL_API_KEY=replace-with-your-own-key
|
|
4
|
+
MODEL_NAME=replace-with-your-vision-model
|
|
5
|
+
|
|
6
|
+
# Optional upload directories.
|
|
7
|
+
# macOS/Linux default delimiter: :
|
|
8
|
+
# Windows default delimiter: ;
|
|
9
|
+
# Defaults are best-effort Claude Desktop / Cowork local pending upload directories for macOS, Windows, and Linux.
|
|
10
|
+
# CLAUDE_UPLOAD_DIRS=~/Library/Application Support/Claude-3p/pending-uploads:~/Library/Application Support/Claude/pending-uploads
|
|
11
|
+
# CLAUDE_UPLOAD_DIRS_DELIMITER=:
|
|
12
|
+
|
|
13
|
+
# Safer defaults.
|
|
14
|
+
ALLOW_LOCAL_IMAGE_PATHS=false
|
|
15
|
+
ALLOW_CLIPBOARD_IMAGES=false
|
|
16
|
+
ALLOW_PRIVATE_NETWORK_URLS=false
|
|
17
|
+
USE_JINA_READER=false
|
|
18
|
+
MAX_IMAGE_BYTES=10485760
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
- Add MCP prompts for latest uploaded images and clipboard images.
|
|
6
|
+
- Return a non-sensitive image source label with image recognition results.
|
|
7
|
+
- Block private-network image URLs by default.
|
|
8
|
+
- Replace local endpoint defaults with a placeholder API endpoint.
|
|
9
|
+
- Improve provider error messages and redact bearer tokens.
|
|
10
|
+
- Add release-time secret scanning and `prepack` checks.
|
|
11
|
+
|
|
12
|
+
## 0.1.0
|
|
13
|
+
|
|
14
|
+
- Initial MCP server with image reading and web link reading tools.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# mcp-vision-web-bridge
|
|
2
|
+
|
|
3
|
+
Local MCP server that gives Claude Desktop, Claude Code, and other MCP clients two practical bridge tools:
|
|
4
|
+
|
|
5
|
+
- read an image from a recent Claude upload, clipboard, local path, URL, or base64 input, then send it to an OpenAI-compatible vision model;
|
|
6
|
+
- read web links locally, extract readable text, then send the result to an OpenAI-compatible model.
|
|
7
|
+
|
|
8
|
+
It does not include a model or any model credits. You bring your own OpenAI-compatible API endpoint and API key.
|
|
9
|
+
|
|
10
|
+
## Use Cases
|
|
11
|
+
|
|
12
|
+
- Your Claude client can accept images, but the third-party model behind it does not reliably receive image content.
|
|
13
|
+
- Your model provider supports vision, but your MCP client needs a local tool to collect image inputs.
|
|
14
|
+
- You want a safer local web reader with private-network blocking enabled by default.
|
|
15
|
+
|
|
16
|
+
## Capabilities
|
|
17
|
+
|
|
18
|
+
| Capability | macOS | Windows | Linux |
|
|
19
|
+
| --- | --- | --- | --- |
|
|
20
|
+
| MCP server | Supported | Supported | Supported |
|
|
21
|
+
| OpenAI-compatible chat completions | Supported | Supported | Supported |
|
|
22
|
+
| Web page reading | Supported | Supported | Supported |
|
|
23
|
+
| Recent Claude upload image | Supported | Best effort | Best effort |
|
|
24
|
+
| Clipboard image | Supported | Supported via PowerShell / Windows Forms | Supported via `wl-paste` or `xclip` |
|
|
25
|
+
|
|
26
|
+
Recent Claude upload paths are client implementation details, not a stable public API. If auto-detection does not work in your environment, set `CLAUDE_UPLOAD_DIRS` manually.
|
|
27
|
+
|
|
28
|
+
## Security Defaults
|
|
29
|
+
|
|
30
|
+
The default configuration is intentionally conservative:
|
|
31
|
+
|
|
32
|
+
- `.env` is ignored and should never be committed.
|
|
33
|
+
- API keys are only read from environment variables.
|
|
34
|
+
- Explicit local image paths are disabled unless `ALLOW_LOCAL_IMAGE_PATHS=true`.
|
|
35
|
+
- Clipboard image reading is disabled unless `ALLOW_CLIPBOARD_IMAGES=true`.
|
|
36
|
+
- Private-network web and image URLs are disabled unless `ALLOW_PRIVATE_NETWORK_URLS=true`.
|
|
37
|
+
- Jina Reader fallback is disabled unless `USE_JINA_READER=true`.
|
|
38
|
+
- The server does not log prompts, image data, API keys, or fetched page bodies.
|
|
39
|
+
|
|
40
|
+
## Requirements
|
|
41
|
+
|
|
42
|
+
- Node.js 20 or newer
|
|
43
|
+
- npm
|
|
44
|
+
|
|
45
|
+
This is a Node.js project. It does not require Python or a virtual environment.
|
|
46
|
+
|
|
47
|
+
## Install
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npm install
|
|
51
|
+
cp .env.example .env
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Edit `.env`:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
MODEL_BASE_URL=https://api.example.com/v1
|
|
58
|
+
MODEL_API_KEY=replace-with-your-own-key
|
|
59
|
+
MODEL_NAME=replace-with-your-vision-model
|
|
60
|
+
|
|
61
|
+
ALLOW_LOCAL_IMAGE_PATHS=false
|
|
62
|
+
ALLOW_CLIPBOARD_IMAGES=false
|
|
63
|
+
ALLOW_PRIVATE_NETWORK_URLS=false
|
|
64
|
+
USE_JINA_READER=false
|
|
65
|
+
MAX_IMAGE_BYTES=10485760
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`MODEL_BASE_URL` must be an OpenAI-compatible `/v1` endpoint.
|
|
69
|
+
|
|
70
|
+
### SiliconFlow Example
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
MODEL_BASE_URL=https://api.siliconflow.cn/v1
|
|
74
|
+
MODEL_API_KEY=replace-with-your-own-key
|
|
75
|
+
MODEL_NAME=Qwen/Qwen3-VL-8B-Instruct
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Use a model that supports vision input.
|
|
79
|
+
|
|
80
|
+
## Claude Desktop Config
|
|
81
|
+
|
|
82
|
+
Use absolute paths for your local checkout:
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"mcpServers": {
|
|
87
|
+
"vision-web-bridge": {
|
|
88
|
+
"command": "node",
|
|
89
|
+
"args": [
|
|
90
|
+
"--env-file-if-exists=/absolute/path/to/mcp-vision-web-bridge/.env",
|
|
91
|
+
"/absolute/path/to/mcp-vision-web-bridge/src/server.mjs"
|
|
92
|
+
]
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Restart Claude Desktop after changing the config.
|
|
99
|
+
|
|
100
|
+
## Claude Code Config
|
|
101
|
+
|
|
102
|
+
Add the same server to your Claude Code MCP config. After restart, check `/mcp` and confirm that `vision-web-bridge` is connected.
|
|
103
|
+
|
|
104
|
+
The server exposes two tools:
|
|
105
|
+
|
|
106
|
+
- `read_image_with_model`
|
|
107
|
+
- `read_links_with_model`
|
|
108
|
+
|
|
109
|
+
It also exposes two MCP prompts:
|
|
110
|
+
|
|
111
|
+
- `/mcp__vision-web-bridge__img`
|
|
112
|
+
- `/mcp__vision-web-bridge__clipboard-image`
|
|
113
|
+
|
|
114
|
+
## Usage
|
|
115
|
+
|
|
116
|
+
Read the latest image uploaded to the Claude client:
|
|
117
|
+
|
|
118
|
+
```text
|
|
119
|
+
Use read_image_with_model with use_latest_upload=true.
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Read the current clipboard image:
|
|
123
|
+
|
|
124
|
+
```text
|
|
125
|
+
Use read_image_with_model with use_clipboard=true and use_latest_upload=false.
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Read a local image path after enabling `ALLOW_LOCAL_IMAGE_PATHS=true`:
|
|
129
|
+
|
|
130
|
+
```text
|
|
131
|
+
Use read_image_with_model with image_path="/absolute/path/to/image.png".
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Read web links:
|
|
135
|
+
|
|
136
|
+
```text
|
|
137
|
+
Use read_links_with_model to summarize https://example.com/article
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Tool Details
|
|
141
|
+
|
|
142
|
+
### `read_image_with_model`
|
|
143
|
+
|
|
144
|
+
Supported image sources:
|
|
145
|
+
|
|
146
|
+
- latest Claude upload;
|
|
147
|
+
- public image URL;
|
|
148
|
+
- base64 image;
|
|
149
|
+
- data URL;
|
|
150
|
+
- local image path, opt-in only;
|
|
151
|
+
- clipboard image, opt-in only.
|
|
152
|
+
|
|
153
|
+
The tool returns the model response and a non-sensitive source label such as `latest uploaded image`, `clipboard image`, or `local image path`.
|
|
154
|
+
|
|
155
|
+
### `read_links_with_model`
|
|
156
|
+
|
|
157
|
+
The tool extracts URLs from the user input, fetches readable page content locally, and asks the configured model to summarize or answer questions.
|
|
158
|
+
|
|
159
|
+
Private-network URLs are blocked by default. Optional Jina Reader fallback can be enabled with `USE_JINA_READER=true`, which sends the URL to Jina Reader.
|
|
160
|
+
|
|
161
|
+
## Environment Variables
|
|
162
|
+
|
|
163
|
+
| Variable | Default | Description |
|
|
164
|
+
| --- | --- | --- |
|
|
165
|
+
| `MODEL_BASE_URL` | `https://api.example.com/v1` | OpenAI-compatible `/v1` endpoint |
|
|
166
|
+
| `OPENAI_BASE_URL` | unset | Fallback base URL if `MODEL_BASE_URL` is not set |
|
|
167
|
+
| `MODEL_API_KEY` | unset | API key for the model provider |
|
|
168
|
+
| `MODEL_NAME` | `replace-with-your-vision-model` | Chat or vision model name |
|
|
169
|
+
| `CLAUDE_UPLOAD_DIRS` | client-specific defaults | Override upload directories |
|
|
170
|
+
| `CLAUDE_UPLOAD_DIRS_DELIMITER` | platform default | Directory list delimiter |
|
|
171
|
+
| `ALLOW_LOCAL_IMAGE_PATHS` | `false` | Allow explicit local image paths |
|
|
172
|
+
| `ALLOW_CLIPBOARD_IMAGES` | `false` | Allow reading image data from clipboard |
|
|
173
|
+
| `ALLOW_PRIVATE_NETWORK_URLS` | `false` | Allow private-network web and image URLs |
|
|
174
|
+
| `USE_JINA_READER` | `false` | Allow Jina Reader fallback |
|
|
175
|
+
| `MAX_IMAGE_BYTES` | `10485760` | Maximum image size in bytes |
|
|
176
|
+
|
|
177
|
+
## Development
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
npm test
|
|
181
|
+
npm run check:secrets
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Before publishing, run:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
npm pack --dry-run
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Check the file list carefully. `.env`, logs, images, local screenshots, and personal paths must not be included.
|
|
191
|
+
|
|
192
|
+
## Windows Notes
|
|
193
|
+
|
|
194
|
+
- Use full absolute paths in `claude_desktop_config.json`.
|
|
195
|
+
- Save JSON config as UTF-8 without BOM.
|
|
196
|
+
- Restart Claude from the system tray after editing config.
|
|
197
|
+
- Clipboard image reading uses PowerShell / Windows Forms.
|
|
198
|
+
|
|
199
|
+
## License
|
|
200
|
+
|
|
201
|
+
MIT
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Security Review Checklist
|
|
2
|
+
|
|
3
|
+
Use this checklist before publishing or tagging a release.
|
|
4
|
+
|
|
5
|
+
## Required Checks
|
|
6
|
+
|
|
7
|
+
- `.env` is present only locally and is not tracked.
|
|
8
|
+
- No real API keys, cookies, tokens, credentials, account IDs, or session data are present.
|
|
9
|
+
- No local screenshots, image caches, logs, downloads, transcripts, or personal drafts are present.
|
|
10
|
+
- No personal filesystem paths are present.
|
|
11
|
+
- No machine-specific network addresses are present in docs, examples, package metadata, or release notes.
|
|
12
|
+
- Local image paths remain disabled by default.
|
|
13
|
+
- Clipboard image reading remains disabled by default.
|
|
14
|
+
- Private-network web and image URLs remain disabled by default.
|
|
15
|
+
- Third-party reader fallback remains disabled by default.
|
|
16
|
+
- Tool errors do not echo bearer tokens or configured API keys.
|
|
17
|
+
|
|
18
|
+
## Current Security Boundaries
|
|
19
|
+
|
|
20
|
+
- `read_image_with_model` requires exactly one image source.
|
|
21
|
+
- `image_path` requires `ALLOW_LOCAL_IMAGE_PATHS=true`.
|
|
22
|
+
- `use_clipboard` requires `ALLOW_CLIPBOARD_IMAGES=true`.
|
|
23
|
+
- `image_url` blocks private-network hosts unless explicitly enabled.
|
|
24
|
+
- `read_links_with_model` blocks private-network hosts unless explicitly enabled.
|
|
25
|
+
- Jina Reader fallback requires `USE_JINA_READER=true`.
|
|
26
|
+
- The tool returns a non-sensitive image source label, not local file paths.
|
|
27
|
+
- The server does not intentionally log prompts, image content, API keys, or fetched web content.
|
|
28
|
+
|
|
29
|
+
## Release Commands
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm test
|
|
33
|
+
npm run check:secrets
|
|
34
|
+
npm pack --dry-run
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Manually inspect the `npm pack --dry-run` file list before publishing.
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mcp-vision-web-bridge",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "A local MCP server that adds image understanding and web-page reading through an OpenAI-compatible model API.",
|
|
6
|
+
"files": [
|
|
7
|
+
"src",
|
|
8
|
+
"README.md",
|
|
9
|
+
"CHANGELOG.md",
|
|
10
|
+
"SECURITY_REVIEW.md",
|
|
11
|
+
"LICENSE",
|
|
12
|
+
".env.example",
|
|
13
|
+
"scripts"
|
|
14
|
+
],
|
|
15
|
+
"bin": {
|
|
16
|
+
"mcp-vision-web-bridge": "src/server.mjs"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"start": "node --env-file-if-exists=.env src/server.mjs",
|
|
20
|
+
"test": "node --test",
|
|
21
|
+
"check:secrets": "node scripts/check-secrets.mjs",
|
|
22
|
+
"prepack": "npm test && npm run check:secrets"
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=20"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
29
|
+
"zod": "^4.4.1"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {}
|
|
32
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
3
|
+
import { join, relative } from 'node:path';
|
|
4
|
+
|
|
5
|
+
const ROOT = process.cwd();
|
|
6
|
+
const EXCLUDED_DIRS = new Set(['.git', '.claude', 'node_modules', 'coverage', 'dist']);
|
|
7
|
+
const EXCLUDED_FILES = new Set(['.env', 'package-lock.json', 'scripts/check-secrets.mjs']);
|
|
8
|
+
const SECRET_PATTERNS = [
|
|
9
|
+
['OpenAI-style API key', /sk-[A-Za-z0-9_-]{16,}/],
|
|
10
|
+
['Slack token', /xox[baprs]-/],
|
|
11
|
+
['GitHub token', /gh[pousr]_[A-Za-z0-9_]{20,}/],
|
|
12
|
+
['AWS access key', /AKIA[0-9A-Z]{16}/],
|
|
13
|
+
['Bearer token', /Bearer\s+[A-Za-z0-9._~+/=-]{16,}/],
|
|
14
|
+
['personal absolute path', /\/Users\/[^\s"'`]+/]
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const findings = [];
|
|
18
|
+
|
|
19
|
+
await scanDir(ROOT);
|
|
20
|
+
|
|
21
|
+
if (findings.length) {
|
|
22
|
+
for (const finding of findings) {
|
|
23
|
+
console.error(`${finding.file}:${finding.line}: ${finding.kind}`);
|
|
24
|
+
}
|
|
25
|
+
process.exitCode = 1;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function scanDir(dir) {
|
|
29
|
+
const entries = await readdir(dir, {
|
|
30
|
+
withFileTypes: true
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
for (const entry of entries) {
|
|
34
|
+
const fullPath = join(dir, entry.name);
|
|
35
|
+
const relPath = relative(ROOT, fullPath) || entry.name;
|
|
36
|
+
|
|
37
|
+
if (entry.isDirectory()) {
|
|
38
|
+
if (EXCLUDED_DIRS.has(entry.name)) continue;
|
|
39
|
+
await scanDir(fullPath);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!entry.isFile()) continue;
|
|
44
|
+
if (EXCLUDED_FILES.has(relPath) || relPath.endsWith('.tgz')) continue;
|
|
45
|
+
|
|
46
|
+
await scanFile(fullPath, relPath);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function scanFile(fullPath, relPath) {
|
|
51
|
+
const info = await stat(fullPath);
|
|
52
|
+
if (info.size > 2 * 1024 * 1024) return;
|
|
53
|
+
|
|
54
|
+
let text;
|
|
55
|
+
try {
|
|
56
|
+
text = await readFile(fullPath, 'utf8');
|
|
57
|
+
} catch {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const lines = text.split(/\r?\n/);
|
|
62
|
+
for (const [index, line] of lines.entries()) {
|
|
63
|
+
for (const [kind, pattern] of SECRET_PATTERNS) {
|
|
64
|
+
if (pattern.test(line)) {
|
|
65
|
+
findings.push({
|
|
66
|
+
file: relPath,
|
|
67
|
+
line: index + 1,
|
|
68
|
+
kind
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|