Files
vision-tool/mcp-server/server.js
2026-07-10 18:46:10 +08:00

168 lines
4.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { readFileSync } from "node:fs";
import { basename } from "node:path";
// ============================================================================
// Configuration (mirrors config.go defaults)
// ============================================================================
const DEFAULT_API_KEY = "sk-68FL9xXRrJhxcY5TCGxoaFlQ94oCBioIJhyGfeDMCkCvA0SV";
const DEFAULT_MODEL = "agnes-2.0-flash";
const DEFAULT_BASE_URL = "https://apihub.agnes-ai.com/v1/chat/completions";
const API_KEY = process.env.AGNES_API_KEY || DEFAULT_API_KEY;
const MODEL = process.env.VISION_MODEL || DEFAULT_MODEL;
const BASE_URL = process.env.VISION_BASE_URL || DEFAULT_BASE_URL;
// ============================================================================
// MIME type detection
// ============================================================================
function detectMimeType(path) {
const ext = path.split(".").pop().toLowerCase();
const map = {
png: "image/png",
jpg: "image/jpeg",
jpeg: "image/jpeg",
gif: "image/gif",
webp: "image/webp",
bmp: "image/bmp",
tiff: "image/tiff",
tif: "image/tiff",
};
return map[ext] || "image/png";
}
// ============================================================================
// Vision API client
// ============================================================================
async function analyzeImage(imagePath, prompt) {
const imgData = readFileSync(imagePath);
const mimeType = detectMimeType(imagePath);
const base64Img = Buffer.from(imgData).toString("base64");
const dataURL = `data:${mimeType};base64,${base64Img}`;
if (!prompt) {
prompt = "请详细描述这张图片的内容。如果图片中有文字,请完整识别出来。请用中文回答。";
}
const body = {
model: MODEL,
messages: [
{
role: "user",
content: [
{ type: "image_url", image_url: { url: dataURL } },
{ type: "text", text: prompt },
],
},
],
chat_template_kwargs: { enable_thinking: true },
};
const resp = await fetch(BASE_URL, {
method: "POST",
headers: {
"Authorization": `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
signal: AbortSignal.timeout(120_000),
});
if (!resp.ok) {
const text = await resp.text();
throw new Error(`API error (HTTP ${resp.status}): ${text}`);
}
const data = await resp.json();
if (data.error) {
throw new Error(`API error: [${data.error.code}] ${data.error.message}`);
}
const content = data.choices?.[0]?.message?.content;
if (!content) {
throw new Error("No content in API response");
}
return content;
}
// ============================================================================
// MCP Server
// ============================================================================
const server = new Server(
{
name: "vision-tool",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
// Register tool list
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: "analyze_image",
description:
"分析图片内容。使用 Agnes AI 视觉模型识别图片中的对象、场景、文字等。" +
"支持 PNG、JPG、GIF、WebP、BMP、TIFF 格式。可以用自定义 prompt 指定分析重点。",
inputSchema: {
type: "object",
properties: {
image_path: {
type: "string",
description: "图片文件的绝对路径(例如 C:\\Users\\xxx\\photo.png",
},
prompt: {
type: "string",
description:
"自定义分析提示词。默认为详细描述图片内容并识别所有文字。例如:" +
'"这张图里有什么文字?" 或 "描述图片中的UI界面布局"。',
},
},
required: ["image_path"],
},
},
],
}));
// Register tool handler
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
if (name !== "analyze_image") {
throw new Error(`Unknown tool: ${name}`);
}
const imagePath = args.image_path;
const prompt = args.prompt || "";
try {
const result = await analyzeImage(imagePath, prompt);
return {
content: [{ type: "text", text: result }],
};
} catch (err) {
return {
content: [{ type: "text", text: `Error: ${err.message}` }],
isError: true,
};
}
});
// Start
const transport = new StdioServerTransport();
await server.connect(transport);