fazee-vision-mcp 0.1.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 ADDED
@@ -0,0 +1,16 @@
1
+ VISION_PROVIDER=gemini
2
+ GEMINI_API_KEY=
3
+ GEMINI_MODEL=gemini-2.5-flash
4
+ OPENROUTER_API_KEY=
5
+ OPENROUTER_MODEL=google/gemini-2.5-flash
6
+ OLLAMA_BASE_URL=http://127.0.0.1:11434
7
+ OLLAMA_MODEL=llava
8
+ VISION_MAX_IMAGE_MB=12
9
+ VISION_MAX_PIXELS=40000000
10
+ VISION_ALLOWED_LOCAL_ROOTS=
11
+ VISION_ENABLE_URL_INPUT=true
12
+ VISION_ENABLE_LOCAL_PATH_INPUT=false
13
+ VISION_CACHE_ENABLED=true
14
+ VISION_CACHE_TTL_SECONDS=300
15
+ VISION_LOG_LEVEL=info
16
+ VISION_REQUEST_TIMEOUT_MS=30000
package/README.md ADDED
@@ -0,0 +1,226 @@
1
+ # Fazee Vision MCP Server
2
+
3
+ Production-grade MCP server for advanced image and screenshot analysis with a Gemini-first provider strategy.
4
+
5
+ ## Features
6
+
7
+ - Gemini as primary/default provider
8
+ - Provider fallback chain: Gemini -> OpenRouter -> Ollama
9
+ - `vision_analyze` MCP tool with strict Zod validation
10
+ - Secure image ingestion: URL, local path, base64, data URL
11
+ - Security protections: SSRF defense, private IP blocking, safe local path allowlist, size/pixel limits
12
+ - Sharp-based preprocessing: EXIF fix, resize, compression, normalization, metadata extraction, hashing
13
+ - Typed errors and stable error codes
14
+ - In-memory TTL caching keyed by image hash + prompt + provider/model/mode
15
+ - Text and JSON output formats
16
+ - Strict TypeScript + Vitest tests
17
+
18
+ ## Project Structure
19
+
20
+ ```txt
21
+ src/
22
+ index.ts
23
+ server.ts
24
+ config.ts
25
+ schemas.ts
26
+ errors.ts
27
+
28
+ image/
29
+ loadImage.ts
30
+ security.ts
31
+ normalize.ts
32
+ types.ts
33
+
34
+ providers/
35
+ base.ts
36
+ gemini.ts
37
+ openrouter.ts
38
+ ollama.ts
39
+
40
+ services/
41
+ visionService.ts
42
+ prompts.ts
43
+
44
+ cache/
45
+ memoryCache.ts
46
+
47
+ utils/
48
+ logger.ts
49
+ timing.ts
50
+
51
+ tests/
52
+ security.test.ts
53
+ imageInput.test.ts
54
+ cache.test.ts
55
+ providerMock.test.ts
56
+ toolContract.test.ts
57
+ ```
58
+
59
+ ## Installation
60
+
61
+ ```bash
62
+ npm install
63
+ ```
64
+
65
+ ## Environment Setup
66
+
67
+ Create `.env`:
68
+
69
+ ```env
70
+ VISION_PROVIDER=gemini
71
+ GEMINI_API_KEY=
72
+ GEMINI_MODEL=gemini-2.5-flash
73
+
74
+ OPENROUTER_API_KEY=
75
+ OPENROUTER_MODEL=google/gemini-2.5-flash
76
+
77
+ OLLAMA_BASE_URL=http://127.0.0.1:11434
78
+ OLLAMA_MODEL=llava
79
+
80
+ VISION_MAX_IMAGE_MB=12
81
+ VISION_MAX_PIXELS=40000000
82
+ VISION_ALLOWED_LOCAL_ROOTS=
83
+ VISION_ENABLE_URL_INPUT=true
84
+ VISION_ENABLE_LOCAL_PATH_INPUT=false
85
+
86
+ VISION_CACHE_ENABLED=true
87
+ VISION_CACHE_TTL_SECONDS=300
88
+ VISION_LOG_LEVEL=info
89
+ VISION_REQUEST_TIMEOUT_MS=30000
90
+ ```
91
+
92
+ ## Provider Setup
93
+
94
+ ### Gemini (Primary)
95
+
96
+ 1. Create API key in Google AI Studio.
97
+ 2. Set `GEMINI_API_KEY`.
98
+ 3. Optional: set `GEMINI_MODEL`.
99
+
100
+ ### OpenRouter
101
+
102
+ 1. Create OpenRouter API key.
103
+ 2. Set `OPENROUTER_API_KEY` and optional `OPENROUTER_MODEL`.
104
+
105
+ ### Ollama (Local)
106
+
107
+ 1. Install/start Ollama locally.
108
+ 2. Pull a vision model (example: `ollama pull llava`).
109
+ 3. Set `OLLAMA_BASE_URL` and `OLLAMA_MODEL`.
110
+
111
+ ## Run
112
+
113
+ ```bash
114
+ npm run dev
115
+ ```
116
+
117
+ Build:
118
+
119
+ ```bash
120
+ npm run build
121
+ node dist/index.js
122
+ ```
123
+
124
+ ## MCP Client Config
125
+
126
+ ### Claude Code (npx)
127
+
128
+ ```json
129
+ {
130
+ "mcpServers": {
131
+ "vision": {
132
+ "command": "npx",
133
+ "args": ["-y", "fazee-vision-mcp@latest"],
134
+ "env": {
135
+ "VISION_PROVIDER": "openrouter",
136
+ "OPENROUTER_API_KEY": "your_key",
137
+ "OPENROUTER_MODEL": "gemini-2.5-flash-lite"
138
+ }
139
+ }
140
+ }
141
+ }
142
+ ```
143
+
144
+ ### Cursor (npx)
145
+
146
+ ```json
147
+ {
148
+ "mcpServers": {
149
+ "vision": {
150
+ "command": "npx",
151
+ "args": ["-y", "fazee-vision-mcp@latest"],
152
+ "env": {
153
+ "VISION_PROVIDER": "openrouter",
154
+ "OPENROUTER_API_KEY": "your_key",
155
+ "OPENROUTER_MODEL": "gemini-2.5-flash-lite"
156
+ }
157
+ }
158
+ }
159
+ }
160
+ ```
161
+
162
+ ### Generic MCP (npx)
163
+
164
+ ```json
165
+ {
166
+ "name": "vision",
167
+ "command": "npx",
168
+ "args": ["-y", "fazee-vision-mcp@latest"],
169
+ "env": {
170
+ "VISION_PROVIDER": "openrouter",
171
+ "OPENROUTER_API_KEY": "your_key",
172
+ "OPENROUTER_MODEL": "gemini-2.5-flash-lite"
173
+ }
174
+ }
175
+ ```
176
+
177
+ ## Publish To npm
178
+
179
+ ```bash
180
+ npm run typecheck
181
+ npm test
182
+ npm run build
183
+ npm login
184
+ npm publish --access public
185
+ ```
186
+
187
+ ## Tool API
188
+
189
+ Tool: `vision_analyze`
190
+
191
+ Input:
192
+ - `imageSource`: URL | local path | base64 | data URL
193
+ - `prompt`: analysis instruction
194
+ - `mode`: `general | palette | hierarchy | components | ocr | ui_analysis | code_screenshot`
195
+ - `options`: `temperature`, `maxTokens`, `outputFormat`, `provider`, `model`, `enableCache`, `timeoutMs`
196
+
197
+ ## Troubleshooting
198
+
199
+ - `CONFIG_ERROR`: missing provider credentials and local provider unavailable
200
+ - `SECURITY_ERROR`: URL/path blocked by security policy
201
+ - `INPUT_ERROR`: invalid image source or limits exceeded
202
+ - `TIMEOUT_ERROR` / `PROVIDER_ERROR`: provider request exceeded timeout or failed
203
+
204
+ ## Security Notes
205
+
206
+ - Blocks localhost/private/metadata targets for URL image fetching
207
+ - Enforces local path allowlist for filesystem image access
208
+ - Enforces image byte and pixel limits
209
+ - Prevents secret leakage by avoiding raw secret logging
210
+
211
+ ## Windows Setup
212
+
213
+ - Use absolute Windows paths in MCP config, for example:
214
+ `C:\\Users\\name\\project\\dist\\index.js`
215
+
216
+ ## Linux Setup
217
+
218
+ - Use absolute Linux paths in MCP config, for example:
219
+ `/home/name/project/dist/index.js`
220
+
221
+ ## Test
222
+
223
+ ```bash
224
+ npm test
225
+ npm run typecheck
226
+ ```
@@ -0,0 +1,2 @@
1
+
2
+ export { }
package/dist/index.js ADDED
@@ -0,0 +1,852 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") {
10
+ for (let key of __getOwnPropNames(from))
11
+ if (!__hasOwnProp.call(to, key) && key !== except)
12
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
13
+ }
14
+ return to;
15
+ };
16
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
17
+ // If the importer is in node compatibility mode or this is not an ESM
18
+ // file that has been converted to a CommonJS file using a Babel-
19
+ // compatible transform (i.e. "__esModule" has not been set), then set
20
+ // "default" to the CommonJS "module.exports" for node compatibility.
21
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
22
+ mod
23
+ ));
24
+
25
+ // src/index.ts
26
+ var import_dotenv = __toESM(require("dotenv"));
27
+
28
+ // src/server.ts
29
+ var import_zod3 = require("zod");
30
+ var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
31
+ var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
32
+
33
+ // src/config.ts
34
+ var import_node_crypto = require("crypto");
35
+ var import_zod2 = require("zod");
36
+
37
+ // src/schemas.ts
38
+ var import_zod = require("zod");
39
+ var OutputFormatSchema = import_zod.z.enum(["text", "json"]);
40
+ var VisionModeSchema = import_zod.z.enum([
41
+ "general",
42
+ "palette",
43
+ "hierarchy",
44
+ "components",
45
+ "ocr",
46
+ "ui_analysis",
47
+ "code_screenshot"
48
+ ]);
49
+ var ProviderNameSchema = import_zod.z.enum(["gemini", "openrouter", "ollama"]);
50
+ var ToolOptionsSchema = import_zod.z.object({
51
+ temperature: import_zod.z.number().min(0).max(2).optional(),
52
+ maxTokens: import_zod.z.number().int().min(16).max(8192).optional(),
53
+ outputFormat: OutputFormatSchema.optional(),
54
+ provider: ProviderNameSchema.optional(),
55
+ model: import_zod.z.string().min(1).max(200).optional(),
56
+ enableCache: import_zod.z.boolean().optional(),
57
+ timeoutMs: import_zod.z.number().int().min(500).max(12e4).optional()
58
+ }).default({});
59
+ var VisionAnalyzeInputSchema = import_zod.z.object({
60
+ imageSource: import_zod.z.string().min(1),
61
+ prompt: import_zod.z.string().min(1).max(8e3),
62
+ mode: VisionModeSchema.default("general"),
63
+ options: ToolOptionsSchema.optional()
64
+ });
65
+ var VisionAnalyzeResponseSchema = import_zod.z.object({
66
+ provider: ProviderNameSchema,
67
+ model: import_zod.z.string(),
68
+ mode: VisionModeSchema,
69
+ outputFormat: OutputFormatSchema,
70
+ analysis: import_zod.z.string(),
71
+ uiAnalysis: import_zod.z.object({
72
+ page_context: import_zod.z.object({
73
+ value: import_zod.z.string(),
74
+ confidence: import_zod.z.number().min(0).max(1)
75
+ }),
76
+ sections: import_zod.z.array(
77
+ import_zod.z.object({
78
+ name: import_zod.z.string(),
79
+ observations: import_zod.z.array(import_zod.z.string()),
80
+ confidence: import_zod.z.number().min(0).max(1)
81
+ })
82
+ ),
83
+ controls: import_zod.z.array(
84
+ import_zod.z.object({
85
+ label: import_zod.z.string(),
86
+ type: import_zod.z.string(),
87
+ state: import_zod.z.string().optional(),
88
+ confidence: import_zod.z.number().min(0).max(1)
89
+ })
90
+ ),
91
+ status_indicators: import_zod.z.array(
92
+ import_zod.z.object({
93
+ label: import_zod.z.string(),
94
+ value: import_zod.z.string(),
95
+ confidence: import_zod.z.number().min(0).max(1)
96
+ })
97
+ ),
98
+ ocr_anchors: import_zod.z.array(import_zod.z.string()),
99
+ uncertainties: import_zod.z.array(import_zod.z.string()),
100
+ inferred_interpretations: import_zod.z.array(import_zod.z.string())
101
+ }).optional(),
102
+ cached: import_zod.z.boolean(),
103
+ image: import_zod.z.object({
104
+ width: import_zod.z.number().int(),
105
+ height: import_zod.z.number().int(),
106
+ format: import_zod.z.string(),
107
+ mimeType: import_zod.z.string(),
108
+ hash: import_zod.z.string(),
109
+ bytes: import_zod.z.number().int()
110
+ }),
111
+ timingMs: import_zod.z.number().int()
112
+ });
113
+
114
+ // src/config.ts
115
+ var ConfigSchema = import_zod2.z.object({
116
+ visionProvider: ProviderNameSchema.default("gemini"),
117
+ geminiApiKey: import_zod2.z.string().optional(),
118
+ geminiModel: import_zod2.z.string().default("gemini-2.5-flash"),
119
+ openrouterApiKey: import_zod2.z.string().optional(),
120
+ openrouterModel: import_zod2.z.string().default("google/gemini-2.5-flash"),
121
+ ollamaBaseUrl: import_zod2.z.string().url().default("http://127.0.0.1:11434"),
122
+ ollamaModel: import_zod2.z.string().default("llava"),
123
+ maxImageMb: import_zod2.z.number().default(12),
124
+ maxPixels: import_zod2.z.number().int().default(4e7),
125
+ allowedLocalRoots: import_zod2.z.array(import_zod2.z.string()).default([]),
126
+ enableUrlInput: import_zod2.z.boolean().default(true),
127
+ enableLocalPathInput: import_zod2.z.boolean().default(false),
128
+ cacheEnabled: import_zod2.z.boolean().default(true),
129
+ cacheTtlSeconds: import_zod2.z.number().int().default(300),
130
+ logLevel: import_zod2.z.enum(["debug", "info", "warn", "error"]).default("info"),
131
+ requestTimeoutMs: import_zod2.z.number().int().default(3e4)
132
+ });
133
+ function parseBool(input, fallback) {
134
+ if (!input) return fallback;
135
+ return ["1", "true", "yes", "on"].includes(input.toLowerCase());
136
+ }
137
+ function parseNumber(input, fallback) {
138
+ if (!input) return fallback;
139
+ const value = Number(input);
140
+ return Number.isFinite(value) ? value : fallback;
141
+ }
142
+ function parseRoots(input) {
143
+ if (!input) return [];
144
+ return input.split(",").map((x) => x.trim()).filter(Boolean);
145
+ }
146
+ function loadConfig(env = process.env) {
147
+ const parsed = ConfigSchema.parse({
148
+ visionProvider: env.VISION_PROVIDER ?? "gemini",
149
+ geminiApiKey: env.GEMINI_API_KEY,
150
+ geminiModel: env.GEMINI_MODEL ?? "gemini-2.5-flash",
151
+ openrouterApiKey: env.OPENROUTER_API_KEY,
152
+ openrouterModel: env.OPENROUTER_MODEL ?? "google/gemini-2.5-flash",
153
+ ollamaBaseUrl: env.OLLAMA_BASE_URL ?? "http://127.0.0.1:11434",
154
+ ollamaModel: env.OLLAMA_MODEL ?? "llava",
155
+ maxImageMb: parseNumber(env.VISION_MAX_IMAGE_MB, 12),
156
+ maxPixels: parseNumber(env.VISION_MAX_PIXELS, 4e7),
157
+ allowedLocalRoots: parseRoots(env.VISION_ALLOWED_LOCAL_ROOTS),
158
+ enableUrlInput: parseBool(env.VISION_ENABLE_URL_INPUT, true),
159
+ enableLocalPathInput: parseBool(env.VISION_ENABLE_LOCAL_PATH_INPUT, false),
160
+ cacheEnabled: parseBool(env.VISION_CACHE_ENABLED, true),
161
+ cacheTtlSeconds: parseNumber(env.VISION_CACHE_TTL_SECONDS, 300),
162
+ logLevel: env.VISION_LOG_LEVEL ?? "info",
163
+ requestTimeoutMs: parseNumber(env.VISION_REQUEST_TIMEOUT_MS, 3e4)
164
+ });
165
+ return parsed;
166
+ }
167
+
168
+ // src/errors.ts
169
+ var VisionError = class extends Error {
170
+ code;
171
+ status;
172
+ details;
173
+ constructor(code, message, status = 500, details) {
174
+ super(message);
175
+ this.name = "VisionError";
176
+ this.code = code;
177
+ this.status = status;
178
+ this.details = details;
179
+ }
180
+ };
181
+ function toVisionError(error) {
182
+ if (error instanceof VisionError) return error;
183
+ if (error instanceof Error) {
184
+ return new VisionError("INTERNAL_ERROR", error.message, 500);
185
+ }
186
+ return new VisionError("INTERNAL_ERROR", "Unknown error", 500, error);
187
+ }
188
+
189
+ // src/cache/memoryCache.ts
190
+ var MemoryCache = class {
191
+ constructor(ttlSeconds) {
192
+ this.ttlSeconds = ttlSeconds;
193
+ }
194
+ ttlSeconds;
195
+ store = /* @__PURE__ */ new Map();
196
+ get(key) {
197
+ const existing = this.store.get(key);
198
+ if (!existing) return void 0;
199
+ if (Date.now() > existing.expiresAt) {
200
+ this.store.delete(key);
201
+ return void 0;
202
+ }
203
+ return existing.value;
204
+ }
205
+ set(key, value) {
206
+ this.store.set(key, {
207
+ value,
208
+ expiresAt: Date.now() + this.ttlSeconds * 1e3
209
+ });
210
+ }
211
+ };
212
+
213
+ // src/providers/gemini.ts
214
+ var import_genai = require("@google/genai");
215
+ var GeminiProvider = class {
216
+ constructor(apiKey, defaultModel) {
217
+ this.apiKey = apiKey;
218
+ this.defaultModel = defaultModel;
219
+ this.client = new import_genai.GoogleGenAI({ apiKey });
220
+ }
221
+ apiKey;
222
+ defaultModel;
223
+ name = "gemini";
224
+ client;
225
+ async analyze(input) {
226
+ const model = input.model ?? this.defaultModel;
227
+ try {
228
+ const response = await this.client.models.generateContent({
229
+ model,
230
+ contents: [
231
+ {
232
+ role: "user",
233
+ parts: [
234
+ { text: input.prompt },
235
+ {
236
+ inlineData: {
237
+ mimeType: input.image.mimeType,
238
+ data: input.image.data.toString("base64")
239
+ }
240
+ }
241
+ ]
242
+ }
243
+ ],
244
+ config: {
245
+ maxOutputTokens: input.maxTokens,
246
+ temperature: input.temperature,
247
+ responseMimeType: input.outputFormat === "json" ? "application/json" : "text/plain"
248
+ }
249
+ });
250
+ const text = response.text?.trim();
251
+ if (!text) {
252
+ throw new VisionError("PROVIDER_ERROR", "Gemini returned an empty response.", 502);
253
+ }
254
+ return { text, model, provider: this.name };
255
+ } catch (error) {
256
+ if (error instanceof VisionError) throw error;
257
+ const msg = error instanceof Error ? error.message : "Gemini request failed";
258
+ if (/429|rate/i.test(msg)) {
259
+ throw new VisionError("RATE_LIMITED", `Gemini rate limited: ${msg}`, 429);
260
+ }
261
+ throw new VisionError("PROVIDER_ERROR", `Gemini provider failed: ${msg}`, 502);
262
+ }
263
+ }
264
+ };
265
+
266
+ // src/providers/ollama.ts
267
+ var OllamaProvider = class {
268
+ constructor(baseUrl, defaultModel) {
269
+ this.baseUrl = baseUrl;
270
+ this.defaultModel = defaultModel;
271
+ }
272
+ baseUrl;
273
+ defaultModel;
274
+ name = "ollama";
275
+ async analyze(input) {
276
+ const model = input.model ?? this.defaultModel;
277
+ const response = await fetch(`${this.baseUrl}/api/generate`, {
278
+ method: "POST",
279
+ headers: {
280
+ "Content-Type": "application/json"
281
+ },
282
+ body: JSON.stringify({
283
+ model,
284
+ prompt: input.prompt,
285
+ images: [input.image.data.toString("base64")],
286
+ stream: false,
287
+ options: {
288
+ temperature: input.temperature,
289
+ num_predict: input.maxTokens
290
+ }
291
+ })
292
+ });
293
+ if (!response.ok) {
294
+ throw new VisionError("PROVIDER_ERROR", `Ollama failed: HTTP ${response.status}`, 502);
295
+ }
296
+ const json = await response.json();
297
+ const text = json.response?.trim();
298
+ if (!text) {
299
+ throw new VisionError("PROVIDER_ERROR", "Ollama returned empty response.", 502);
300
+ }
301
+ return { text, model, provider: this.name };
302
+ }
303
+ };
304
+
305
+ // src/providers/openrouter.ts
306
+ var OpenRouterProvider = class {
307
+ constructor(apiKey, defaultModel) {
308
+ this.apiKey = apiKey;
309
+ this.defaultModel = defaultModel;
310
+ }
311
+ apiKey;
312
+ defaultModel;
313
+ name = "openrouter";
314
+ async analyze(input) {
315
+ const model = input.model ?? this.defaultModel;
316
+ const response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
317
+ method: "POST",
318
+ headers: {
319
+ "Content-Type": "application/json",
320
+ Authorization: `Bearer ${this.apiKey}`
321
+ },
322
+ body: JSON.stringify({
323
+ model,
324
+ temperature: input.temperature,
325
+ max_tokens: input.maxTokens,
326
+ messages: [
327
+ {
328
+ role: "user",
329
+ content: [
330
+ { type: "text", text: input.prompt },
331
+ {
332
+ type: "image_url",
333
+ image_url: {
334
+ url: `data:${input.image.mimeType};base64,${input.image.data.toString("base64")}`
335
+ }
336
+ }
337
+ ]
338
+ }
339
+ ]
340
+ })
341
+ });
342
+ if (!response.ok) {
343
+ throw new VisionError("PROVIDER_ERROR", `OpenRouter failed: HTTP ${response.status}`, 502);
344
+ }
345
+ const json = await response.json();
346
+ const text = json.choices?.[0]?.message?.content?.trim();
347
+ if (!text) {
348
+ throw new VisionError("PROVIDER_ERROR", "OpenRouter returned empty content.", 502);
349
+ }
350
+ return { text, model, provider: this.name };
351
+ }
352
+ };
353
+
354
+ // src/services/visionService.ts
355
+ var import_node_crypto3 = require("crypto");
356
+
357
+ // src/image/loadImage.ts
358
+ var import_promises2 = __toESM(require("fs/promises"));
359
+
360
+ // src/image/normalize.ts
361
+ var import_node_crypto2 = require("crypto");
362
+ var import_sharp = __toESM(require("sharp"));
363
+ var SUPPORTED_MIME = /* @__PURE__ */ new Map([
364
+ ["jpeg", "image/jpeg"],
365
+ ["png", "image/png"],
366
+ ["webp", "image/webp"],
367
+ ["gif", "image/gif"]
368
+ ]);
369
+ async function normalizeImage(input, sourceKind, config) {
370
+ const maxBytes = Math.floor(config.maxImageMb * 1024 * 1024);
371
+ if (input.byteLength > maxBytes) {
372
+ throw new VisionError("INPUT_ERROR", `Image is too large. Max is ${config.maxImageMb}MB.`, 400);
373
+ }
374
+ const metadata = await (0, import_sharp.default)(input, { failOn: "error" }).metadata();
375
+ if (!metadata.width || !metadata.height || !metadata.format) {
376
+ throw new VisionError("INPUT_ERROR", "Unsupported image or corrupted data.", 400);
377
+ }
378
+ if (metadata.width * metadata.height > config.maxPixels) {
379
+ throw new VisionError("INPUT_ERROR", `Image pixel count exceeds limit of ${config.maxPixels}.`, 400);
380
+ }
381
+ let pipeline = (0, import_sharp.default)(input, { failOn: "error" }).rotate();
382
+ const shouldResize = metadata.width > 2200 || metadata.height > 2200;
383
+ if (shouldResize) {
384
+ pipeline = pipeline.resize({ width: 2200, height: 2200, fit: "inside", withoutEnlargement: true });
385
+ }
386
+ const normalizedBuffer = await pipeline.webp({ quality: 85, effort: 4 }).toBuffer();
387
+ const finalMeta = await (0, import_sharp.default)(normalizedBuffer).metadata();
388
+ const format = finalMeta.format ?? "webp";
389
+ const mimeType = SUPPORTED_MIME.get(format) ?? "image/webp";
390
+ return {
391
+ data: normalizedBuffer,
392
+ format,
393
+ mimeType,
394
+ width: finalMeta.width ?? metadata.width,
395
+ height: finalMeta.height ?? metadata.height,
396
+ bytes: normalizedBuffer.byteLength,
397
+ hash: (0, import_node_crypto2.createHash)("sha256").update(normalizedBuffer).digest("hex"),
398
+ sourceKind
399
+ };
400
+ }
401
+
402
+ // src/image/security.ts
403
+ var import_promises = __toESM(require("dns/promises"));
404
+ var import_node_net = __toESM(require("net"));
405
+ var import_node_path = __toESM(require("path"));
406
+ var import_node_url = require("url");
407
+ var BLOCKED_HOSTNAMES = /* @__PURE__ */ new Set(["localhost", "metadata.google.internal"]);
408
+ var PRIVATE_CIDR_CHECKS = [
409
+ /^10\./,
410
+ /^127\./,
411
+ /^169\.254\./,
412
+ /^172\.(1[6-9]|2\d|3[0-1])\./,
413
+ /^192\.168\./,
414
+ /^0\./
415
+ ];
416
+ function isPrivateIpv4(ip) {
417
+ return PRIVATE_CIDR_CHECKS.some((r) => r.test(ip));
418
+ }
419
+ function isBlockedIpv6(ip) {
420
+ const normalized = ip.toLowerCase();
421
+ return normalized === "::1" || normalized.startsWith("fc") || normalized.startsWith("fd") || normalized.startsWith("fe80:");
422
+ }
423
+ async function validateRemoteUrl(rawUrl) {
424
+ let parsed;
425
+ try {
426
+ parsed = new import_node_url.URL(rawUrl);
427
+ } catch {
428
+ throw new VisionError("SECURITY_ERROR", "Invalid URL.", 400);
429
+ }
430
+ if (!["http:", "https:"].includes(parsed.protocol)) {
431
+ throw new VisionError("SECURITY_ERROR", "Only HTTP/HTTPS URLs are allowed.", 400);
432
+ }
433
+ if (BLOCKED_HOSTNAMES.has(parsed.hostname.toLowerCase())) {
434
+ throw new VisionError("SECURITY_ERROR", "URL host is blocked.", 403);
435
+ }
436
+ const host = parsed.hostname;
437
+ if (import_node_net.default.isIP(host)) {
438
+ if (import_node_net.default.isIPv4(host) && isPrivateIpv4(host) || import_node_net.default.isIPv6(host) && isBlockedIpv6(host)) {
439
+ throw new VisionError("SECURITY_ERROR", "Private or loopback IP is blocked.", 403);
440
+ }
441
+ return parsed;
442
+ }
443
+ const resolved = await import_promises.default.lookup(host, { all: true });
444
+ for (const addr of resolved) {
445
+ if (addr.family === 4 && isPrivateIpv4(addr.address) || addr.family === 6 && isBlockedIpv6(addr.address)) {
446
+ throw new VisionError("SECURITY_ERROR", "Resolved IP is private/loopback and blocked.", 403);
447
+ }
448
+ }
449
+ return parsed;
450
+ }
451
+ function validateLocalPath(inputPath, allowedRoots) {
452
+ const resolved = import_node_path.default.resolve(inputPath);
453
+ if (allowedRoots.length === 0) {
454
+ throw new VisionError("SECURITY_ERROR", "Local file access disabled: allowlist is empty.", 403);
455
+ }
456
+ const isAllowed = allowedRoots.some((root) => {
457
+ const resolvedRoot = import_node_path.default.resolve(root);
458
+ const relative = import_node_path.default.relative(resolvedRoot, resolved);
459
+ return relative === "" || !relative.startsWith("..") && !import_node_path.default.isAbsolute(relative);
460
+ });
461
+ if (!isAllowed) {
462
+ throw new VisionError("SECURITY_ERROR", "Local path is outside allowed roots.", 403);
463
+ }
464
+ return resolved;
465
+ }
466
+
467
+ // src/image/loadImage.ts
468
+ function isDataUrl(input) {
469
+ return input.startsWith("data:image/");
470
+ }
471
+ function isBase64(input) {
472
+ return /^[A-Za-z0-9+/=\r\n]+$/.test(input) && input.length > 128;
473
+ }
474
+ function parseDataUrl(input) {
475
+ const match = /^data:(image\/[\w+.-]+);base64,(.+)$/i.exec(input);
476
+ if (!match) {
477
+ throw new VisionError("INPUT_ERROR", "Invalid image data URL.", 400);
478
+ }
479
+ try {
480
+ return Buffer.from(match[2], "base64");
481
+ } catch {
482
+ throw new VisionError("INPUT_ERROR", "Invalid base64 in data URL.", 400);
483
+ }
484
+ }
485
+ async function loadFromUrl(url, timeoutMs) {
486
+ const controller = new AbortController();
487
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
488
+ try {
489
+ const response = await fetch(url, { signal: controller.signal });
490
+ if (!response.ok) {
491
+ throw new VisionError("INPUT_ERROR", `Failed to fetch image URL: HTTP ${response.status}`, 400);
492
+ }
493
+ const buf = Buffer.from(await response.arrayBuffer());
494
+ return buf;
495
+ } catch (error) {
496
+ if (error instanceof VisionError) throw error;
497
+ throw new VisionError("INPUT_ERROR", "Failed to fetch remote image.", 400, error);
498
+ } finally {
499
+ clearTimeout(timer);
500
+ }
501
+ }
502
+ async function loadImageFromSource(imageSource, config, timeoutMs) {
503
+ let raw;
504
+ let sourceKind;
505
+ if (isDataUrl(imageSource)) {
506
+ raw = parseDataUrl(imageSource);
507
+ sourceKind = "data-url";
508
+ } else if (/^https?:\/\//i.test(imageSource)) {
509
+ if (!config.enableUrlInput) {
510
+ throw new VisionError("SECURITY_ERROR", "URL input is disabled.", 403);
511
+ }
512
+ const safe = await validateRemoteUrl(imageSource);
513
+ raw = await loadFromUrl(safe.toString(), timeoutMs);
514
+ sourceKind = "url";
515
+ } else if (config.enableLocalPathInput) {
516
+ const localPath = validateLocalPath(imageSource, config.allowedLocalRoots);
517
+ raw = await import_promises2.default.readFile(localPath);
518
+ sourceKind = "local";
519
+ } else if (isBase64(imageSource)) {
520
+ try {
521
+ raw = Buffer.from(imageSource, "base64");
522
+ sourceKind = "base64";
523
+ } catch {
524
+ throw new VisionError("INPUT_ERROR", "Invalid base64 image input.", 400);
525
+ }
526
+ } else {
527
+ throw new VisionError("INPUT_ERROR", "Unsupported image source. Use URL, local path, base64, or data URL.", 400);
528
+ }
529
+ if (!raw || raw.byteLength === 0) {
530
+ throw new VisionError("INPUT_ERROR", "Empty image data.", 400);
531
+ }
532
+ return normalizeImage(raw, sourceKind, config);
533
+ }
534
+
535
+ // src/utils/timing.ts
536
+ async function withTimeout(promise, timeoutMs, onTimeout) {
537
+ let timer;
538
+ try {
539
+ const timeoutPromise = new Promise((_, reject) => {
540
+ timer = setTimeout(() => reject(onTimeout()), timeoutMs);
541
+ });
542
+ return await Promise.race([promise, timeoutPromise]);
543
+ } finally {
544
+ if (timer) clearTimeout(timer);
545
+ }
546
+ }
547
+ async function timed(fn) {
548
+ const start = Date.now();
549
+ const value = await fn();
550
+ return { value, durationMs: Date.now() - start };
551
+ }
552
+
553
+ // src/services/prompts.ts
554
+ var MODE_GUIDANCE = {
555
+ general: "Provide a clear and accurate description with key details and context.",
556
+ palette: "Extract visual design tokens: colors, spacing, typography, shadows, radius, and style patterns.",
557
+ hierarchy: "Analyze hierarchy, attention flow, focus areas, layout clarity, and readability.",
558
+ components: "Identify UI components, consistency, reuse quality, interaction patterns, and design-system maturity.",
559
+ ocr: "Extract all visible text accurately. Preserve ordering and structure where possible.",
560
+ ui_analysis: "Analyze the software UI: goals, flows, usability, information architecture, and issues.",
561
+ code_screenshot: "Analyze code/terminal screenshot: summarize code intent, errors, stack traces, and likely fixes."
562
+ };
563
+ function buildPrompt(mode, userPrompt, outputFormat) {
564
+ const uiFactPolicy = mode === "ui_analysis" ? "Strict policy: separate observed facts from inferred interpretations. If uncertain, explicitly list uncertainty instead of asserting." : "";
565
+ const format = outputFormat === "json" ? "Return strict JSON with stable keys and no markdown." : "Return concise plain text with headings when useful.";
566
+ return [
567
+ "You are an expert vision analyst for images and screenshots.",
568
+ `Mode: ${mode}. ${MODE_GUIDANCE[mode]}`,
569
+ uiFactPolicy,
570
+ format,
571
+ `User request: ${userPrompt}`
572
+ ].join("\n\n");
573
+ }
574
+ function buildUiOcrPrompt(userPrompt) {
575
+ return [
576
+ "You are extracting OCR anchors from a software screenshot.",
577
+ "Return only visible text anchors and short structural labels.",
578
+ "No assumptions. Include exact labels, button text, section titles, and model IDs if visible.",
579
+ "Output plain text lines only.",
580
+ `User request context: ${userPrompt}`
581
+ ].join("\n\n");
582
+ }
583
+ function buildUiStructuredJsonPrompt(userPrompt, ocrAnchors) {
584
+ return [
585
+ "You are an expert UI screenshot analyst.",
586
+ "Strict policy: separate observed facts from inferred interpretations. If uncertain, include the item under uncertainties.",
587
+ "Use OCR anchors as grounding evidence and do not invent labels not supported by image evidence.",
588
+ "Return strict JSON only with this exact shape:",
589
+ JSON.stringify(
590
+ {
591
+ page_context: { value: "string", confidence: 0 },
592
+ sections: [{ name: "string", observations: ["string"], confidence: 0 }],
593
+ controls: [{ label: "string", type: "string", state: "optional", confidence: 0 }],
594
+ status_indicators: [{ label: "string", value: "string", confidence: 0 }],
595
+ ocr_anchors: ["string"],
596
+ uncertainties: ["string"],
597
+ inferred_interpretations: ["string"]
598
+ },
599
+ null,
600
+ 2
601
+ ),
602
+ `OCR anchors:
603
+ ${ocrAnchors || "<none>"}`,
604
+ `User request: ${userPrompt}`
605
+ ].join("\n\n");
606
+ }
607
+
608
+ // src/services/visionService.ts
609
+ var VisionService = class {
610
+ constructor(config, logger, providers, providerOrder, cache) {
611
+ this.config = config;
612
+ this.logger = logger;
613
+ this.providers = providers;
614
+ this.providerOrder = providerOrder;
615
+ this.cache = cache;
616
+ }
617
+ config;
618
+ logger;
619
+ providers;
620
+ providerOrder;
621
+ cache;
622
+ getCacheKey(input) {
623
+ return (0, import_node_crypto3.createHash)("sha256").update(JSON.stringify(input)).digest("hex");
624
+ }
625
+ selectProvider(explicit) {
626
+ if (explicit) return [explicit, ...this.providerOrder.filter((p) => p !== explicit)];
627
+ return [...this.providerOrder];
628
+ }
629
+ async analyze(input) {
630
+ const options = input.options ?? {};
631
+ const timeoutMs = options.timeoutMs ?? this.config.requestTimeoutMs;
632
+ const image = await loadImageFromSource(input.imageSource, this.config, timeoutMs);
633
+ const prompt = buildPrompt(input.mode, input.prompt, options.outputFormat ?? "text");
634
+ const orderedProviders = this.selectProvider(options.provider ?? this.config.visionProvider);
635
+ const cacheKey = this.getCacheKey({
636
+ hash: image.hash,
637
+ prompt,
638
+ mode: input.mode,
639
+ provider: orderedProviders[0],
640
+ model: options.model ?? "default",
641
+ outputFormat: options.outputFormat ?? "text"
642
+ });
643
+ const cacheEnabled = options.enableCache ?? this.config.cacheEnabled;
644
+ if (cacheEnabled) {
645
+ const cached = this.cache.get(cacheKey);
646
+ if (cached) return { ...cached, cached: true };
647
+ }
648
+ const analyzeRequest = {
649
+ image,
650
+ prompt,
651
+ mode: input.mode,
652
+ outputFormat: options.outputFormat ?? "text",
653
+ temperature: options.temperature,
654
+ maxTokens: options.maxTokens,
655
+ timeoutMs,
656
+ model: options.model,
657
+ provider: options.provider
658
+ };
659
+ let lastError;
660
+ for (const providerName of orderedProviders) {
661
+ const provider = this.providers[providerName];
662
+ if (!provider) continue;
663
+ try {
664
+ const { value, durationMs } = await timed(async () => {
665
+ if (input.mode !== "ui_analysis" || (options.outputFormat ?? "text") !== "json") {
666
+ return withTimeout(provider.analyze(analyzeRequest), timeoutMs, () => new VisionError("TIMEOUT_ERROR", `${providerName} timed out`, 504));
667
+ }
668
+ const ocrPass = await withTimeout(
669
+ provider.analyze({
670
+ ...analyzeRequest,
671
+ mode: "ocr",
672
+ outputFormat: "text",
673
+ prompt: buildUiOcrPrompt(input.prompt)
674
+ }),
675
+ timeoutMs,
676
+ () => new VisionError("TIMEOUT_ERROR", `${providerName} OCR pass timed out`, 504)
677
+ );
678
+ return withTimeout(
679
+ provider.analyze({
680
+ ...analyzeRequest,
681
+ outputFormat: "json",
682
+ prompt: buildUiStructuredJsonPrompt(input.prompt, ocrPass.text)
683
+ }),
684
+ timeoutMs,
685
+ () => new VisionError("TIMEOUT_ERROR", `${providerName} UI pass timed out`, 504)
686
+ );
687
+ });
688
+ const uiAnalysis = this.tryParseUiAnalysis(value, input.mode, options.outputFormat ?? "text");
689
+ const result = VisionAnalyzeResponseSchema.parse({
690
+ provider: value.provider,
691
+ model: value.model,
692
+ mode: input.mode,
693
+ outputFormat: options.outputFormat ?? "text",
694
+ analysis: value.text,
695
+ uiAnalysis,
696
+ cached: false,
697
+ image: {
698
+ width: image.width,
699
+ height: image.height,
700
+ format: image.format,
701
+ mimeType: image.mimeType,
702
+ hash: image.hash,
703
+ bytes: image.bytes
704
+ },
705
+ timingMs: durationMs
706
+ });
707
+ if (cacheEnabled) this.cache.set(cacheKey, result);
708
+ return result;
709
+ } catch (error) {
710
+ lastError = error instanceof Error ? error : new Error(String(error));
711
+ this.logger.warn(`Provider failed: ${providerName}`, { error: lastError.message });
712
+ }
713
+ }
714
+ throw new VisionError("PROVIDER_ERROR", `All providers failed. Last error: ${lastError?.message ?? "unknown"}`, 502);
715
+ }
716
+ tryParseUiAnalysis(value, mode, outputFormat) {
717
+ if (mode !== "ui_analysis" || outputFormat !== "json") return void 0;
718
+ try {
719
+ const cleaned = value.text.trim().replace(/^```json\s*/i, "").replace(/^```\s*/i, "").replace(/\s*```$/, "");
720
+ const parsed = JSON.parse(cleaned);
721
+ return parsed;
722
+ } catch {
723
+ return void 0;
724
+ }
725
+ }
726
+ };
727
+
728
+ // src/utils/logger.ts
729
+ var ORDER = {
730
+ debug: 10,
731
+ info: 20,
732
+ warn: 30,
733
+ error: 40
734
+ };
735
+ var Logger = class {
736
+ constructor(level) {
737
+ this.level = level;
738
+ }
739
+ level;
740
+ shouldLog(level) {
741
+ return ORDER[level] >= ORDER[this.level];
742
+ }
743
+ debug(message, context) {
744
+ if (this.shouldLog("debug")) console.debug(`[vision][debug] ${message}`, context ?? "");
745
+ }
746
+ info(message, context) {
747
+ if (this.shouldLog("info")) console.info(`[vision][info] ${message}`, context ?? "");
748
+ }
749
+ warn(message, context) {
750
+ if (this.shouldLog("warn")) console.warn(`[vision][warn] ${message}`, context ?? "");
751
+ }
752
+ error(message, context) {
753
+ if (this.shouldLog("error")) console.error(`[vision][error] ${message}`, context ?? "");
754
+ }
755
+ };
756
+
757
+ // src/server.ts
758
+ function createVisionServer() {
759
+ const config = loadConfig();
760
+ const logger = new Logger(config.logLevel);
761
+ const providers = {};
762
+ if (config.geminiApiKey) {
763
+ providers.gemini = new GeminiProvider(config.geminiApiKey, config.geminiModel);
764
+ }
765
+ if (config.openrouterApiKey) {
766
+ providers.openrouter = new OpenRouterProvider(config.openrouterApiKey, config.openrouterModel);
767
+ }
768
+ providers.ollama = new OllamaProvider(config.ollamaBaseUrl, config.ollamaModel);
769
+ if (!providers.gemini && !providers.openrouter && !providers.ollama) {
770
+ throw new VisionError("CONFIG_ERROR", "No provider configured.", 500);
771
+ }
772
+ const service = new VisionService(
773
+ config,
774
+ logger,
775
+ providers,
776
+ ["gemini", "openrouter", "ollama"],
777
+ new MemoryCache(config.cacheTtlSeconds)
778
+ );
779
+ const server = new import_mcp.McpServer({
780
+ name: "vision-mcp",
781
+ version: "0.1.0"
782
+ });
783
+ server.registerTool(
784
+ "vision_analyze",
785
+ {
786
+ description: "Analyze images and screenshots with Gemini-first vision pipeline.",
787
+ inputSchema: {
788
+ imageSource: import_zod3.z.string(),
789
+ prompt: import_zod3.z.string(),
790
+ mode: import_zod3.z.enum(["general", "palette", "hierarchy", "components", "ocr", "ui_analysis", "code_screenshot"]).optional(),
791
+ options: import_zod3.z.object({
792
+ temperature: import_zod3.z.number().min(0).max(2).optional(),
793
+ maxTokens: import_zod3.z.number().int().min(16).max(8192).optional(),
794
+ outputFormat: import_zod3.z.enum(["text", "json"]).optional(),
795
+ provider: import_zod3.z.enum(["gemini", "openrouter", "ollama"]).optional(),
796
+ model: import_zod3.z.string().optional(),
797
+ enableCache: import_zod3.z.boolean().optional(),
798
+ timeoutMs: import_zod3.z.number().int().min(500).max(12e4).optional()
799
+ }).optional()
800
+ },
801
+ outputSchema: VisionAnalyzeResponseSchema.shape
802
+ },
803
+ async (args) => {
804
+ try {
805
+ const input = VisionAnalyzeInputSchema.parse(args);
806
+ const result = await service.analyze(input);
807
+ return {
808
+ content: [
809
+ {
810
+ type: "text",
811
+ text: result.outputFormat === "json" ? JSON.stringify(result, null, 2) : result.analysis
812
+ }
813
+ ],
814
+ structuredContent: result
815
+ };
816
+ } catch (error) {
817
+ const vError = toVisionError(error);
818
+ return {
819
+ isError: true,
820
+ content: [
821
+ {
822
+ type: "text",
823
+ text: JSON.stringify(
824
+ {
825
+ code: vError.code,
826
+ message: vError.message
827
+ },
828
+ null,
829
+ 2
830
+ )
831
+ }
832
+ ]
833
+ };
834
+ }
835
+ }
836
+ );
837
+ return { server, config };
838
+ }
839
+ async function runServer() {
840
+ const { server, config } = createVisionServer();
841
+ const transport = new import_stdio.StdioServerTransport();
842
+ await server.connect(transport);
843
+ console.error(`[vision-mcp] running on stdio with provider=${config.visionProvider}`);
844
+ }
845
+
846
+ // src/index.ts
847
+ import_dotenv.default.config();
848
+ runServer().catch((error) => {
849
+ console.error("[vision-mcp] fatal error", error);
850
+ process.exit(1);
851
+ });
852
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/server.ts","../src/config.ts","../src/schemas.ts","../src/errors.ts","../src/cache/memoryCache.ts","../src/providers/gemini.ts","../src/providers/ollama.ts","../src/providers/openrouter.ts","../src/services/visionService.ts","../src/image/loadImage.ts","../src/image/normalize.ts","../src/image/security.ts","../src/utils/timing.ts","../src/services/prompts.ts","../src/utils/logger.ts"],"sourcesContent":["import dotenv from \"dotenv\";\n\nimport { runServer } from \"./server\";\n\ndotenv.config();\n\nrunServer().catch((error) => {\n console.error(\"[vision-mcp] fatal error\", error);\n process.exit(1);\n});\r\n","import { z } from \"zod\";\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\n\nimport { loadConfig } from \"./config\";\nimport { VisionError, toVisionError } from \"./errors\";\nimport { MemoryCache } from \"./cache/memoryCache\";\nimport { GeminiProvider } from \"./providers/gemini\";\nimport { OllamaProvider } from \"./providers/ollama\";\nimport { OpenRouterProvider } from \"./providers/openrouter\";\nimport { VisionAnalyzeInputSchema, VisionAnalyzeResponseSchema } from \"./schemas\";\nimport { VisionService } from \"./services/visionService\";\nimport { Logger } from \"./utils/logger\";\n\nexport function createVisionServer() {\n const config = loadConfig();\n const logger = new Logger(config.logLevel);\n\n const providers: Record<string, { analyze: VisionService[\"analyze\"] } | any> = {};\n\n if (config.geminiApiKey) {\n providers.gemini = new GeminiProvider(config.geminiApiKey, config.geminiModel);\n }\n if (config.openrouterApiKey) {\n providers.openrouter = new OpenRouterProvider(config.openrouterApiKey, config.openrouterModel);\n }\n providers.ollama = new OllamaProvider(config.ollamaBaseUrl, config.ollamaModel);\n\n if (!providers.gemini && !providers.openrouter && !providers.ollama) {\n throw new VisionError(\"CONFIG_ERROR\", \"No provider configured.\", 500);\n }\n\n const service = new VisionService(\n config,\n logger,\n providers,\n [\"gemini\", \"openrouter\", \"ollama\"],\n new MemoryCache(config.cacheTtlSeconds),\n );\n\n const server = new McpServer({\n name: \"vision-mcp\",\n version: \"0.1.0\",\n });\n\n server.registerTool(\n \"vision_analyze\",\n {\n description: \"Analyze images and screenshots with Gemini-first vision pipeline.\",\n inputSchema: {\n imageSource: z.string(),\n prompt: z.string(),\n mode: z.enum([\"general\", \"palette\", \"hierarchy\", \"components\", \"ocr\", \"ui_analysis\", \"code_screenshot\"]).optional(),\n options: z\n .object({\n temperature: z.number().min(0).max(2).optional(),\n maxTokens: z.number().int().min(16).max(8192).optional(),\n outputFormat: z.enum([\"text\", \"json\"]).optional(),\n provider: z.enum([\"gemini\", \"openrouter\", \"ollama\"]).optional(),\n model: z.string().optional(),\n enableCache: z.boolean().optional(),\n timeoutMs: z.number().int().min(500).max(120000).optional(),\n })\n .optional(),\n },\n outputSchema: VisionAnalyzeResponseSchema.shape,\n },\n async (args) => {\n try {\n const input = VisionAnalyzeInputSchema.parse(args);\n const result = await service.analyze(input);\n\n return {\n content: [\n {\n type: \"text\",\n text: result.outputFormat === \"json\" ? JSON.stringify(result, null, 2) : result.analysis,\n },\n ],\n structuredContent: result,\n };\n } catch (error) {\n const vError = toVisionError(error);\n return {\n isError: true,\n content: [\n {\n type: \"text\",\n text: JSON.stringify(\n {\n code: vError.code,\n message: vError.message,\n },\n null,\n 2,\n ),\n },\n ],\n };\n }\n },\n );\n\n return { server, config };\n}\n\nexport async function runServer(): Promise<void> {\n const { server, config } = createVisionServer();\n const transport = new StdioServerTransport();\n await server.connect(transport);\n console.error(`[vision-mcp] running on stdio with provider=${config.visionProvider}`);\n}\r\n","import { createHash } from \"node:crypto\";\nimport { z } from \"zod\";\n\nimport { ProviderNameSchema, type ProviderName } from \"./schemas\";\n\nconst ConfigSchema = z.object({\n visionProvider: ProviderNameSchema.default(\"gemini\"),\n geminiApiKey: z.string().optional(),\n geminiModel: z.string().default(\"gemini-2.5-flash\"),\n openrouterApiKey: z.string().optional(),\n openrouterModel: z.string().default(\"google/gemini-2.5-flash\"),\n ollamaBaseUrl: z.string().url().default(\"http://127.0.0.1:11434\"),\n ollamaModel: z.string().default(\"llava\"),\n maxImageMb: z.number().default(12),\n maxPixels: z.number().int().default(40000000),\n allowedLocalRoots: z.array(z.string()).default([]),\n enableUrlInput: z.boolean().default(true),\n enableLocalPathInput: z.boolean().default(false),\n cacheEnabled: z.boolean().default(true),\n cacheTtlSeconds: z.number().int().default(300),\n logLevel: z.enum([\"debug\", \"info\", \"warn\", \"error\"]).default(\"info\"),\n requestTimeoutMs: z.number().int().default(30000),\n});\n\nexport type AppConfig = z.infer<typeof ConfigSchema>;\n\nfunction parseBool(input: string | undefined, fallback: boolean): boolean {\n if (!input) return fallback;\n return [\"1\", \"true\", \"yes\", \"on\"].includes(input.toLowerCase());\n}\n\nfunction parseNumber(input: string | undefined, fallback: number): number {\n if (!input) return fallback;\n const value = Number(input);\n return Number.isFinite(value) ? value : fallback;\n}\n\nfunction parseRoots(input: string | undefined): string[] {\n if (!input) return [];\n return input.split(\",\").map((x) => x.trim()).filter(Boolean);\n}\n\nexport function loadConfig(env: NodeJS.ProcessEnv = process.env): AppConfig {\n const parsed = ConfigSchema.parse({\n visionProvider: (env.VISION_PROVIDER as ProviderName | undefined) ?? \"gemini\",\n geminiApiKey: env.GEMINI_API_KEY,\n geminiModel: env.GEMINI_MODEL ?? \"gemini-2.5-flash\",\n openrouterApiKey: env.OPENROUTER_API_KEY,\n openrouterModel: env.OPENROUTER_MODEL ?? \"google/gemini-2.5-flash\",\n ollamaBaseUrl: env.OLLAMA_BASE_URL ?? \"http://127.0.0.1:11434\",\n ollamaModel: env.OLLAMA_MODEL ?? \"llava\",\n maxImageMb: parseNumber(env.VISION_MAX_IMAGE_MB, 12),\n maxPixels: parseNumber(env.VISION_MAX_PIXELS, 40000000),\n allowedLocalRoots: parseRoots(env.VISION_ALLOWED_LOCAL_ROOTS),\n enableUrlInput: parseBool(env.VISION_ENABLE_URL_INPUT, true),\n enableLocalPathInput: parseBool(env.VISION_ENABLE_LOCAL_PATH_INPUT, false),\n cacheEnabled: parseBool(env.VISION_CACHE_ENABLED, true),\n cacheTtlSeconds: parseNumber(env.VISION_CACHE_TTL_SECONDS, 300),\n logLevel: (env.VISION_LOG_LEVEL as AppConfig[\"logLevel\"] | undefined) ?? \"info\",\n requestTimeoutMs: parseNumber(env.VISION_REQUEST_TIMEOUT_MS, 30000),\n });\n\n return parsed;\n}\n\nexport function maskSecret(secret: string | undefined): string {\n if (!secret) return \"<unset>\";\n return `${createHash(\"sha256\").update(secret).digest(\"hex\").slice(0, 8)}...`;\n}\r\n","import { z } from \"zod\";\n\nexport const OutputFormatSchema = z.enum([\"text\", \"json\"]);\nexport type OutputFormat = z.infer<typeof OutputFormatSchema>;\n\nexport const VisionModeSchema = z.enum([\n \"general\",\n \"palette\",\n \"hierarchy\",\n \"components\",\n \"ocr\",\n \"ui_analysis\",\n \"code_screenshot\",\n]);\nexport type VisionMode = z.infer<typeof VisionModeSchema>;\n\nexport const ProviderNameSchema = z.enum([\"gemini\", \"openrouter\", \"ollama\"]);\nexport type ProviderName = z.infer<typeof ProviderNameSchema>;\n\nexport const ToolOptionsSchema = z\n .object({\n temperature: z.number().min(0).max(2).optional(),\n maxTokens: z.number().int().min(16).max(8192).optional(),\n outputFormat: OutputFormatSchema.optional(),\n provider: ProviderNameSchema.optional(),\n model: z.string().min(1).max(200).optional(),\n enableCache: z.boolean().optional(),\n timeoutMs: z.number().int().min(500).max(120000).optional(),\n })\n .default({});\n\nexport const VisionAnalyzeInputSchema = z.object({\n imageSource: z.string().min(1),\n prompt: z.string().min(1).max(8000),\n mode: VisionModeSchema.default(\"general\"),\n options: ToolOptionsSchema.optional(),\n});\n\nexport type VisionAnalyzeInput = z.infer<typeof VisionAnalyzeInputSchema>;\n\nexport const VisionAnalyzeResponseSchema = z.object({\n provider: ProviderNameSchema,\n model: z.string(),\n mode: VisionModeSchema,\n outputFormat: OutputFormatSchema,\n analysis: z.string(),\n uiAnalysis: z\n .object({\n page_context: z.object({\n value: z.string(),\n confidence: z.number().min(0).max(1),\n }),\n sections: z.array(\n z.object({\n name: z.string(),\n observations: z.array(z.string()),\n confidence: z.number().min(0).max(1),\n }),\n ),\n controls: z.array(\n z.object({\n label: z.string(),\n type: z.string(),\n state: z.string().optional(),\n confidence: z.number().min(0).max(1),\n }),\n ),\n status_indicators: z.array(\n z.object({\n label: z.string(),\n value: z.string(),\n confidence: z.number().min(0).max(1),\n }),\n ),\n ocr_anchors: z.array(z.string()),\n uncertainties: z.array(z.string()),\n inferred_interpretations: z.array(z.string()),\n })\n .optional(),\n cached: z.boolean(),\n image: z.object({\n width: z.number().int(),\n height: z.number().int(),\n format: z.string(),\n mimeType: z.string(),\n hash: z.string(),\n bytes: z.number().int(),\n }),\n timingMs: z.number().int(),\n});\n\nexport type VisionAnalyzeResponse = z.infer<typeof VisionAnalyzeResponseSchema>;\r\n","export type ErrorCode =\n | \"CONFIG_ERROR\"\n | \"VALIDATION_ERROR\"\n | \"SECURITY_ERROR\"\n | \"INPUT_ERROR\"\n | \"TIMEOUT_ERROR\"\n | \"RATE_LIMITED\"\n | \"PROVIDER_ERROR\"\n | \"INTERNAL_ERROR\";\n\nexport class VisionError extends Error {\n readonly code: ErrorCode;\n readonly status: number;\n readonly details?: unknown;\n\n constructor(code: ErrorCode, message: string, status = 500, details?: unknown) {\n super(message);\n this.name = \"VisionError\";\n this.code = code;\n this.status = status;\n this.details = details;\n }\n}\n\nexport function toVisionError(error: unknown): VisionError {\n if (error instanceof VisionError) return error;\n if (error instanceof Error) {\n return new VisionError(\"INTERNAL_ERROR\", error.message, 500);\n }\n return new VisionError(\"INTERNAL_ERROR\", \"Unknown error\", 500, error);\n}\r\n","export class MemoryCache<T> {\n private readonly store = new Map<string, { expiresAt: number; value: T }>();\n\n constructor(private readonly ttlSeconds: number) {}\n\n get(key: string): T | undefined {\n const existing = this.store.get(key);\n if (!existing) return undefined;\n if (Date.now() > existing.expiresAt) {\n this.store.delete(key);\n return undefined;\n }\n return existing.value;\n }\n\n set(key: string, value: T): void {\n this.store.set(key, {\n value,\n expiresAt: Date.now() + this.ttlSeconds * 1000,\n });\n }\n}\r\n","import { GoogleGenAI } from \"@google/genai\";\n\nimport { VisionError } from \"../errors\";\nimport type { AnalyzeRequest } from \"../image/types\";\nimport type { VisionProvider, ProviderResult } from \"./base\";\n\nexport class GeminiProvider implements VisionProvider {\n readonly name = \"gemini\" as const;\n private readonly client: GoogleGenAI;\n\n constructor(private readonly apiKey: string, private readonly defaultModel: string) {\n this.client = new GoogleGenAI({ apiKey });\n }\n\n async analyze(input: AnalyzeRequest): Promise<ProviderResult> {\n const model = input.model ?? this.defaultModel;\n\n try {\n const response = await this.client.models.generateContent({\n model,\n contents: [\n {\n role: \"user\",\n parts: [\n { text: input.prompt },\n {\n inlineData: {\n mimeType: input.image.mimeType,\n data: input.image.data.toString(\"base64\"),\n },\n },\n ],\n },\n ],\n config: {\n maxOutputTokens: input.maxTokens,\n temperature: input.temperature,\n responseMimeType: input.outputFormat === \"json\" ? \"application/json\" : \"text/plain\",\n },\n });\n\n const text = response.text?.trim();\n if (!text) {\n throw new VisionError(\"PROVIDER_ERROR\", \"Gemini returned an empty response.\", 502);\n }\n\n return { text, model, provider: this.name };\n } catch (error) {\n if (error instanceof VisionError) throw error;\n const msg = error instanceof Error ? error.message : \"Gemini request failed\";\n if (/429|rate/i.test(msg)) {\n throw new VisionError(\"RATE_LIMITED\", `Gemini rate limited: ${msg}`, 429);\n }\n throw new VisionError(\"PROVIDER_ERROR\", `Gemini provider failed: ${msg}`, 502);\n }\n }\n}\r\n","import { VisionError } from \"../errors\";\nimport type { AnalyzeRequest } from \"../image/types\";\nimport type { VisionProvider, ProviderResult } from \"./base\";\n\nexport class OllamaProvider implements VisionProvider {\n readonly name = \"ollama\" as const;\n\n constructor(private readonly baseUrl: string, private readonly defaultModel: string) {}\n\n async analyze(input: AnalyzeRequest): Promise<ProviderResult> {\n const model = input.model ?? this.defaultModel;\n const response = await fetch(`${this.baseUrl}/api/generate`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({\n model,\n prompt: input.prompt,\n images: [input.image.data.toString(\"base64\")],\n stream: false,\n options: {\n temperature: input.temperature,\n num_predict: input.maxTokens,\n },\n }),\n });\n\n if (!response.ok) {\n throw new VisionError(\"PROVIDER_ERROR\", `Ollama failed: HTTP ${response.status}`, 502);\n }\n\n const json = (await response.json()) as { response?: string };\n const text = json.response?.trim();\n if (!text) {\n throw new VisionError(\"PROVIDER_ERROR\", \"Ollama returned empty response.\", 502);\n }\n\n return { text, model, provider: this.name };\n }\n}\r\n","import { VisionError } from \"../errors\";\nimport type { AnalyzeRequest } from \"../image/types\";\nimport type { VisionProvider, ProviderResult } from \"./base\";\n\nexport class OpenRouterProvider implements VisionProvider {\n readonly name = \"openrouter\" as const;\n\n constructor(private readonly apiKey: string, private readonly defaultModel: string) {}\n\n async analyze(input: AnalyzeRequest): Promise<ProviderResult> {\n const model = input.model ?? this.defaultModel;\n const response = await fetch(\"https://openrouter.ai/api/v1/chat/completions\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.apiKey}`,\n },\n body: JSON.stringify({\n model,\n temperature: input.temperature,\n max_tokens: input.maxTokens,\n messages: [\n {\n role: \"user\",\n content: [\n { type: \"text\", text: input.prompt },\n {\n type: \"image_url\",\n image_url: {\n url: `data:${input.image.mimeType};base64,${input.image.data.toString(\"base64\")}`,\n },\n },\n ],\n },\n ],\n }),\n });\n\n if (!response.ok) {\n throw new VisionError(\"PROVIDER_ERROR\", `OpenRouter failed: HTTP ${response.status}`, 502);\n }\n\n const json = (await response.json()) as {\n choices?: Array<{ message?: { content?: string } }>;\n };\n\n const text = json.choices?.[0]?.message?.content?.trim();\n if (!text) {\n throw new VisionError(\"PROVIDER_ERROR\", \"OpenRouter returned empty content.\", 502);\n }\n\n return { text, model, provider: this.name };\n }\n}\r\n","import { createHash } from \"node:crypto\";\n\nimport type { AppConfig } from \"../config\";\nimport { VisionError } from \"../errors\";\nimport { loadImageFromSource } from \"../image/loadImage\";\nimport type { AnalyzeRequest } from \"../image/types\";\nimport { type VisionAnalyzeInput, VisionAnalyzeResponseSchema, type VisionAnalyzeResponse } from \"../schemas\";\nimport type { Logger } from \"../utils/logger\";\nimport { timed, withTimeout } from \"../utils/timing\";\nimport { MemoryCache } from \"../cache/memoryCache\";\nimport type { VisionProvider, ProviderResult } from \"../providers/base\";\nimport { buildPrompt, buildUiOcrPrompt, buildUiStructuredJsonPrompt } from \"./prompts\";\n\nexport class VisionService {\n constructor(\n private readonly config: AppConfig,\n private readonly logger: Logger,\n private readonly providers: Record<string, VisionProvider>,\n private readonly providerOrder: string[],\n private readonly cache: MemoryCache<VisionAnalyzeResponse>,\n ) {}\n\n private getCacheKey(input: {\n hash: string;\n prompt: string;\n mode: string;\n provider: string;\n model: string;\n outputFormat: string;\n }): string {\n return createHash(\"sha256\")\n .update(JSON.stringify(input))\n .digest(\"hex\");\n }\n\n private selectProvider(explicit?: string): string[] {\n if (explicit) return [explicit, ...this.providerOrder.filter((p) => p !== explicit)];\n return [...this.providerOrder];\n }\n\n async analyze(input: VisionAnalyzeInput): Promise<VisionAnalyzeResponse> {\n const options = input.options ?? {};\n const timeoutMs = options.timeoutMs ?? this.config.requestTimeoutMs;\n const image = await loadImageFromSource(input.imageSource, this.config, timeoutMs);\n const prompt = buildPrompt(input.mode, input.prompt, options.outputFormat ?? \"text\");\n\n const orderedProviders = this.selectProvider(options.provider ?? this.config.visionProvider);\n\n const cacheKey = this.getCacheKey({\n hash: image.hash,\n prompt,\n mode: input.mode,\n provider: orderedProviders[0],\n model: options.model ?? \"default\",\n outputFormat: options.outputFormat ?? \"text\",\n });\n\n const cacheEnabled = options.enableCache ?? this.config.cacheEnabled;\n if (cacheEnabled) {\n const cached = this.cache.get(cacheKey);\n if (cached) return { ...cached, cached: true };\n }\n\n const analyzeRequest: AnalyzeRequest = {\n image,\n prompt,\n mode: input.mode,\n outputFormat: options.outputFormat ?? \"text\",\n temperature: options.temperature,\n maxTokens: options.maxTokens,\n timeoutMs,\n model: options.model,\n provider: options.provider,\n };\n\n let lastError: Error | undefined;\n\n for (const providerName of orderedProviders) {\n const provider = this.providers[providerName];\n if (!provider) continue;\n\n try {\n const { value, durationMs } = await timed(async () => {\n if (input.mode !== \"ui_analysis\" || (options.outputFormat ?? \"text\") !== \"json\") {\n return withTimeout(provider.analyze(analyzeRequest), timeoutMs, () => new VisionError(\"TIMEOUT_ERROR\", `${providerName} timed out`, 504));\n }\n\n const ocrPass = await withTimeout(\n provider.analyze({\n ...analyzeRequest,\n mode: \"ocr\",\n outputFormat: \"text\",\n prompt: buildUiOcrPrompt(input.prompt),\n }),\n timeoutMs,\n () => new VisionError(\"TIMEOUT_ERROR\", `${providerName} OCR pass timed out`, 504),\n );\n\n return withTimeout(\n provider.analyze({\n ...analyzeRequest,\n outputFormat: \"json\",\n prompt: buildUiStructuredJsonPrompt(input.prompt, ocrPass.text),\n }),\n timeoutMs,\n () => new VisionError(\"TIMEOUT_ERROR\", `${providerName} UI pass timed out`, 504),\n );\n });\n\n const uiAnalysis = this.tryParseUiAnalysis(value, input.mode, options.outputFormat ?? \"text\");\n\n const result: VisionAnalyzeResponse = VisionAnalyzeResponseSchema.parse({\n provider: value.provider,\n model: value.model,\n mode: input.mode,\n outputFormat: options.outputFormat ?? \"text\",\n analysis: value.text,\n uiAnalysis,\n cached: false,\n image: {\n width: image.width,\n height: image.height,\n format: image.format,\n mimeType: image.mimeType,\n hash: image.hash,\n bytes: image.bytes,\n },\n timingMs: durationMs,\n });\n\n if (cacheEnabled) this.cache.set(cacheKey, result);\n return result;\n } catch (error) {\n lastError = error instanceof Error ? error : new Error(String(error));\n this.logger.warn(`Provider failed: ${providerName}`, { error: lastError.message });\n }\n }\n\n throw new VisionError(\"PROVIDER_ERROR\", `All providers failed. Last error: ${lastError?.message ?? \"unknown\"}`, 502);\n }\n\n private tryParseUiAnalysis(value: ProviderResult, mode: VisionAnalyzeInput[\"mode\"], outputFormat: \"text\" | \"json\") {\n if (mode !== \"ui_analysis\" || outputFormat !== \"json\") return undefined;\n try {\n const cleaned = value.text.trim().replace(/^```json\\s*/i, \"\").replace(/^```\\s*/i, \"\").replace(/\\s*```$/, \"\");\n const parsed = JSON.parse(cleaned) as VisionAnalyzeResponse[\"uiAnalysis\"];\n return parsed;\n } catch {\n return undefined;\n }\n }\n}\n","import fs from \"node:fs/promises\";\n\nimport type { AppConfig } from \"../config\";\nimport { VisionError } from \"../errors\";\nimport { normalizeImage } from \"./normalize\";\nimport { validateLocalPath, validateRemoteUrl } from \"./security\";\nimport type { ImageAsset, ImageSourceKind } from \"./types\";\n\nfunction isDataUrl(input: string): boolean {\n return input.startsWith(\"data:image/\");\n}\n\nfunction isBase64(input: string): boolean {\n return /^[A-Za-z0-9+/=\\r\\n]+$/.test(input) && input.length > 128;\n}\n\nfunction parseDataUrl(input: string): Buffer {\n const match = /^data:(image\\/[\\w+.-]+);base64,(.+)$/i.exec(input);\n if (!match) {\n throw new VisionError(\"INPUT_ERROR\", \"Invalid image data URL.\", 400);\n }\n\n try {\n return Buffer.from(match[2], \"base64\");\n } catch {\n throw new VisionError(\"INPUT_ERROR\", \"Invalid base64 in data URL.\", 400);\n }\n}\n\nasync function loadFromUrl(url: string, timeoutMs: number): Promise<Buffer> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n\n try {\n const response = await fetch(url, { signal: controller.signal });\n if (!response.ok) {\n throw new VisionError(\"INPUT_ERROR\", `Failed to fetch image URL: HTTP ${response.status}`, 400);\n }\n const buf = Buffer.from(await response.arrayBuffer());\n return buf;\n } catch (error) {\n if (error instanceof VisionError) throw error;\n throw new VisionError(\"INPUT_ERROR\", \"Failed to fetch remote image.\", 400, error);\n } finally {\n clearTimeout(timer);\n }\n}\n\nexport async function loadImageFromSource(imageSource: string, config: AppConfig, timeoutMs: number): Promise<ImageAsset> {\n let raw: Buffer;\n let sourceKind: ImageSourceKind;\n\n if (isDataUrl(imageSource)) {\n raw = parseDataUrl(imageSource);\n sourceKind = \"data-url\";\n } else if (/^https?:\\/\\//i.test(imageSource)) {\n if (!config.enableUrlInput) {\n throw new VisionError(\"SECURITY_ERROR\", \"URL input is disabled.\", 403);\n }\n\n const safe = await validateRemoteUrl(imageSource);\n raw = await loadFromUrl(safe.toString(), timeoutMs);\n sourceKind = \"url\";\n } else if (config.enableLocalPathInput) {\n const localPath = validateLocalPath(imageSource, config.allowedLocalRoots);\n raw = await fs.readFile(localPath);\n sourceKind = \"local\";\n } else if (isBase64(imageSource)) {\n try {\n raw = Buffer.from(imageSource, \"base64\");\n sourceKind = \"base64\";\n } catch {\n throw new VisionError(\"INPUT_ERROR\", \"Invalid base64 image input.\", 400);\n }\n } else {\n throw new VisionError(\"INPUT_ERROR\", \"Unsupported image source. Use URL, local path, base64, or data URL.\", 400);\n }\n\n if (!raw || raw.byteLength === 0) {\n throw new VisionError(\"INPUT_ERROR\", \"Empty image data.\", 400);\n }\n\n return normalizeImage(raw, sourceKind, config);\n}\r\n","import { createHash } from \"node:crypto\";\nimport sharp from \"sharp\";\n\nimport { VisionError } from \"../errors\";\nimport type { AppConfig } from \"../config\";\nimport type { ImageAsset, ImageSourceKind } from \"./types\";\n\nconst SUPPORTED_MIME = new Map<string, string>([\n [\"jpeg\", \"image/jpeg\"],\n [\"png\", \"image/png\"],\n [\"webp\", \"image/webp\"],\n [\"gif\", \"image/gif\"],\n]);\n\nexport async function normalizeImage(input: Buffer, sourceKind: ImageSourceKind, config: AppConfig): Promise<ImageAsset> {\n const maxBytes = Math.floor(config.maxImageMb * 1024 * 1024);\n if (input.byteLength > maxBytes) {\n throw new VisionError(\"INPUT_ERROR\", `Image is too large. Max is ${config.maxImageMb}MB.`, 400);\n }\n\n const metadata = await sharp(input, { failOn: \"error\" }).metadata();\n if (!metadata.width || !metadata.height || !metadata.format) {\n throw new VisionError(\"INPUT_ERROR\", \"Unsupported image or corrupted data.\", 400);\n }\n\n if (metadata.width * metadata.height > config.maxPixels) {\n throw new VisionError(\"INPUT_ERROR\", `Image pixel count exceeds limit of ${config.maxPixels}.`, 400);\n }\n\n let pipeline = sharp(input, { failOn: \"error\" }).rotate();\n const shouldResize = metadata.width > 2200 || metadata.height > 2200;\n if (shouldResize) {\n pipeline = pipeline.resize({ width: 2200, height: 2200, fit: \"inside\", withoutEnlargement: true });\n }\n\n const normalizedBuffer = await pipeline\n .webp({ quality: 85, effort: 4 })\n .toBuffer();\n\n const finalMeta = await sharp(normalizedBuffer).metadata();\n const format = finalMeta.format ?? \"webp\";\n const mimeType = SUPPORTED_MIME.get(format) ?? \"image/webp\";\n\n return {\n data: normalizedBuffer,\n format,\n mimeType,\n width: finalMeta.width ?? metadata.width,\n height: finalMeta.height ?? metadata.height,\n bytes: normalizedBuffer.byteLength,\n hash: createHash(\"sha256\").update(normalizedBuffer).digest(\"hex\"),\n sourceKind,\n };\n}\r\n","import dns from \"node:dns/promises\";\nimport net from \"node:net\";\nimport path from \"node:path\";\nimport { URL } from \"node:url\";\n\nimport { VisionError } from \"../errors\";\n\nconst BLOCKED_HOSTNAMES = new Set([\"localhost\", \"metadata.google.internal\"]);\n\nconst PRIVATE_CIDR_CHECKS = [\n /^10\\./,\n /^127\\./,\n /^169\\.254\\./,\n /^172\\.(1[6-9]|2\\d|3[0-1])\\./,\n /^192\\.168\\./,\n /^0\\./,\n];\n\nfunction isPrivateIpv4(ip: string): boolean {\n return PRIVATE_CIDR_CHECKS.some((r) => r.test(ip));\n}\n\nfunction isBlockedIpv6(ip: string): boolean {\n const normalized = ip.toLowerCase();\n return normalized === \"::1\" || normalized.startsWith(\"fc\") || normalized.startsWith(\"fd\") || normalized.startsWith(\"fe80:\");\n}\n\nexport async function validateRemoteUrl(rawUrl: string): Promise<URL> {\n let parsed: URL;\n try {\n parsed = new URL(rawUrl);\n } catch {\n throw new VisionError(\"SECURITY_ERROR\", \"Invalid URL.\", 400);\n }\n\n if (![\"http:\", \"https:\"].includes(parsed.protocol)) {\n throw new VisionError(\"SECURITY_ERROR\", \"Only HTTP/HTTPS URLs are allowed.\", 400);\n }\n\n if (BLOCKED_HOSTNAMES.has(parsed.hostname.toLowerCase())) {\n throw new VisionError(\"SECURITY_ERROR\", \"URL host is blocked.\", 403);\n }\n\n const host = parsed.hostname;\n if (net.isIP(host)) {\n if ((net.isIPv4(host) && isPrivateIpv4(host)) || (net.isIPv6(host) && isBlockedIpv6(host))) {\n throw new VisionError(\"SECURITY_ERROR\", \"Private or loopback IP is blocked.\", 403);\n }\n return parsed;\n }\n\n const resolved = await dns.lookup(host, { all: true });\n for (const addr of resolved) {\n if ((addr.family === 4 && isPrivateIpv4(addr.address)) || (addr.family === 6 && isBlockedIpv6(addr.address))) {\n throw new VisionError(\"SECURITY_ERROR\", \"Resolved IP is private/loopback and blocked.\", 403);\n }\n }\n\n return parsed;\n}\n\nexport function validateLocalPath(inputPath: string, allowedRoots: string[]): string {\n const resolved = path.resolve(inputPath);\n\n if (allowedRoots.length === 0) {\n throw new VisionError(\"SECURITY_ERROR\", \"Local file access disabled: allowlist is empty.\", 403);\n }\n\n const isAllowed = allowedRoots.some((root) => {\n const resolvedRoot = path.resolve(root);\n const relative = path.relative(resolvedRoot, resolved);\n return (relative === \"\" || (!relative.startsWith(\"..\") && !path.isAbsolute(relative)));\n });\n\n if (!isAllowed) {\n throw new VisionError(\"SECURITY_ERROR\", \"Local path is outside allowed roots.\", 403);\n }\n\n return resolved;\n}\r\n","export async function withTimeout<T>(promise: Promise<T>, timeoutMs: number, onTimeout: () => Error): Promise<T> {\n let timer: NodeJS.Timeout | undefined;\n try {\n const timeoutPromise = new Promise<T>((_, reject) => {\n timer = setTimeout(() => reject(onTimeout()), timeoutMs);\n });\n\n return await Promise.race([promise, timeoutPromise]);\n } finally {\n if (timer) clearTimeout(timer);\n }\n}\n\nexport async function timed<T>(fn: () => Promise<T>): Promise<{ value: T; durationMs: number }> {\n const start = Date.now();\n const value = await fn();\n return { value, durationMs: Date.now() - start };\n}\r\n","import type { VisionMode } from \"../schemas\";\n\nconst MODE_GUIDANCE: Record<VisionMode, string> = {\n general: \"Provide a clear and accurate description with key details and context.\",\n palette: \"Extract visual design tokens: colors, spacing, typography, shadows, radius, and style patterns.\",\n hierarchy: \"Analyze hierarchy, attention flow, focus areas, layout clarity, and readability.\",\n components: \"Identify UI components, consistency, reuse quality, interaction patterns, and design-system maturity.\",\n ocr: \"Extract all visible text accurately. Preserve ordering and structure where possible.\",\n ui_analysis: \"Analyze the software UI: goals, flows, usability, information architecture, and issues.\",\n code_screenshot: \"Analyze code/terminal screenshot: summarize code intent, errors, stack traces, and likely fixes.\",\n};\n\nexport function buildPrompt(mode: VisionMode, userPrompt: string, outputFormat: \"text\" | \"json\"): string {\n const uiFactPolicy =\n mode === \"ui_analysis\"\n ? \"Strict policy: separate observed facts from inferred interpretations. If uncertain, explicitly list uncertainty instead of asserting.\"\n : \"\";\n\n const format =\n outputFormat === \"json\"\n ? \"Return strict JSON with stable keys and no markdown.\"\n : \"Return concise plain text with headings when useful.\";\n\n return [\n \"You are an expert vision analyst for images and screenshots.\",\n `Mode: ${mode}. ${MODE_GUIDANCE[mode]}`,\n uiFactPolicy,\n format,\n `User request: ${userPrompt}`,\n ].join(\"\\n\\n\");\n}\n\nexport function buildUiOcrPrompt(userPrompt: string): string {\n return [\n \"You are extracting OCR anchors from a software screenshot.\",\n \"Return only visible text anchors and short structural labels.\",\n \"No assumptions. Include exact labels, button text, section titles, and model IDs if visible.\",\n \"Output plain text lines only.\",\n `User request context: ${userPrompt}`,\n ].join(\"\\n\\n\");\n}\n\nexport function buildUiStructuredJsonPrompt(userPrompt: string, ocrAnchors: string): string {\n return [\n \"You are an expert UI screenshot analyst.\",\n \"Strict policy: separate observed facts from inferred interpretations. If uncertain, include the item under uncertainties.\",\n \"Use OCR anchors as grounding evidence and do not invent labels not supported by image evidence.\",\n \"Return strict JSON only with this exact shape:\",\n JSON.stringify(\n {\n page_context: { value: \"string\", confidence: 0.0 },\n sections: [{ name: \"string\", observations: [\"string\"], confidence: 0.0 }],\n controls: [{ label: \"string\", type: \"string\", state: \"optional\", confidence: 0.0 }],\n status_indicators: [{ label: \"string\", value: \"string\", confidence: 0.0 }],\n ocr_anchors: [\"string\"],\n uncertainties: [\"string\"],\n inferred_interpretations: [\"string\"],\n },\n null,\n 2,\n ),\n `OCR anchors:\\n${ocrAnchors || \"<none>\"}`,\n `User request: ${userPrompt}`,\n ].join(\"\\n\\n\");\n}\n","type LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\";\n\nconst ORDER: Record<LogLevel, number> = {\n debug: 10,\n info: 20,\n warn: 30,\n error: 40,\n};\n\nexport class Logger {\n constructor(private readonly level: LogLevel) {}\n\n private shouldLog(level: LogLevel): boolean {\n return ORDER[level] >= ORDER[this.level];\n }\n\n debug(message: string, context?: unknown): void {\n if (this.shouldLog(\"debug\")) console.debug(`[vision][debug] ${message}`, context ?? \"\");\n }\n\n info(message: string, context?: unknown): void {\n if (this.shouldLog(\"info\")) console.info(`[vision][info] ${message}`, context ?? \"\");\n }\n\n warn(message: string, context?: unknown): void {\n if (this.shouldLog(\"warn\")) console.warn(`[vision][warn] ${message}`, context ?? \"\");\n }\n\n error(message: string, context?: unknown): void {\n if (this.shouldLog(\"error\")) console.error(`[vision][error] ${message}`, context ?? \"\");\n }\n}\r\n"],"mappings":"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","names":["import_zod","import_zod","import_node_crypto","import_promises","import_node_crypto","sharp","net","dns","path","fs","dotenv"]}
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "fazee-vision-mcp",
3
+ "version": "0.1.0",
4
+ "description": "Production-grade Vision MCP server for image and screenshot analysis",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "dev": "tsx src/index.ts",
9
+ "build": "tsup src/index.ts --format cjs --dts --sourcemap --clean",
10
+ "test": "vitest run",
11
+ "test:watch": "vitest",
12
+ "typecheck": "tsc --noEmit"
13
+ },
14
+ "bin": {
15
+ "fazee-vision-mcp": "dist/index.js"
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "README.md",
20
+ ".env.example"
21
+ ],
22
+ "keywords": [
23
+ "mcp",
24
+ "vision",
25
+ "gemini",
26
+ "screenshot",
27
+ "typescript"
28
+ ],
29
+ "author": "",
30
+ "license": "MIT",
31
+ "type": "commonjs",
32
+ "engines": {
33
+ "node": ">=20.0.0"
34
+ },
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
38
+ "dependencies": {
39
+ "@google/genai": "^2.2.0",
40
+ "@modelcontextprotocol/sdk": "^1.29.0",
41
+ "dotenv": "^17.4.2",
42
+ "sharp": "^0.34.5",
43
+ "zod": "^4.4.3"
44
+ },
45
+ "devDependencies": {
46
+ "@types/node": "^25.7.0",
47
+ "tsup": "^8.5.1",
48
+ "tsx": "^4.21.0",
49
+ "typescript": "^6.0.3",
50
+ "vitest": "^4.1.6"
51
+ }
52
+ }