@xyzensun/visionmcp 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/README.md ADDED
@@ -0,0 +1,120 @@
1
+ # @xyzensun/visionmcp
2
+
3
+ [English](./README.md) | [中文](./README_CN.md)
4
+
5
+ A Node.js MCP stdio server that exposes a single image-understanding tool (`readimg`) for MCP clients that lack native multimodal input. The tool forwards the image to a user-configured multimodal model and returns plain text.
6
+
7
+ Supports three upstream request formats out of the box: **OpenAI**, **Anthropic**, **Gemini**.
8
+
9
+ ## Install / use via MCP
10
+
11
+ No global install needed. Configure your MCP client to launch the package via `npx`:
12
+
13
+ ```json
14
+ {
15
+ "mcpServers": {
16
+ "visionmcp": {
17
+ "command": "npx",
18
+ "args": ["-y", "@xyzensun/visionmcp"],
19
+ "env": {
20
+ "BASE_URL": "https://api-inference.modelscope.ai",
21
+ "FORMAT": "openai",
22
+ "API_KEY": "your_api_key_here",
23
+ "MODEL": "Qwen/Qwen3-VL-235B-A22B-Instruct"
24
+ }
25
+ }
26
+ }
27
+ }
28
+ ```
29
+
30
+ > Do not commit `API_KEY` or any MCP config file containing secrets.
31
+
32
+ ## Required environment variables
33
+
34
+ | Variable | Description |
35
+ | ---------- | ------------------------------------------------------------------------------------------------- |
36
+ | `BASE_URL` | Service root URL, e.g. `https://api-inference.modelscope.ai`. **Do not include the API path** — paths are appended automatically per `FORMAT`. |
37
+ | `FORMAT` | One of `openai`, `anthropic`, `gemini`. |
38
+ | `API_KEY` | Upstream provider API key. |
39
+ | `MODEL` | Model identifier sent to the upstream endpoint. |
40
+
41
+ ## Tool: `readimg`
42
+
43
+ Parameters:
44
+
45
+ ```ts
46
+ {
47
+ image_path?: string; // absolute local file path
48
+ image_base64?: string; // raw base64 or data URL (data:image/png;base64,...)
49
+ mime_type?: string; // optional MIME override
50
+ prompt?: string; // optional; default: "请详细描述这张图片"
51
+ timeout_seconds?: number; // optional; default 120; aborts upstream on timeout
52
+ }
53
+ ```
54
+
55
+ - Exactly one of `image_path` or `image_base64` must be provided.
56
+ - `image_path` must be a local path; `http://` / `https://` URLs are rejected.
57
+ - MIME inference: file extension for local paths, prefix for data URLs, fallback `image/png`.
58
+ - On timeout, the upstream request is aborted via `AbortSignal` and the tool returns `timeout after <N> seconds`.
59
+
60
+ ### Local path example
61
+
62
+ ```json
63
+ {
64
+ "image_path": "/absolute/path/to/image.png",
65
+ "prompt": "Describe the main objects in this image."
66
+ }
67
+ ```
68
+
69
+ ### Base64 example
70
+
71
+ ```json
72
+ {
73
+ "image_base64": "data:image/png;base64,iVBORw0KGgo=",
74
+ "prompt": "What is in this image?",
75
+ "timeout_seconds": 60
76
+ }
77
+ ```
78
+
79
+ ## Provider path conventions
80
+
81
+ | Format | Method | URL |
82
+ | ----------- | ------ | --------------------------------------------------------- |
83
+ | `openai` | POST | `{BASE_URL}/v1/chat/completions` |
84
+ | `anthropic` | POST | `{BASE_URL}/v1/messages` |
85
+ | `gemini` | POST | `{BASE_URL}/v1beta/models/{MODEL}:generateContent` |
86
+
87
+ All requests are non-streaming and send `temperature: 0.7`. `max_tokens` is not set.
88
+
89
+ ## What is not supported
90
+
91
+ - Remote image URLs (`http://` / `https://` in `image_path`).
92
+ - Multiple images per call.
93
+ - Structured JSON output.
94
+ - Caller-provided `temperature` / `max_tokens`.
95
+ - Local path allowlists or isolation.
96
+ - Custom upstream endpoint paths beyond the conventions above.
97
+
98
+ ## Security notes
99
+
100
+ Local files are read with the server process's filesystem permissions; the MVP does not restrict paths. Use only in trusted local MCP environments under your own control. Remote URL fetching is disabled to avoid SSRF.
101
+
102
+ ## Development
103
+
104
+ ```bash
105
+ npm install
106
+ npm run build
107
+ npm test
108
+ ```
109
+
110
+ Manual MCP client smoke test:
111
+
112
+ ```bash
113
+ API_KEY=<your_key> \
114
+ MODEL=Qwen/Qwen3-VL-235B-A22B-Instruct \
115
+ node test/mcp-client.mjs ./test/sample.png "Describe this image"
116
+ ```
117
+
118
+ ## License
119
+
120
+ MIT
@@ -0,0 +1,9 @@
1
+ export declare const SUPPORTED_FORMATS: readonly ["openai", "anthropic", "gemini"];
2
+ export type ProviderFormat = (typeof SUPPORTED_FORMATS)[number];
3
+ export type ServerConfig = {
4
+ baseUrl: string;
5
+ format: ProviderFormat;
6
+ apiKey: string;
7
+ model: string;
8
+ };
9
+ export declare function loadConfig(env?: NodeJS.ProcessEnv): ServerConfig;
package/dist/config.js ADDED
@@ -0,0 +1,28 @@
1
+ import { z } from "zod";
2
+ export const SUPPORTED_FORMATS = ["openai", "anthropic", "gemini"];
3
+ const envSchema = z.object({
4
+ BASE_URL: z.string().trim().min(1, "BASE_URL is required"),
5
+ FORMAT: z.enum(SUPPORTED_FORMATS, {
6
+ errorMap: () => ({ message: "FORMAT must be one of openai, anthropic, or gemini" }),
7
+ }),
8
+ API_KEY: z.string().trim().min(1, "API_KEY is required"),
9
+ MODEL: z.string().trim().min(1, "MODEL is required"),
10
+ });
11
+ export function loadConfig(env = process.env) {
12
+ const parsed = envSchema.safeParse(env);
13
+ if (!parsed.success) {
14
+ const message = parsed.error.issues.map(formatConfigIssue).join("; ");
15
+ throw new Error(message);
16
+ }
17
+ return {
18
+ baseUrl: parsed.data.BASE_URL.replace(/\/+$/, ""),
19
+ format: parsed.data.FORMAT,
20
+ apiKey: parsed.data.API_KEY,
21
+ model: parsed.data.MODEL,
22
+ };
23
+ }
24
+ function formatConfigIssue(issue) {
25
+ const name = issue.path.join(".");
26
+ return name ? `${name}: ${issue.message}` : issue.message;
27
+ }
28
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAiB,MAAM,KAAK,CAAC;AAEvC,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,QAAQ,EAAE,WAAW,EAAE,QAAQ,CAAU,CAAC;AAU5E,MAAM,SAAS,GAAG,CAAC,CAAC,MAAM,CAAC;IACzB,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,sBAAsB,CAAC;IAC1D,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,iBAAiB,EAAE;QAChC,QAAQ,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,oDAAoD,EAAE,CAAC;KACpF,CAAC;IACF,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,qBAAqB,CAAC;IACxD,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,mBAAmB,CAAC;CACrD,CAAC,CAAC;AAEH,MAAM,UAAU,UAAU,CAAC,MAAyB,OAAO,CAAC,GAAG;IAC7D,MAAM,MAAM,GAAG,SAAS,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;IAExC,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACtE,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC;IAC3B,CAAC;IACD,OAAO;QACL,OAAO,EAAE,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;QACjD,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM;QAC1B,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,OAAO;QAC3B,KAAK,EAAE,MAAM,CAAC,IAAI,CAAC,KAAK;KACzB,CAAC;AACJ,CAAC;AAED,SAAS,iBAAiB,CAAC,KAAe;IACxC,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAClC,OAAO,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,KAAK,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC;AAC5D,CAAC"}
@@ -0,0 +1,6 @@
1
+ export declare class UpstreamError extends Error {
2
+ constructor(status: number, body: string);
3
+ }
4
+ export declare function parseJsonResponse(response: Response): Promise<unknown>;
5
+ export declare function truncateSummary(value: string): string;
6
+ export declare function asErrorMessage(error: unknown): string;
package/dist/errors.js ADDED
@@ -0,0 +1,32 @@
1
+ const UPSTREAM_SUMMARY_LIMIT = 500;
2
+ export class UpstreamError extends Error {
3
+ constructor(status, body) {
4
+ super(`Upstream request failed with status ${status}: ${truncateSummary(body)}`);
5
+ this.name = "UpstreamError";
6
+ }
7
+ }
8
+ export async function parseJsonResponse(response) {
9
+ const text = await response.text();
10
+ if (!response.ok) {
11
+ throw new UpstreamError(response.status, text);
12
+ }
13
+ if (!text.trim()) {
14
+ return {};
15
+ }
16
+ try {
17
+ return JSON.parse(text);
18
+ }
19
+ catch {
20
+ throw new Error("Upstream response was not valid JSON");
21
+ }
22
+ }
23
+ export function truncateSummary(value) {
24
+ const normalized = value.replace(/\s+/g, " ").trim();
25
+ return normalized.length > UPSTREAM_SUMMARY_LIMIT
26
+ ? `${normalized.slice(0, UPSTREAM_SUMMARY_LIMIT)}...`
27
+ : normalized;
28
+ }
29
+ export function asErrorMessage(error) {
30
+ return error instanceof Error ? error.message : String(error);
31
+ }
32
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,MAAM,sBAAsB,GAAG,GAAG,CAAC;AAEnC,MAAM,OAAO,aAAc,SAAQ,KAAK;IACtC,YAAY,MAAc,EAAE,IAAY;QACtC,KAAK,CAAC,uCAAuC,MAAM,KAAK,eAAe,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACjF,IAAI,CAAC,IAAI,GAAG,eAAe,CAAC;IAC9B,CAAC;CACF;AAED,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,QAAkB;IACxD,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;IAEnC,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,IAAI,aAAa,CAAC,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IACjD,CAAC;IAED,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC;QACjB,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAY,CAAC;IACrC,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;IAC1D,CAAC;AACH,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,KAAa;IAC3C,MAAM,UAAU,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IACrD,OAAO,UAAU,CAAC,MAAM,GAAG,sBAAsB;QAC/C,CAAC,CAAC,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,sBAAsB,CAAC,KAAK;QACrD,CAAC,CAAC,UAAU,CAAC;AACjB,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,KAAc;IAC3C,OAAO,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAChE,CAAC"}
@@ -0,0 +1,8 @@
1
+ import type { AnalyzeImageInput } from "./schema.js";
2
+ export type LoadedImage = {
3
+ base64: string;
4
+ mimeType: string;
5
+ };
6
+ export declare function loadImage(input: AnalyzeImageInput): Promise<LoadedImage>;
7
+ export declare function parseBase64Image(value: string, mimeType?: string): LoadedImage;
8
+ export declare function inferMimeType(filePath: string): string;
package/dist/image.js ADDED
@@ -0,0 +1,69 @@
1
+ import { extname } from "node:path";
2
+ import { readFile } from "node:fs/promises";
3
+ const MIME_BY_EXTENSION = {
4
+ ".png": "image/png",
5
+ ".jpg": "image/jpeg",
6
+ ".jpeg": "image/jpeg",
7
+ ".webp": "image/webp",
8
+ ".gif": "image/gif",
9
+ };
10
+ const DATA_URL_PATTERN = /^data:([^;,]+);base64,(.*)$/s;
11
+ const BASE64_PATTERN = /^[A-Za-z0-9+/]+={0,2}$/;
12
+ export async function loadImage(input) {
13
+ if (input.image_path) {
14
+ return loadImagePath(input.image_path, input.mime_type);
15
+ }
16
+ if (input.image_base64) {
17
+ return parseBase64Image(input.image_base64, input.mime_type);
18
+ }
19
+ throw new Error("Exactly one of image_path or image_base64 must be provided");
20
+ }
21
+ async function loadImagePath(imagePath, mimeType) {
22
+ if (/^https?:\/\//i.test(imagePath)) {
23
+ throw new Error("image_path must be a local file path, not an HTTP or HTTPS URL");
24
+ }
25
+ try {
26
+ const file = await readFile(imagePath);
27
+ return {
28
+ base64: file.toString("base64"),
29
+ mimeType: mimeType ?? inferMimeType(imagePath),
30
+ };
31
+ }
32
+ catch (error) {
33
+ const reason = error instanceof Error ? error.message : String(error);
34
+ throw new Error(`Unable to read local image file: ${reason}`);
35
+ }
36
+ }
37
+ export function parseBase64Image(value, mimeType) {
38
+ const trimmed = value.trim();
39
+ const dataUrlMatch = DATA_URL_PATTERN.exec(trimmed);
40
+ if (dataUrlMatch) {
41
+ const [, dataUrlMimeType, data] = dataUrlMatch;
42
+ const base64 = normalizeBase64(data);
43
+ return {
44
+ base64,
45
+ mimeType: mimeType ?? dataUrlMimeType,
46
+ };
47
+ }
48
+ return {
49
+ base64: normalizeBase64(trimmed),
50
+ mimeType: mimeType ?? "image/png",
51
+ };
52
+ }
53
+ export function inferMimeType(filePath) {
54
+ return MIME_BY_EXTENSION[extname(filePath).toLowerCase()] ?? "image/png";
55
+ }
56
+ function normalizeBase64(value) {
57
+ const compact = value.replace(/\s/g, "");
58
+ if (!compact || compact.length % 4 !== 0 || !BASE64_PATTERN.test(compact)) {
59
+ throw new Error("Invalid base64 image input");
60
+ }
61
+ try {
62
+ Buffer.from(compact, "base64");
63
+ return compact;
64
+ }
65
+ catch {
66
+ throw new Error("Invalid base64 image input");
67
+ }
68
+ }
69
+ //# sourceMappingURL=image.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image.js","sourceRoot":"","sources":["../src/image.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAS5C,MAAM,iBAAiB,GAA2B;IAChD,MAAM,EAAE,WAAW;IACnB,MAAM,EAAE,YAAY;IACpB,OAAO,EAAE,YAAY;IACrB,OAAO,EAAE,YAAY;IACrB,MAAM,EAAE,WAAW;CACpB,CAAC;AAEF,MAAM,gBAAgB,GAAG,8BAA8B,CAAC;AACxD,MAAM,cAAc,GAAG,wBAAwB,CAAC;AAEhD,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,KAAwB;IACtD,IAAI,KAAK,CAAC,UAAU,EAAE,CAAC;QACrB,OAAO,aAAa,CAAC,KAAK,CAAC,UAAU,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;IAC1D,CAAC;IAED,IAAI,KAAK,CAAC,YAAY,EAAE,CAAC;QACvB,OAAO,gBAAgB,CAAC,KAAK,CAAC,YAAY,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;IAC/D,CAAC;IAED,MAAM,IAAI,KAAK,CAAC,4DAA4D,CAAC,CAAC;AAChF,CAAC;AAED,KAAK,UAAU,aAAa,CAAC,SAAiB,EAAE,QAAiB;IAC/D,IAAI,eAAe,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC;QACpC,MAAM,IAAI,KAAK,CAAC,gEAAgE,CAAC,CAAC;IACpF,CAAC;IAED,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,SAAS,CAAC,CAAC;QACvC,OAAO;YACL,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC;YAC/B,QAAQ,EAAE,QAAQ,IAAI,aAAa,CAAC,SAAS,CAAC;SAC/C,CAAC;IACJ,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,MAAM,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACtE,MAAM,IAAI,KAAK,CAAC,oCAAoC,MAAM,EAAE,CAAC,CAAC;IAChE,CAAC;AACH,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,KAAa,EAAE,QAAiB;IAC/D,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,MAAM,YAAY,GAAG,gBAAgB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAEpD,IAAI,YAAY,EAAE,CAAC;QACjB,MAAM,CAAC,EAAE,eAAe,EAAE,IAAI,CAAC,GAAG,YAAY,CAAC;QAC/C,MAAM,MAAM,GAAG,eAAe,CAAC,IAAI,CAAC,CAAC;QACrC,OAAO;YACL,MAAM;YACN,QAAQ,EAAE,QAAQ,IAAI,eAAe;SACtC,CAAC;IACJ,CAAC;IAED,OAAO;QACL,MAAM,EAAE,eAAe,CAAC,OAAO,CAAC;QAChC,QAAQ,EAAE,QAAQ,IAAI,WAAW;KAClC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,QAAgB;IAC5C,OAAO,iBAAiB,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,WAAW,EAAE,CAAC,IAAI,WAAW,CAAC;AAC3E,CAAC;AAED,SAAS,eAAe,CAAC,KAAa;IACpC,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IAEzC,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QAC1E,MAAM,IAAI,KAAK,CAAC,4BAA4B,CAAC,CAAC;IAChD,CAAC;IAED,IAAI,CAAC;QACH,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;QAC/B,OAAO,OAAO,CAAC;IACjB,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CAAC,4BAA4B,CAAC,CAAC;IAChD,CAAC;AACH,CAAC"}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { loadConfig } from "./config.js";
5
+ import { asErrorMessage } from "./errors.js";
6
+ import { loadImage } from "./image.js";
7
+ import { getProvider } from "./providers/index.js";
8
+ import { DEFAULT_PROMPT, analyzeImageInputShape, parseAnalyzeImageInput } from "./schema.js";
9
+ import { DEFAULT_TIMEOUT_SECONDS, withTimeout } from "./timeout.js";
10
+ const server = new McpServer({
11
+ name: "visionmcp",
12
+ version: "0.1.0",
13
+ });
14
+ server.tool("readimg", `readimg is an image-reading tool backed by a specialized vision LLM. It accepts a base64-encoded image or an absolute local file path, and supports an optional custom prompt (a sensible default is used when omitted) and a configurable timeout (defaults to 120 seconds).
15
+ there is an json scheme example
16
+ <example>
17
+ {
18
+ "image_path": "/absolute/path/to/photo.png",
19
+ "prompt": "What objects are visible in this image?",
20
+ "timeout_seconds": 60
21
+ }
22
+ </example>`, analyzeImageInputShape, async (input) => {
23
+ try {
24
+ const parsedInput = parseAnalyzeImageInput(input);
25
+ const config = loadConfig();
26
+ const image = await loadImage(parsedInput);
27
+ const provider = getProvider(config.format);
28
+ const text = await withTimeout(parsedInput.timeout_seconds ?? DEFAULT_TIMEOUT_SECONDS, (signal) => provider.analyze(config, {
29
+ prompt: parsedInput.prompt ?? DEFAULT_PROMPT,
30
+ image,
31
+ signal,
32
+ }));
33
+ return {
34
+ content: [{ type: "text", text }],
35
+ };
36
+ }
37
+ catch (error) {
38
+ throw new Error(asErrorMessage(error));
39
+ }
40
+ });
41
+ const transport = new StdioServerTransport();
42
+ await server.connect(transport);
43
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AAEjF,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,sBAAsB,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAC7F,OAAO,EAAE,uBAAuB,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEpE,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;IAC3B,IAAI,EAAE,WAAW;IACjB,OAAO,EAAE,OAAO;CACjB,CAAC,CAAC;AAEH,MAAM,CAAC,IAAI,CACT,SAAS,EACT;;;;;;;;WAQS,EACT,sBAAsB,EACtB,KAAK,EAAE,KAAK,EAAE,EAAE;IACd,IAAI,CAAC;QACH,MAAM,WAAW,GAAG,sBAAsB,CAAC,KAAK,CAAC,CAAC;QAClD,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;QAC5B,MAAM,KAAK,GAAG,MAAM,SAAS,CAAC,WAAW,CAAC,CAAC;QAC3C,MAAM,QAAQ,GAAG,WAAW,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAC5C,MAAM,IAAI,GAAG,MAAM,WAAW,CAAC,WAAW,CAAC,eAAe,IAAI,uBAAuB,EAAE,CAAC,MAAM,EAAE,EAAE,CAChG,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE;YACvB,MAAM,EAAE,WAAW,CAAC,MAAM,IAAI,cAAc;YAC5C,KAAK;YACL,MAAM;SACP,CAAC,CACH,CAAC;QAEF,OAAO;YACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;SAClC,CAAC;IACJ,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC;IACzC,CAAC;AACH,CAAC,CACF,CAAC;AAEF,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;AAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { ProviderAdapter } from "./types.js";
2
+ export declare const anthropicProvider: ProviderAdapter;
3
+ export declare function extractAnthropicText(json: unknown): string;
@@ -0,0 +1,51 @@
1
+ import { parseJsonResponse } from "../errors.js";
2
+ import { joinBaseUrl } from "./base-url.js";
3
+ export const anthropicProvider = {
4
+ async analyze(config, request) {
5
+ const response = await fetch(joinBaseUrl(config.baseUrl, "/v1/messages"), {
6
+ method: "POST",
7
+ headers: {
8
+ "x-api-key": config.apiKey,
9
+ "anthropic-version": "2023-06-01",
10
+ "Content-Type": "application/json",
11
+ },
12
+ signal: request.signal,
13
+ body: JSON.stringify({
14
+ model: config.model,
15
+ temperature: 0.7,
16
+ messages: [
17
+ {
18
+ role: "user",
19
+ content: [
20
+ {
21
+ type: "image",
22
+ source: {
23
+ type: "base64",
24
+ media_type: request.image.mimeType,
25
+ data: request.image.base64,
26
+ },
27
+ },
28
+ { type: "text", text: request.prompt },
29
+ ],
30
+ },
31
+ ],
32
+ }),
33
+ });
34
+ return extractAnthropicText(await parseJsonResponse(response));
35
+ },
36
+ };
37
+ export function extractAnthropicText(json) {
38
+ if (!isRecord(json) || !Array.isArray(json.content)) {
39
+ throw new Error("Anthropic response did not contain extractable text");
40
+ }
41
+ for (const block of json.content) {
42
+ if (isRecord(block) && block.type === "text" && typeof block.text === "string" && block.text.trim()) {
43
+ return block.text;
44
+ }
45
+ }
46
+ throw new Error("Anthropic response did not contain extractable text");
47
+ }
48
+ function isRecord(value) {
49
+ return typeof value === "object" && value !== null;
50
+ }
51
+ //# sourceMappingURL=anthropic.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"anthropic.js","sourceRoot":"","sources":["../../src/providers/anthropic.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAG5C,MAAM,CAAC,MAAM,iBAAiB,GAAoB;IAChD,KAAK,CAAC,OAAO,CAAC,MAAM,EAAE,OAAO;QAC3B,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,EAAE,cAAc,CAAC,EAAE;YACxE,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACP,WAAW,EAAE,MAAM,CAAC,MAAM;gBAC1B,mBAAmB,EAAE,YAAY;gBACjC,cAAc,EAAE,kBAAkB;aACnC;YACD,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;gBACnB,KAAK,EAAE,MAAM,CAAC,KAAK;gBACnB,WAAW,EAAE,GAAG;gBAChB,QAAQ,EAAE;oBACR;wBACE,IAAI,EAAE,MAAM;wBACZ,OAAO,EAAE;4BACP;gCACE,IAAI,EAAE,OAAO;gCACb,MAAM,EAAE;oCACN,IAAI,EAAE,QAAQ;oCACd,UAAU,EAAE,OAAO,CAAC,KAAK,CAAC,QAAQ;oCAClC,IAAI,EAAE,OAAO,CAAC,KAAK,CAAC,MAAM;iCAC3B;6BACF;4BACD,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,CAAC,MAAM,EAAE;yBACvC;qBACF;iBACF;aACF,CAAC;SACH,CAAC,CAAC;QAEH,OAAO,oBAAoB,CAAC,MAAM,iBAAiB,CAAC,QAAQ,CAAC,CAAC,CAAC;IACjE,CAAC;CACF,CAAC;AAEF,MAAM,UAAU,oBAAoB,CAAC,IAAa;IAChD,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QACpD,MAAM,IAAI,KAAK,CAAC,qDAAqD,CAAC,CAAC;IACzE,CAAC;IAED,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;QACjC,IAAI,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,IAAI,OAAO,KAAK,CAAC,IAAI,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC;YACpG,OAAO,KAAK,CAAC,IAAI,CAAC;QACpB,CAAC;IACH,CAAC;IAED,MAAM,IAAI,KAAK,CAAC,qDAAqD,CAAC,CAAC;AACzE,CAAC;AAED,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,CAAC;AACrD,CAAC"}
@@ -0,0 +1 @@
1
+ export declare function joinBaseUrl(baseUrl: string, path: string): string;
@@ -0,0 +1,4 @@
1
+ export function joinBaseUrl(baseUrl, path) {
2
+ return `${baseUrl.replace(/\/+$/, "")}${path.startsWith("/") ? path : `/${path}`}`;
3
+ }
4
+ //# sourceMappingURL=base-url.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"base-url.js","sourceRoot":"","sources":["../../src/providers/base-url.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,WAAW,CAAC,OAAe,EAAE,IAAY;IACvD,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,EAAE,CAAC;AACrF,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { ProviderAdapter } from "./types.js";
2
+ export declare const geminiProvider: ProviderAdapter;
3
+ export declare function extractGeminiText(json: unknown): string;
@@ -0,0 +1,53 @@
1
+ import { parseJsonResponse } from "../errors.js";
2
+ import { joinBaseUrl } from "./base-url.js";
3
+ export const geminiProvider = {
4
+ async analyze(config, request) {
5
+ const response = await fetch(joinBaseUrl(config.baseUrl, `/v1beta/models/${encodeURIComponent(config.model)}:generateContent`), {
6
+ method: "POST",
7
+ headers: {
8
+ "x-goog-api-key": config.apiKey,
9
+ "Content-Type": "application/json",
10
+ },
11
+ signal: request.signal,
12
+ body: JSON.stringify({
13
+ contents: [
14
+ {
15
+ parts: [
16
+ { text: request.prompt },
17
+ {
18
+ inline_data: {
19
+ mime_type: request.image.mimeType,
20
+ data: request.image.base64,
21
+ },
22
+ },
23
+ ],
24
+ },
25
+ ],
26
+ generationConfig: {
27
+ temperature: 0.7,
28
+ },
29
+ }),
30
+ });
31
+ return extractGeminiText(await parseJsonResponse(response));
32
+ },
33
+ };
34
+ export function extractGeminiText(json) {
35
+ if (!isRecord(json) || !Array.isArray(json.candidates)) {
36
+ throw new Error("Gemini response did not contain extractable text");
37
+ }
38
+ for (const candidate of json.candidates) {
39
+ if (!isRecord(candidate) || !isRecord(candidate.content) || !Array.isArray(candidate.content.parts)) {
40
+ continue;
41
+ }
42
+ for (const part of candidate.content.parts) {
43
+ if (isRecord(part) && typeof part.text === "string" && part.text.trim()) {
44
+ return part.text;
45
+ }
46
+ }
47
+ }
48
+ throw new Error("Gemini response did not contain extractable text");
49
+ }
50
+ function isRecord(value) {
51
+ return typeof value === "object" && value !== null;
52
+ }
53
+ //# sourceMappingURL=gemini.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gemini.js","sourceRoot":"","sources":["../../src/providers/gemini.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAG5C,MAAM,CAAC,MAAM,cAAc,GAAoB;IAC7C,KAAK,CAAC,OAAO,CAAC,MAAM,EAAE,OAAO;QAC3B,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,EAAE,kBAAkB,kBAAkB,CAAC,MAAM,CAAC,KAAK,CAAC,kBAAkB,CAAC,EAAE;YAC9H,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACP,gBAAgB,EAAE,MAAM,CAAC,MAAM;gBAC/B,cAAc,EAAE,kBAAkB;aACnC;YACD,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;gBACnB,QAAQ,EAAE;oBACR;wBACE,KAAK,EAAE;4BACL,EAAE,IAAI,EAAE,OAAO,CAAC,MAAM,EAAE;4BACxB;gCACE,WAAW,EAAE;oCACX,SAAS,EAAE,OAAO,CAAC,KAAK,CAAC,QAAQ;oCACjC,IAAI,EAAE,OAAO,CAAC,KAAK,CAAC,MAAM;iCAC3B;6BACF;yBACF;qBACF;iBACF;gBACD,gBAAgB,EAAE;oBAChB,WAAW,EAAE,GAAG;iBACjB;aACF,CAAC;SACH,CAAC,CAAC;QAEH,OAAO,iBAAiB,CAAC,MAAM,iBAAiB,CAAC,QAAQ,CAAC,CAAC,CAAC;IAC9D,CAAC;CACF,CAAC;AAEF,MAAM,UAAU,iBAAiB,CAAC,IAAa;IAC7C,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;QACvD,MAAM,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAC;IACtE,CAAC;IAED,KAAK,MAAM,SAAS,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;QACxC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YACpG,SAAS;QACX,CAAC;QAED,KAAK,MAAM,IAAI,IAAI,SAAS,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;YAC3C,IAAI,QAAQ,CAAC,IAAI,CAAC,IAAI,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC;gBACxE,OAAO,IAAI,CAAC,IAAI,CAAC;YACnB,CAAC;QACH,CAAC;IACH,CAAC;IAED,MAAM,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAC;AACtE,CAAC;AAED,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,CAAC;AACrD,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { ProviderFormat } from "../config.js";
2
+ import type { ProviderAdapter } from "./types.js";
3
+ export declare function getProvider(format: ProviderFormat): ProviderAdapter;
4
+ export type { AnalyzeRequest, ProviderAdapter } from "./types.js";
@@ -0,0 +1,12 @@
1
+ import { anthropicProvider } from "./anthropic.js";
2
+ import { geminiProvider } from "./gemini.js";
3
+ import { openaiProvider } from "./openai.js";
4
+ const PROVIDERS = {
5
+ openai: openaiProvider,
6
+ anthropic: anthropicProvider,
7
+ gemini: geminiProvider,
8
+ };
9
+ export function getProvider(format) {
10
+ return PROVIDERS[format];
11
+ }
12
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/providers/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAG7C,MAAM,SAAS,GAA4C;IACzD,MAAM,EAAE,cAAc;IACtB,SAAS,EAAE,iBAAiB;IAC5B,MAAM,EAAE,cAAc;CACvB,CAAC;AAEF,MAAM,UAAU,WAAW,CAAC,MAAsB;IAChD,OAAO,SAAS,CAAC,MAAM,CAAC,CAAC;AAC3B,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { ProviderAdapter } from "./types.js";
2
+ export declare const openaiProvider: ProviderAdapter;
3
+ export declare function extractOpenAiText(json: unknown): string;
@@ -0,0 +1,62 @@
1
+ import { parseJsonResponse } from "../errors.js";
2
+ import { joinBaseUrl } from "./base-url.js";
3
+ export const openaiProvider = {
4
+ async analyze(config, request) {
5
+ const response = await fetch(joinBaseUrl(config.baseUrl, "/v1/chat/completions"), {
6
+ method: "POST",
7
+ headers: {
8
+ Authorization: `Bearer ${config.apiKey}`,
9
+ "Content-Type": "application/json",
10
+ },
11
+ signal: request.signal,
12
+ body: JSON.stringify({
13
+ model: config.model,
14
+ temperature: 0.7,
15
+ messages: [
16
+ {
17
+ role: "user",
18
+ content: [
19
+ { type: "text", text: request.prompt },
20
+ {
21
+ type: "image_url",
22
+ image_url: {
23
+ url: `data:${request.image.mimeType};base64,${request.image.base64}`,
24
+ },
25
+ },
26
+ ],
27
+ },
28
+ ],
29
+ }),
30
+ });
31
+ return extractOpenAiText(await parseJsonResponse(response));
32
+ },
33
+ };
34
+ export function extractOpenAiText(json) {
35
+ if (!isRecord(json)) {
36
+ throw new Error("OpenAI response did not contain extractable text");
37
+ }
38
+ const choices = json.choices;
39
+ if (!Array.isArray(choices) || choices.length === 0 || !isRecord(choices[0])) {
40
+ throw new Error("OpenAI response did not contain extractable text");
41
+ }
42
+ const message = choices[0].message;
43
+ if (!isRecord(message)) {
44
+ throw new Error("OpenAI response did not contain extractable text");
45
+ }
46
+ const content = message.content;
47
+ if (typeof content === "string" && content.trim()) {
48
+ return content;
49
+ }
50
+ if (Array.isArray(content)) {
51
+ for (const block of content) {
52
+ if (isRecord(block) && typeof block.text === "string" && block.text.trim()) {
53
+ return block.text;
54
+ }
55
+ }
56
+ }
57
+ throw new Error("OpenAI response did not contain extractable text");
58
+ }
59
+ function isRecord(value) {
60
+ return typeof value === "object" && value !== null;
61
+ }
62
+ //# sourceMappingURL=openai.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openai.js","sourceRoot":"","sources":["../../src/providers/openai.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAG5C,MAAM,CAAC,MAAM,cAAc,GAAoB;IAC7C,KAAK,CAAC,OAAO,CAAC,MAAM,EAAE,OAAO;QAC3B,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,EAAE,sBAAsB,CAAC,EAAE;YAChF,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACP,aAAa,EAAE,UAAU,MAAM,CAAC,MAAM,EAAE;gBACxC,cAAc,EAAE,kBAAkB;aACnC;YACD,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;gBACnB,KAAK,EAAE,MAAM,CAAC,KAAK;gBACnB,WAAW,EAAE,GAAG;gBAChB,QAAQ,EAAE;oBACR;wBACE,IAAI,EAAE,MAAM;wBACZ,OAAO,EAAE;4BACP,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,CAAC,MAAM,EAAE;4BACtC;gCACE,IAAI,EAAE,WAAW;gCACjB,SAAS,EAAE;oCACT,GAAG,EAAE,QAAQ,OAAO,CAAC,KAAK,CAAC,QAAQ,WAAW,OAAO,CAAC,KAAK,CAAC,MAAM,EAAE;iCACrE;6BACF;yBACF;qBACF;iBACF;aACF,CAAC;SACH,CAAC,CAAC;QAEH,OAAO,iBAAiB,CAAC,MAAM,iBAAiB,CAAC,QAAQ,CAAC,CAAC,CAAC;IAC9D,CAAC;CACF,CAAC;AAEF,MAAM,UAAU,iBAAiB,CAAC,IAAa;IAC7C,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QACpB,MAAM,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAC;IACtE,CAAC;IAED,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC;IAC7B,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7E,MAAM,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAC;IACtE,CAAC;IAED,MAAM,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;IACnC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;QACvB,MAAM,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAC;IACtE,CAAC;IAED,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IAChC,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC;QAClD,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QAC3B,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;YAC5B,IAAI,QAAQ,CAAC,KAAK,CAAC,IAAI,OAAO,KAAK,CAAC,IAAI,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC;gBAC3E,OAAO,KAAK,CAAC,IAAI,CAAC;YACpB,CAAC;QACH,CAAC;IACH,CAAC;IAED,MAAM,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAC;AACtE,CAAC;AAED,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,CAAC;AACrD,CAAC"}
@@ -0,0 +1,10 @@
1
+ import type { ServerConfig } from "../config.js";
2
+ import type { LoadedImage } from "../image.js";
3
+ export type AnalyzeRequest = {
4
+ prompt: string;
5
+ image: LoadedImage;
6
+ signal?: AbortSignal;
7
+ };
8
+ export type ProviderAdapter = {
9
+ analyze(config: ServerConfig, request: AnalyzeRequest): Promise<string>;
10
+ };
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/providers/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,42 @@
1
+ import { z } from "zod";
2
+ export declare const DEFAULT_PROMPT = "\u8BF7\u8BE6\u7EC6\u63CF\u8FF0\u8FD9\u5F20\u56FE\u7247";
3
+ export declare const analyzeImageInputShape: {
4
+ image_path: z.ZodOptional<z.ZodString>;
5
+ image_base64: z.ZodOptional<z.ZodString>;
6
+ mime_type: z.ZodOptional<z.ZodString>;
7
+ prompt: z.ZodOptional<z.ZodString>;
8
+ timeout_seconds: z.ZodOptional<z.ZodNumber>;
9
+ };
10
+ export declare const analyzeImageInputSchema: z.ZodEffects<z.ZodObject<{
11
+ image_path: z.ZodOptional<z.ZodString>;
12
+ image_base64: z.ZodOptional<z.ZodString>;
13
+ mime_type: z.ZodOptional<z.ZodString>;
14
+ prompt: z.ZodOptional<z.ZodString>;
15
+ timeout_seconds: z.ZodOptional<z.ZodNumber>;
16
+ }, "strict", z.ZodTypeAny, {
17
+ image_path?: string | undefined;
18
+ image_base64?: string | undefined;
19
+ mime_type?: string | undefined;
20
+ prompt?: string | undefined;
21
+ timeout_seconds?: number | undefined;
22
+ }, {
23
+ image_path?: string | undefined;
24
+ image_base64?: string | undefined;
25
+ mime_type?: string | undefined;
26
+ prompt?: string | undefined;
27
+ timeout_seconds?: number | undefined;
28
+ }>, {
29
+ image_path?: string | undefined;
30
+ image_base64?: string | undefined;
31
+ mime_type?: string | undefined;
32
+ prompt?: string | undefined;
33
+ timeout_seconds?: number | undefined;
34
+ }, {
35
+ image_path?: string | undefined;
36
+ image_base64?: string | undefined;
37
+ mime_type?: string | undefined;
38
+ prompt?: string | undefined;
39
+ timeout_seconds?: number | undefined;
40
+ }>;
41
+ export type AnalyzeImageInput = z.infer<typeof analyzeImageInputSchema>;
42
+ export declare function parseAnalyzeImageInput(input: unknown): AnalyzeImageInput;
package/dist/schema.js ADDED
@@ -0,0 +1,39 @@
1
+ import { z } from "zod";
2
+ export const DEFAULT_PROMPT = "请详细描述这张图片";
3
+ export const analyzeImageInputShape = {
4
+ image_path: z.string().optional(),
5
+ image_base64: z.string().optional(),
6
+ mime_type: z.string().optional(),
7
+ prompt: z.string().optional(),
8
+ timeout_seconds: z.number().positive().optional(),
9
+ };
10
+ export const analyzeImageInputSchema = z
11
+ .object(analyzeImageInputShape)
12
+ .strict()
13
+ .superRefine((value, ctx) => {
14
+ const hasPath = typeof value.image_path === "string" && value.image_path.length > 0;
15
+ const hasBase64 = typeof value.image_base64 === "string" && value.image_base64.length > 0;
16
+ if (hasPath === hasBase64) {
17
+ ctx.addIssue({
18
+ code: z.ZodIssueCode.custom,
19
+ message: "Exactly one of image_path or image_base64 must be provided",
20
+ path: ["image_path"],
21
+ });
22
+ }
23
+ if (hasPath && /^https?:\/\//i.test(value.image_path ?? "")) {
24
+ ctx.addIssue({
25
+ code: z.ZodIssueCode.custom,
26
+ message: "image_path must be a local file path, not an HTTP or HTTPS URL",
27
+ path: ["image_path"],
28
+ });
29
+ }
30
+ });
31
+ export function parseAnalyzeImageInput(input) {
32
+ const parsed = analyzeImageInputSchema.safeParse(input);
33
+ if (!parsed.success) {
34
+ const message = parsed.error.issues.map((issue) => issue.message).join("; ");
35
+ throw new Error(message);
36
+ }
37
+ return parsed.data;
38
+ }
39
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,MAAM,CAAC,MAAM,cAAc,GAAG,WAAW,CAAC;AAE1C,MAAM,CAAC,MAAM,sBAAsB,GAAG;IACpC,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACjC,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACnC,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAChC,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC7B,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;CAClD,CAAC;AAEF,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC;KACrC,MAAM,CAAC,sBAAsB,CAAC;KAC9B,MAAM,EAAE;KACR,WAAW,CAAC,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;IAC1B,MAAM,OAAO,GAAG,OAAO,KAAK,CAAC,UAAU,KAAK,QAAQ,IAAI,KAAK,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC;IACpF,MAAM,SAAS,GAAG,OAAO,KAAK,CAAC,YAAY,KAAK,QAAQ,IAAI,KAAK,CAAC,YAAY,CAAC,MAAM,GAAG,CAAC,CAAC;IAE1F,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QAC1B,GAAG,CAAC,QAAQ,CAAC;YACX,IAAI,EAAE,CAAC,CAAC,YAAY,CAAC,MAAM;YAC3B,OAAO,EAAE,4DAA4D;YACrE,IAAI,EAAE,CAAC,YAAY,CAAC;SACrB,CAAC,CAAC;IACL,CAAC;IAED,IAAI,OAAO,IAAI,eAAe,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU,IAAI,EAAE,CAAC,EAAE,CAAC;QAC5D,GAAG,CAAC,QAAQ,CAAC;YACX,IAAI,EAAE,CAAC,CAAC,YAAY,CAAC,MAAM;YAC3B,OAAO,EAAE,gEAAgE;YACzE,IAAI,EAAE,CAAC,YAAY,CAAC;SACrB,CAAC,CAAC;IACL,CAAC;AACH,CAAC,CAAC,CAAC;AAIL,MAAM,UAAU,sBAAsB,CAAC,KAAc;IACnD,MAAM,MAAM,GAAG,uBAAuB,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IAExD,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC7E,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC;IAC3B,CAAC;IAED,OAAO,MAAM,CAAC,IAAI,CAAC;AACrB,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare const DEFAULT_TIMEOUT_SECONDS = 120;
2
+ export declare function withTimeout<T>(timeoutSeconds: number, operation: (signal: AbortSignal) => Promise<T>): Promise<T>;
@@ -0,0 +1,21 @@
1
+ export const DEFAULT_TIMEOUT_SECONDS = 120;
2
+ export async function withTimeout(timeoutSeconds, operation) {
3
+ const controller = new AbortController();
4
+ const timeout = setTimeout(() => controller.abort(), timeoutSeconds * 1000);
5
+ try {
6
+ return await operation(controller.signal);
7
+ }
8
+ catch (error) {
9
+ if (controller.signal.aborted || isAbortError(error)) {
10
+ throw new Error(`timeout after ${timeoutSeconds} seconds`);
11
+ }
12
+ throw error;
13
+ }
14
+ finally {
15
+ clearTimeout(timeout);
16
+ }
17
+ }
18
+ function isAbortError(error) {
19
+ return error instanceof Error && error.name === "AbortError";
20
+ }
21
+ //# sourceMappingURL=timeout.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timeout.js","sourceRoot":"","sources":["../src/timeout.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,uBAAuB,GAAG,GAAG,CAAC;AAE3C,MAAM,CAAC,KAAK,UAAU,WAAW,CAAI,cAAsB,EAAE,SAA8C;IACzG,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;IACzC,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,cAAc,GAAG,IAAI,CAAC,CAAC;IAE5E,IAAI,CAAC;QACH,OAAO,MAAM,SAAS,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;IAC5C,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,UAAU,CAAC,MAAM,CAAC,OAAO,IAAI,YAAY,CAAC,KAAK,CAAC,EAAE,CAAC;YACrD,MAAM,IAAI,KAAK,CAAC,iBAAiB,cAAc,UAAU,CAAC,CAAC;QAC7D,CAAC;QAED,MAAM,KAAK,CAAC;IACd,CAAC;YAAS,CAAC;QACT,YAAY,CAAC,OAAO,CAAC,CAAC;IACxB,CAAC;AACH,CAAC;AAED,SAAS,YAAY,CAAC,KAAc;IAClC,OAAO,KAAK,YAAY,KAAK,IAAI,KAAK,CAAC,IAAI,KAAK,YAAY,CAAC;AAC/D,CAAC"}
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@xyzensun/visionmcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP stdio server for image understanding through configurable multimodal providers (OpenAI / Anthropic / Gemini formats).",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "bin": {
8
+ "visionmcp": "./dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "README.md"
13
+ ],
14
+ "keywords": [
15
+ "mcp",
16
+ "model-context-protocol",
17
+ "vision",
18
+ "image-understanding",
19
+ "multimodal",
20
+ "openai",
21
+ "anthropic",
22
+ "gemini"
23
+ ],
24
+ "scripts": {
25
+ "build": "tsc",
26
+ "test": "vitest run",
27
+ "prepublishOnly": "npm run build && npm test"
28
+ },
29
+ "dependencies": {
30
+ "@modelcontextprotocol/sdk": "^1.13.0",
31
+ "zod": "^3.25.0"
32
+ },
33
+ "devDependencies": {
34
+ "@types/node": "^22.15.0",
35
+ "typescript": "^5.8.0",
36
+ "vitest": "^3.1.0"
37
+ },
38
+ "engines": {
39
+ "node": ">=20"
40
+ },
41
+ "publishConfig": {
42
+ "access": "public"
43
+ }
44
+ }