@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 +120 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.js +28 -0
- package/dist/config.js.map +1 -0
- package/dist/errors.d.ts +6 -0
- package/dist/errors.js +32 -0
- package/dist/errors.js.map +1 -0
- package/dist/image.d.ts +8 -0
- package/dist/image.js +69 -0
- package/dist/image.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +43 -0
- package/dist/index.js.map +1 -0
- package/dist/providers/anthropic.d.ts +3 -0
- package/dist/providers/anthropic.js +51 -0
- package/dist/providers/anthropic.js.map +1 -0
- package/dist/providers/base-url.d.ts +1 -0
- package/dist/providers/base-url.js +4 -0
- package/dist/providers/base-url.js.map +1 -0
- package/dist/providers/gemini.d.ts +3 -0
- package/dist/providers/gemini.js +53 -0
- package/dist/providers/gemini.js.map +1 -0
- package/dist/providers/index.d.ts +4 -0
- package/dist/providers/index.js +12 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/openai.d.ts +3 -0
- package/dist/providers/openai.js +62 -0
- package/dist/providers/openai.js.map +1 -0
- package/dist/providers/types.d.ts +10 -0
- package/dist/providers/types.js +2 -0
- package/dist/providers/types.js.map +1 -0
- package/dist/schema.d.ts +42 -0
- package/dist/schema.js +39 -0
- package/dist/schema.js.map +1 -0
- package/dist/timeout.d.ts +2 -0
- package/dist/timeout.js +21 -0
- package/dist/timeout.js.map +1 -0
- package/package.json +44 -0
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
|
package/dist/config.d.ts
ADDED
|
@@ -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"}
|
package/dist/errors.d.ts
ADDED
|
@@ -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"}
|
package/dist/image.d.ts
ADDED
|
@@ -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"}
|
package/dist/index.d.ts
ADDED
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,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 @@
|
|
|
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,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,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,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 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/providers/types.ts"],"names":[],"mappings":""}
|
package/dist/schema.d.ts
ADDED
|
@@ -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"}
|
package/dist/timeout.js
ADDED
|
@@ -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
|
+
}
|