luma-mcp 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Jochen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,328 @@
1
+ # Luma MCP
2
+
3
+ 基于智谱 GLM-4.5V 的视觉理解 MCP 服务器,为不支持图片理解的 AI 助手提供视觉能力。
4
+
5
+ [English](./docs/README_EN.md) | 中文
6
+
7
+ ## 特性
8
+
9
+ - **简单设计**: 单一 `analyze_image` 工具处理所有图片分析任务
10
+ - **智能理解**: 自动识别代码、UI、错误等不同场景
11
+ - **全面支持**: 代码截图、界面设计、错误诊断、通用图片
12
+ - **标准 MCP 协议**: 无缝集成 Claude Desktop、Cline 等 MCP 客户端
13
+ - **GLM-4.5V 驱动**: 中文理解优秀,API 性价比高
14
+ - **URL 支持**: 支持本地文件和远程图片 URL
15
+ - **重试机制**: 内置指数退避重试,提高可靠性
16
+ - **思考模式**: 默认启用深度分析
17
+
18
+ ## 快速开始
19
+
20
+ ### 前置要求
21
+
22
+ - Node.js >= 18.0.0
23
+ - 智谱 AI API Key ([获取地址](https://open.bigmodel.cn/))
24
+
25
+ ### 安装
26
+
27
+ #### 方式 1: 本地开发(推荐用于测试)
28
+
29
+ ```bash
30
+ git clone https://github.com/JochenYang/luma-mcp.git
31
+ cd luma-mcp
32
+ npm install
33
+ npm run build
34
+ ```
35
+
36
+ #### 方式 2: 使用 npx(需要先发布到 npm)
37
+
38
+ ```bash
39
+ npx luma-mcp
40
+ ```
41
+
42
+ ### 配置
43
+
44
+ #### Claude Desktop
45
+
46
+ **Windows 配置文件位置**: `%APPDATA%\Claude\claude_desktop_config.json`
47
+
48
+ **macOS 配置文件位置**: `~/Library/Application Support/Claude/claude_desktop_config.json`
49
+
50
+ **使用 npx(推荐)**:
51
+
52
+ ```json
53
+ {
54
+ "mcpServers": {
55
+ "luma": {
56
+ "command": "npx",
57
+ "args": ["-y", "luma-mcp"],
58
+ "env": {
59
+ "ZHIPU_API_KEY": "your-zhipu-api-key"
60
+ }
61
+ }
62
+ }
63
+ }
64
+ ```
65
+
66
+ **本地开发**:
67
+
68
+ ```json
69
+ {
70
+ "mcpServers": {
71
+ "luma": {
72
+ "command": "node",
73
+ "args": ["D:\\codes\\Luma_mcp\\build\\index.js"],
74
+ "env": {
75
+ "ZHIPU_API_KEY": "your-zhipu-api-key"
76
+ }
77
+ }
78
+ }
79
+ }
80
+ ```
81
+
82
+ 配置完成后重启 Claude Desktop。
83
+
84
+ #### Cline (VSCode)
85
+
86
+ **使用 npx(推荐)**:
87
+
88
+ 在项目根目录或 `.vscode/` 目录下创建 `mcp.json`:
89
+
90
+ ```json
91
+ {
92
+ "mcpServers": {
93
+ "luma": {
94
+ "command": "npx",
95
+ "args": ["-y", "luma-mcp"],
96
+ "env": {
97
+ "ZHIPU_API_KEY": "your-zhipu-api-key"
98
+ }
99
+ }
100
+ }
101
+ }
102
+ ```
103
+
104
+ **本地开发**:
105
+
106
+ ```json
107
+ {
108
+ "mcpServers": {
109
+ "luma": {
110
+ "command": "node",
111
+ "args": ["D:\\codes\\Luma_mcp\\build\\index.js"],
112
+ "env": {
113
+ "ZHIPU_API_KEY": "your-zhipu-api-key"
114
+ }
115
+ }
116
+ }
117
+ }
118
+ ```
119
+
120
+ #### Claude Code (命令行)
121
+
122
+ ```bash
123
+ claude mcp add -s user luma-mcp --env ZHIPU_API_KEY=your-api-key -- npx -y luma-mcp
124
+ ```
125
+
126
+ #### 其他工具
127
+
128
+ 更多 MCP 客户端配置方法请参考[智谱官方文档](https://docs.bigmodel.cn/cn/coding-plan/mcp/vision-mcp-server#claude-code)
129
+
130
+ ## 使用方法
131
+
132
+ ### 重要提示
133
+
134
+ **MCP 工具调用机制**:
135
+ - MCP 工具需要 AI 模型**主动调用**才会执行
136
+ - 如果使用的 AI 模型本身支持视觉(如 Claude 4.5 Sonnet),它会优先使用自己的视觉能力
137
+ - Luma MCP 主要服务于**不支持视觉的模型**(如 GPT-4、Claude Opus 等文本模型)
138
+
139
+ **如何确保工具被调用**:
140
+ 1. 明确提及工具:`使用 Luma 分析这张图片`
141
+ 2. 提供图片路径:`请分析 ./screenshot.png 中的代码错误`
142
+ 3. 使用工具名称:`用 analyze_image 工具查看这张图片`
143
+
144
+ **注意**: 直接在聊天框粘贴图片,非视觉模型不会自动调用 Luma,需要明确指示。
145
+
146
+ ### 在 Claude Desktop 中使用
147
+
148
+ 配置完成后,在 Claude 对话中可以这样使用:
149
+
150
+ **推荐用法(明确指示)**:
151
+ ```
152
+ 用户: 使用 Luma 分析 ./code-error.png,这段代码为什么报错?
153
+ Claude: [调用 Luma 分析图片,返回详细分析]
154
+ ```
155
+
156
+ **或提供图片路径**:
157
+ ```
158
+ 用户: 请分析 https://example.com/screenshot.jpg 中的界面问题
159
+ Claude: [自动调用 analyze_image 工具]
160
+ ```
161
+
162
+ ### 本地测试
163
+
164
+ 不需要 MCP 客户端即可测试:
165
+
166
+ ```bash
167
+ # 设置 API Key
168
+ export ZHIPU_API_KEY="your-api-key" # macOS/Linux
169
+ $env:ZHIPU_API_KEY="your-api-key" # Windows PowerShell
170
+
171
+ # 测试本地图片
172
+ npm run test:local ./test.png
173
+
174
+ # 测试并提问
175
+ npm run test:local ./code-error.png "这段代码有什么问题?"
176
+
177
+ # 测试远程URL
178
+ npm run test:local https://example.com/image.jpg
179
+ ```
180
+
181
+ ## 工具说明
182
+
183
+ ### analyze_image
184
+
185
+ 分析图片内容的通用工具。
186
+
187
+ **参数**:
188
+
189
+ - `image_source` (必需): 图片路径或 URL
190
+ - 支持格式: JPG, PNG, WebP, GIF
191
+ - 本地文件: 绝对路径或相对路径
192
+ - 远程图片: https:// 开头的 URL
193
+ - `question` (可选): 关于图片的问题或分析指令
194
+
195
+ **示例**:
196
+
197
+ ```typescript
198
+ // 通用分析
199
+ analyze_image({
200
+ image_source: "./screenshot.png"
201
+ })
202
+
203
+ // 代码分析
204
+ analyze_image({
205
+ image_source: "./code-error.png",
206
+ question: "这段代码为什么报错?请提供修复建议"
207
+ })
208
+
209
+ // UI 分析
210
+ analyze_image({
211
+ image_source: "https://example.com/ui.png",
212
+ question: "分析这个界面的布局和可用性问题"
213
+ })
214
+ ```
215
+
216
+ ## 环境变量
217
+
218
+ | 变量名 | 必需 | 默认值 | 说明 |
219
+ |-------------------------|------|------------|----------------------|
220
+ | `ZHIPU_API_KEY` | 是 | - | 智谱 AI 的 API 密钥 |
221
+ | `ZHIPU_MODEL` | 否 | `glm-4.5v` | 使用的模型 |
222
+ | `ZHIPU_MAX_TOKENS` | 否 | `4096` | 最大生成 tokens |
223
+ | `ZHIPU_TEMPERATURE` | 否 | `0.7` | 温度参数 (0-1) |
224
+ | `ZHIPU_TOP_P` | 否 | `0.7` | Top-p 参数 (0-1) |
225
+ | `ZHIPU_ENABLE_THINKING` | 否 | `false` | 是否强制启用思考模式 |
226
+
227
+ 注意: 思考模式默认已启用,无需额外配置。
228
+
229
+ ## 开发
230
+
231
+ ```bash
232
+ # 开发模式(监听文件变化)
233
+ npm run watch
234
+
235
+ # 构建
236
+ npm run build
237
+
238
+ # 本地测试
239
+ npm run test:local <图片路径> [问题]
240
+ ```
241
+
242
+ ## 项目结构
243
+
244
+ ```
245
+ luma-mcp/
246
+ ├── src/
247
+ │ ├── index.ts # MCP 服务器入口
248
+ │ ├── config.ts # 配置管理
249
+ │ ├── zhipu-client.ts # GLM-4.5V API 客户端
250
+ │ ├── image-processor.ts # 图片处理
251
+ │ ├── prompts.ts # 提示词模板
252
+ │ └── utils/
253
+ │ ├── logger.ts # 日志工具
254
+ │ └── helpers.ts # 工具函数
255
+ ├── test/
256
+ │ └── test-local.ts # 本地测试脚本
257
+ ├── docs/
258
+ │ ├── design.md # 设计文档
259
+ │ ├── installation.md # 安装指南
260
+ │ └── README_EN.md # 英文文档
261
+ ├── build/ # 编译输出
262
+ └── package.json
263
+ ```
264
+
265
+ ## 常见问题
266
+
267
+ ### 如何获取 API Key?
268
+
269
+ 1. 访问 [智谱开放平台](https://open.bigmodel.cn/)
270
+ 2. 注册/登录账号
271
+ 3. 进入控制台创建 API Key
272
+ 4. 复制 API Key 到配置文件
273
+
274
+ ### 支持哪些图片格式?
275
+
276
+ 支持 JPG、PNG、WebP、GIF 格式。建议使用 JPG 格式以获得更好的压缩率。
277
+
278
+ ### 图片大小限制?
279
+
280
+ - 最大文件大小: 10MB
281
+ - 超过 2MB 的图片会自动压缩
282
+ - 推荐分辨率: 800-2048 像素
283
+
284
+ ### 如何查看日志?
285
+
286
+ 日志文件位置: `~/.luma-mcp/luma-mcp-YYYY-MM-DD.log`
287
+
288
+ ### API 调用失败怎么办?
289
+
290
+ 1. 检查 API Key 是否正确
291
+ 2. 确认智谱账户余额充足
292
+ 3. 检查网络连接
293
+ 4. 查看日志文件了解详细错误信息
294
+
295
+ ### 成本如何?
296
+
297
+ GLM-4.5V 定价请参考[智谱官方定价](https://open.bigmodel.cn/pricing)。
298
+
299
+ 典型场景估算:
300
+ - 简单图片理解: 500-1000 tokens
301
+ - 代码截图分析: 1500-2500 tokens
302
+ - 详细 UI 分析: 2000-3000 tokens
303
+
304
+ 启用思考模式会增加约 20-30% tokens。
305
+
306
+ ## 贡献
307
+
308
+ 欢迎提交 Issue 和 Pull Request!
309
+
310
+ ## 许可证
311
+
312
+ MIT License
313
+
314
+ ## 相关链接
315
+
316
+ - [智谱 AI 开放平台](https://open.bigmodel.cn/)
317
+ - [GLM-4.5V 文档](https://docs.bigmodel.cn/cn/guide/models/vlm/glm-4.5v)
318
+ - [MCP 协议文档](https://modelcontextprotocol.io/)
319
+ - [设计文档](./docs/design.md)
320
+ - [安装配置指南](./docs/installation.md)
321
+
322
+ ## 作者
323
+
324
+ Jochen
325
+
326
+ ---
327
+
328
+ **注意**: 请勿在公开仓库中提交包含真实 API Key 的配置文件。
@@ -0,0 +1,17 @@
1
+ /**
2
+ * 配置管理模块
3
+ * 从环境变量读取配置
4
+ */
5
+ export interface LumaConfig {
6
+ apiKey: string;
7
+ model: string;
8
+ maxTokens: number;
9
+ temperature: number;
10
+ topP: number;
11
+ enableThinking: boolean;
12
+ }
13
+ /**
14
+ * 从环境变量加载配置
15
+ */
16
+ export declare function loadConfig(): LumaConfig;
17
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,cAAc,EAAE,OAAO,CAAC;CACzB;AAED;;GAEG;AACH,wBAAgB,UAAU,IAAI,UAAU,CAevC"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * 配置管理模块
3
+ * 从环境变量读取配置
4
+ */
5
+ /**
6
+ * 从环境变量加载配置
7
+ */
8
+ export function loadConfig() {
9
+ const apiKey = process.env.ZHIPU_API_KEY;
10
+ if (!apiKey) {
11
+ throw new Error('ZHIPU_API_KEY environment variable is required');
12
+ }
13
+ return {
14
+ apiKey,
15
+ model: process.env.ZHIPU_MODEL || 'glm-4.5v',
16
+ maxTokens: parseInt(process.env.ZHIPU_MAX_TOKENS || '4096', 10),
17
+ temperature: parseFloat(process.env.ZHIPU_TEMPERATURE || '0.7'),
18
+ topP: parseFloat(process.env.ZHIPU_TOP_P || '0.7'),
19
+ enableThinking: process.env.ZHIPU_ENABLE_THINKING === 'true',
20
+ };
21
+ }
22
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAWH;;GAEG;AACH,MAAM,UAAU,UAAU;IACxB,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC;IAEzC,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CAAC,gDAAgD,CAAC,CAAC;IACpE,CAAC;IAED,OAAO;QACL,MAAM;QACN,KAAK,EAAE,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,UAAU;QAC5C,SAAS,EAAE,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,IAAI,MAAM,EAAE,EAAE,CAAC;QAC/D,WAAW,EAAE,UAAU,CAAC,OAAO,CAAC,GAAG,CAAC,iBAAiB,IAAI,KAAK,CAAC;QAC/D,IAAI,EAAE,UAAU,CAAC,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,KAAK,CAAC;QAClD,cAAc,EAAE,OAAO,CAAC,GAAG,CAAC,qBAAqB,KAAK,MAAM;KAC7D,CAAC;AACJ,CAAC"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * 图片处理工具
3
+ * 负责读取、压缩和编码图片
4
+ * 支持本地文件和远程URL
5
+ */
6
+ /**
7
+ * 验证图片来源(文件或URL)
8
+ */
9
+ export declare function validateImageSource(imageSource: string, maxSizeMB?: number): Promise<void>;
10
+ /**
11
+ * 将图片转换为 base64 data URL 或返回URL
12
+ */
13
+ export declare function imageToBase64(imagePath: string): Promise<string>;
14
+ //# sourceMappingURL=image-processor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image-processor.d.ts","sourceRoot":"","sources":["../src/image-processor.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAOH;;GAEG;AACH,wBAAsB,mBAAmB,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,GAAE,MAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CA6BpG;AAED;;GAEG;AACH,wBAAsB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAyBtE"}
@@ -0,0 +1,97 @@
1
+ /**
2
+ * 图片处理工具
3
+ * 负责读取、压缩和编码图片
4
+ * 支持本地文件和远程URL
5
+ */
6
+ import { readFile, stat } from 'fs/promises';
7
+ import sharp from 'sharp';
8
+ import { isUrl } from './utils/helpers.js';
9
+ import { logger } from './utils/logger.js';
10
+ /**
11
+ * 验证图片来源(文件或URL)
12
+ */
13
+ export async function validateImageSource(imageSource, maxSizeMB = 10) {
14
+ // 如果是URL,直接返回
15
+ if (isUrl(imageSource)) {
16
+ logger.debug('Image source is URL, skipping validation', { imageSource });
17
+ return;
18
+ }
19
+ // 验证本地文件
20
+ try {
21
+ const stats = await stat(imageSource);
22
+ const fileSizeMB = stats.size / (1024 * 1024);
23
+ if (fileSizeMB > maxSizeMB) {
24
+ throw new Error(`Image file too large: ${fileSizeMB.toFixed(2)}MB (max: ${maxSizeMB}MB)`);
25
+ }
26
+ // 验证文件格式
27
+ const ext = imageSource.toLowerCase().split('.').pop();
28
+ const supportedFormats = ['jpg', 'jpeg', 'png', 'webp', 'gif'];
29
+ if (!ext || !supportedFormats.includes(ext)) {
30
+ throw new Error(`Unsupported image format: ${ext}. Supported: ${supportedFormats.join(', ')}`);
31
+ }
32
+ }
33
+ catch (error) {
34
+ if (error.code === 'ENOENT') {
35
+ throw new Error(`Image file not found: ${imageSource}`);
36
+ }
37
+ throw error;
38
+ }
39
+ }
40
+ /**
41
+ * 将图片转换为 base64 data URL 或返回URL
42
+ */
43
+ export async function imageToBase64(imagePath) {
44
+ try {
45
+ // 如果是URL,直接返回
46
+ if (isUrl(imagePath)) {
47
+ logger.info('Using remote image URL', { url: imagePath });
48
+ return imagePath;
49
+ }
50
+ // 本地文件:读取并编码
51
+ let imageBuffer = await readFile(imagePath);
52
+ // 检查文件大小,如果超过 2MB 则压缩
53
+ if (imageBuffer.length > 2 * 1024 * 1024) {
54
+ logger.info('Compressing large image', { originalSize: `${(imageBuffer.length / (1024 * 1024)).toFixed(2)}MB` });
55
+ imageBuffer = Buffer.from(await compressImage(imageBuffer));
56
+ }
57
+ // 转换为 base64
58
+ const base64 = imageBuffer.toString('base64');
59
+ const mimeType = getMimeType(imagePath);
60
+ return `data:${mimeType};base64,${base64}`;
61
+ }
62
+ catch (error) {
63
+ throw new Error(`Failed to process image: ${error instanceof Error ? error.message : 'Unknown error'}`);
64
+ }
65
+ }
66
+ /**
67
+ * 压缩图片
68
+ */
69
+ async function compressImage(imageBuffer) {
70
+ return sharp(imageBuffer)
71
+ .resize(2048, 2048, {
72
+ fit: 'inside',
73
+ withoutEnlargement: true
74
+ })
75
+ .jpeg({ quality: 85 })
76
+ .toBuffer();
77
+ }
78
+ /**
79
+ * 根据文件扩展名获取 MIME 类型
80
+ */
81
+ function getMimeType(filePath) {
82
+ const ext = filePath.toLowerCase().split('.').pop();
83
+ switch (ext) {
84
+ case 'jpg':
85
+ case 'jpeg':
86
+ return 'image/jpeg';
87
+ case 'png':
88
+ return 'image/png';
89
+ case 'webp':
90
+ return 'image/webp';
91
+ case 'gif':
92
+ return 'image/gif';
93
+ default:
94
+ return 'image/jpeg'; // 默认使用 jpeg
95
+ }
96
+ }
97
+ //# sourceMappingURL=image-processor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image-processor.js","sourceRoot":"","sources":["../src/image-processor.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Luma MCP Server
4
+ * 视觉理解 MCP 服务器,powered by GLM-4.5V
5
+ */
6
+ export {};
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA;;;GAGG"}
package/build/index.js ADDED
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Luma MCP Server
4
+ * 视觉理解 MCP 服务器,powered by GLM-4.5V
5
+ */
6
+ // 第一件事:重定向console到stderr,避免污染MCP的stdout
7
+ import { setupConsoleRedirection, logger } from './utils/logger.js';
8
+ setupConsoleRedirection();
9
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
10
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
11
+ import { z } from 'zod';
12
+ import { loadConfig } from './config.js';
13
+ import { ZhipuClient } from './zhipu-client.js';
14
+ import { imageToBase64, validateImageSource } from './image-processor.js';
15
+ import { buildAnalysisPrompt } from './prompts.js';
16
+ import { withRetry, createSuccessResponse, createErrorResponse } from './utils/helpers.js';
17
+ /**
18
+ * 创建 MCP 服务器
19
+ */
20
+ async function createServer() {
21
+ logger.info('Initializing Luma MCP Server');
22
+ // 加载配置
23
+ const config = loadConfig();
24
+ const zhipuClient = new ZhipuClient(config);
25
+ // 创建服务器 - 使用 McpServer
26
+ const server = new McpServer({
27
+ name: 'luma-mcp',
28
+ version: '1.0.0',
29
+ }, {
30
+ capabilities: {
31
+ tools: {},
32
+ },
33
+ });
34
+ // 创建带重试的分析函数
35
+ const analyzeWithRetry = withRetry(async (imageSource, question) => {
36
+ // 1. 验证图片来源
37
+ await validateImageSource(imageSource);
38
+ // 2. 处理图片(读取或返回URL)
39
+ const imageDataUrl = await imageToBase64(imageSource);
40
+ // 3. 构建提示词
41
+ const prompt = buildAnalysisPrompt(question);
42
+ // 4. 调用 GLM-4.5V 分析图片
43
+ return await zhipuClient.analyzeImage(imageDataUrl, prompt);
44
+ }, 2, // 最多重试2次
45
+ 1000 // 初始延迟1秒
46
+ );
47
+ // 注册工具 - 使用 McpServer.tool() API
48
+ server.tool('analyze_image', `使用智谱GLM-4.5V视觉模型分析图片内容。
49
+
50
+ **何时自动调用此工具**:
51
+ 1. 用户提供了图片文件路径(包括临时路径、相对路径、绝对路径)
52
+ 2. 用户提供了图片URL(https://开头)
53
+ 3. 用户粘贴了图片到聊天框(会生成临时路径)
54
+ 4. 用户要求分析、理解、查看、识别图片内容
55
+ 5. 用户询问关于图片中的代码、UI、错误信息等问题
56
+
57
+ **支持场景**:代码截图、UI界面、错误信息、通用图片分析。
58
+ **支持格式**:JPG、PNG、WebP、GIF。
59
+ **支持来源**:本地文件、远程URL、临时文件(包括截图)。
60
+
61
+ 如果你是不支持视觉的AI模型,看到图片路径时应主动调用此工具来分析图片内容。`, {
62
+ image_source: z.string().describe('图片来源:本地文件路径(含临时路径)或远程URL。例如:"./image.png"、"/tmp/screenshot.png"、"C:\\Users\\...\\image.jpg"、"https://example.com/pic.jpg"'),
63
+ question: z.string().optional().describe('可选:用户的问题或分析指令。例如:"这段代码为什么报错?"、"分析这个UI设计的问题"、"识别图片中的文字"'),
64
+ }, async (params) => {
65
+ try {
66
+ logger.info('Analyzing image', {
67
+ source: params.image_source,
68
+ hasQuestion: !!params.question,
69
+ });
70
+ // 执行分析(带重试)
71
+ const result = await analyzeWithRetry(params.image_source, params.question);
72
+ logger.info('Image analysis completed successfully');
73
+ return createSuccessResponse(result);
74
+ }
75
+ catch (error) {
76
+ logger.error('Image analysis failed', {
77
+ error: error instanceof Error ? error.message : String(error),
78
+ });
79
+ return createErrorResponse(error instanceof Error ? error.message : 'Unknown error');
80
+ }
81
+ });
82
+ return server;
83
+ }
84
+ /**
85
+ * 主函数
86
+ */
87
+ async function main() {
88
+ try {
89
+ const server = await createServer();
90
+ const transport = new StdioServerTransport();
91
+ await server.connect(transport);
92
+ logger.info('Luma MCP server started successfully on stdio');
93
+ }
94
+ catch (error) {
95
+ logger.error('Failed to start Luma MCP server', {
96
+ error: error instanceof Error ? error.message : String(error),
97
+ });
98
+ process.exit(1);
99
+ }
100
+ }
101
+ // 全局错误处理
102
+ process.on('uncaughtException', (error) => {
103
+ logger.error('Uncaught exception', { error: error.message, stack: error.stack });
104
+ process.exit(1);
105
+ });
106
+ process.on('unhandledRejection', (reason) => {
107
+ logger.error('Unhandled rejection', { reason });
108
+ process.exit(1);
109
+ });
110
+ process.on('SIGINT', () => {
111
+ logger.info('Received SIGINT, shutting down gracefully');
112
+ process.exit(0);
113
+ });
114
+ process.on('SIGTERM', () => {
115
+ logger.info('Received SIGTERM, shutting down gracefully');
116
+ process.exit(0);
117
+ });
118
+ main();
119
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * 提示词模板
3
+ * 参考 Claude Sonnet 4.5 的视觉理解方法
4
+ */
5
+ /**
6
+ * 构建图片分析提示词
7
+ */
8
+ export declare function buildAnalysisPrompt(question?: string): string;
9
+ //# sourceMappingURL=prompts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompts.d.ts","sourceRoot":"","sources":["../src/prompts.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAkC7D"}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * 提示词模板
3
+ * 参考 Claude Sonnet 4.5 的视觉理解方法
4
+ */
5
+ /**
6
+ * 构建图片分析提示词
7
+ */
8
+ export function buildAnalysisPrompt(question) {
9
+ if (question) {
10
+ // 有用户问题时,直接回答问题
11
+ return `
12
+ <image_analysis>
13
+ 请仔细观察这张图片,并回答以下问题:
14
+
15
+ ${question}
16
+
17
+ 分析要求:
18
+ 1. 系统性观察:从整体到细节,全面观察图片内容
19
+ 2. 准确识别:精确识别所有文字、符号、代码、数字等
20
+ 3. 结构化输出:使用清晰的标题和列表组织信息
21
+ 4. 中文表达:使用清晰准确的中文描述
22
+ 5. 具体可执行:如果涉及代码或问题,提供具体的解决方案
23
+
24
+ 请开始分析:
25
+ </image_analysis>
26
+ `.trim();
27
+ }
28
+ else {
29
+ // 没有问题时,通用描述
30
+ return `
31
+ <image_analysis>
32
+ 请详细描述这张图片的内容,包括:
33
+
34
+ 1. **主要内容**:图片展示了什么
35
+ 2. **关键元素**:重要的视觉元素、文字、符号
36
+ 3. **布局结构**:元素的组织和排列
37
+ 4. **文字内容**:提取所有可见文字
38
+
39
+ 请用中文清晰描述。
40
+ </image_analysis>
41
+ `.trim();
42
+ }
43
+ }
44
+ //# sourceMappingURL=prompts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompts.js","sourceRoot":"","sources":["../src/prompts.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;GAEG;AACH,MAAM,UAAU,mBAAmB,CAAC,QAAiB;IACnD,IAAI,QAAQ,EAAE,CAAC;QACb,gBAAgB;QAChB,OAAO;;;;EAIT,QAAQ;;;;;;;;;;;CAWT,CAAC,IAAI,EAAE,CAAC;IACP,CAAC;SAAM,CAAC;QACN,aAAa;QACb,OAAO;;;;;;;;;;;CAWV,CAAC,IAAI,EAAE,CAAC;IACP,CAAC;AACH,CAAC"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * 工具函数
3
+ */
4
+ /**
5
+ * 带重试机制的异步函数包装器
6
+ */
7
+ export declare function withRetry<T>(fn: (...args: any[]) => Promise<T>, maxRetries?: number, initialDelay?: number): (...args: any[]) => Promise<T>;
8
+ /**
9
+ * 检查字符串是否为 URL
10
+ */
11
+ export declare function isUrl(source: string): boolean;
12
+ /**
13
+ * 创建成功响应
14
+ */
15
+ export declare function createSuccessResponse(data: string): {
16
+ content: {
17
+ type: "text";
18
+ text: string;
19
+ }[];
20
+ };
21
+ /**
22
+ * 创建错误响应
23
+ */
24
+ export declare function createErrorResponse(message: string): {
25
+ content: {
26
+ type: "text";
27
+ text: string;
28
+ }[];
29
+ isError: boolean;
30
+ };
31
+ //# sourceMappingURL=helpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../src/utils/helpers.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;GAEG;AACH,wBAAgB,SAAS,CAAC,CAAC,EACzB,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,CAAC,CAAC,EAClC,UAAU,GAAE,MAAU,EACtB,YAAY,GAAE,MAAa,GAC1B,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,CAAC,CAAC,CAsBhC;AAED;;GAEG;AACH,wBAAgB,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAO7C;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM;;;;;EAIjD;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM;;;;;;EAKlD"}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * 工具函数
3
+ */
4
+ /**
5
+ * 带重试机制的异步函数包装器
6
+ */
7
+ export function withRetry(fn, maxRetries = 2, initialDelay = 1000) {
8
+ return async (...args) => {
9
+ let lastError;
10
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
11
+ try {
12
+ return await fn(...args);
13
+ }
14
+ catch (error) {
15
+ lastError = error instanceof Error ? error : new Error(String(error));
16
+ if (attempt === maxRetries) {
17
+ throw lastError;
18
+ }
19
+ // 指数退避
20
+ const delay = initialDelay * Math.pow(2, attempt);
21
+ await new Promise(resolve => setTimeout(resolve, delay));
22
+ }
23
+ }
24
+ throw lastError;
25
+ };
26
+ }
27
+ /**
28
+ * 检查字符串是否为 URL
29
+ */
30
+ export function isUrl(source) {
31
+ try {
32
+ const url = new URL(source);
33
+ return url.protocol === 'http:' || url.protocol === 'https:';
34
+ }
35
+ catch {
36
+ return false;
37
+ }
38
+ }
39
+ /**
40
+ * 创建成功响应
41
+ */
42
+ export function createSuccessResponse(data) {
43
+ return {
44
+ content: [{ type: 'text', text: data }],
45
+ };
46
+ }
47
+ /**
48
+ * 创建错误响应
49
+ */
50
+ export function createErrorResponse(message) {
51
+ return {
52
+ content: [{ type: 'text', text: `错误: ${message}` }],
53
+ isError: true,
54
+ };
55
+ }
56
+ //# sourceMappingURL=helpers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"helpers.js","sourceRoot":"","sources":["../../src/utils/helpers.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;GAEG;AACH,MAAM,UAAU,SAAS,CACvB,EAAkC,EAClC,aAAqB,CAAC,EACtB,eAAuB,IAAI;IAE3B,OAAO,KAAK,EAAE,GAAG,IAAW,EAAc,EAAE;QAC1C,IAAI,SAAgB,CAAC;QAErB,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,UAAU,EAAE,OAAO,EAAE,EAAE,CAAC;YACvD,IAAI,CAAC;gBACH,OAAO,MAAM,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC;YAC3B,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,SAAS,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;gBAEtE,IAAI,OAAO,KAAK,UAAU,EAAE,CAAC;oBAC3B,MAAM,SAAS,CAAC;gBAClB,CAAC;gBAED,OAAO;gBACP,MAAM,KAAK,GAAG,YAAY,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;gBAClD,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC;YAC3D,CAAC;QACH,CAAC;QAED,MAAM,SAAU,CAAC;IACnB,CAAC,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,KAAK,CAAC,MAAc;IAClC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC;QAC5B,OAAO,GAAG,CAAC,QAAQ,KAAK,OAAO,IAAI,GAAG,CAAC,QAAQ,KAAK,QAAQ,CAAC;IAC/D,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,qBAAqB,CAAC,IAAY;IAChD,OAAO;QACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;KACjD,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,mBAAmB,CAAC,OAAe;IACjD,OAAO;QACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,OAAO,OAAO,EAAE,EAAE,CAAC;QAC5D,OAAO,EAAE,IAAI;KACd,CAAC;AACJ,CAAC"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * 日志工具
3
+ * 将日志输出到 stderr,避免污染 MCP 的 stdout JSON 通信
4
+ */
5
+ declare class Logger {
6
+ private logFilePath?;
7
+ constructor();
8
+ private initLogFile;
9
+ private write;
10
+ info(message: string, ...args: any[]): void;
11
+ error(message: string, ...args: any[]): void;
12
+ warn(message: string, ...args: any[]): void;
13
+ debug(message: string, ...args: any[]): void;
14
+ }
15
+ export declare const logger: Logger;
16
+ /**
17
+ * 重定向 console 到 logger,避免污染 stdout
18
+ */
19
+ export declare function setupConsoleRedirection(): void;
20
+ export {};
21
+ //# sourceMappingURL=logger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/utils/logger.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAMH,cAAM,MAAM;IACV,OAAO,CAAC,WAAW,CAAC,CAAS;;IAM7B,OAAO,CAAC,WAAW;IAenB,OAAO,CAAC,KAAK;IAkBb,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE;IAIpC,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE;IAIrC,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE;IAIpC,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE;CAGtC;AAED,eAAO,MAAM,MAAM,QAAe,CAAC;AAEnC;;GAEG;AACH,wBAAgB,uBAAuB,SAMtC"}
@@ -0,0 +1,67 @@
1
+ /**
2
+ * 日志工具
3
+ * 将日志输出到 stderr,避免污染 MCP 的 stdout JSON 通信
4
+ */
5
+ import { appendFileSync, mkdirSync } from 'fs';
6
+ import { join } from 'path';
7
+ import { homedir } from 'os';
8
+ class Logger {
9
+ logFilePath;
10
+ constructor() {
11
+ this.initLogFile();
12
+ }
13
+ initLogFile() {
14
+ try {
15
+ const homeDir = homedir();
16
+ const now = new Date();
17
+ const dateStr = now.toISOString().split('T')[0]; // YYYY-MM-DD
18
+ const logDir = join(homeDir, '.luma-mcp');
19
+ mkdirSync(logDir, { recursive: true });
20
+ this.logFilePath = join(logDir, `luma-mcp-${dateStr}.log`);
21
+ }
22
+ catch (error) {
23
+ // 如果无法创建日志文件,只输出到 stderr
24
+ process.stderr.write(`[WARN] Failed to initialize log file: ${error}\n`);
25
+ }
26
+ }
27
+ write(level, message, ...args) {
28
+ const timestamp = new Date().toISOString();
29
+ const argsStr = args.length > 0 ? ` ${JSON.stringify(args)}` : '';
30
+ const logMessage = `[${timestamp}] ${level.toUpperCase()}: ${message}${argsStr}`;
31
+ // 输出到 stderr
32
+ process.stderr.write(logMessage + '\n');
33
+ // 写入日志文件
34
+ if (this.logFilePath) {
35
+ try {
36
+ appendFileSync(this.logFilePath, logMessage + '\n');
37
+ }
38
+ catch {
39
+ // 忽略文件写入错误
40
+ }
41
+ }
42
+ }
43
+ info(message, ...args) {
44
+ this.write('info', message, ...args);
45
+ }
46
+ error(message, ...args) {
47
+ this.write('error', message, ...args);
48
+ }
49
+ warn(message, ...args) {
50
+ this.write('warn', message, ...args);
51
+ }
52
+ debug(message, ...args) {
53
+ this.write('debug', message, ...args);
54
+ }
55
+ }
56
+ export const logger = new Logger();
57
+ /**
58
+ * 重定向 console 到 logger,避免污染 stdout
59
+ */
60
+ export function setupConsoleRedirection() {
61
+ console.log = logger.info.bind(logger);
62
+ console.info = logger.info.bind(logger);
63
+ console.error = logger.error.bind(logger);
64
+ console.warn = logger.warn.bind(logger);
65
+ console.debug = logger.debug.bind(logger);
66
+ }
67
+ //# sourceMappingURL=logger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.js","sourceRoot":"","sources":["../../src/utils/logger.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * 智谱 GLM-4.5V API 客户端
3
+ */
4
+ import type { LumaConfig } from './config.js';
5
+ /**
6
+ * 智谱 API 客户端
7
+ */
8
+ export declare class ZhipuClient {
9
+ private config;
10
+ private apiEndpoint;
11
+ constructor(config: LumaConfig);
12
+ /**
13
+ * 分析图片
14
+ */
15
+ analyzeImage(imageDataUrl: string, prompt: string, enableThinking?: boolean): Promise<string>;
16
+ }
17
+ //# sourceMappingURL=zhipu-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zhipu-client.d.ts","sourceRoot":"","sources":["../src/zhipu-client.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AA4C9C;;GAEG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,MAAM,CAAa;IAC3B,OAAO,CAAC,WAAW,CAA2D;gBAElE,MAAM,EAAE,UAAU;IAI9B;;OAEG;IACG,YAAY,CAAC,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC;CA2EpG"}
@@ -0,0 +1,83 @@
1
+ /**
2
+ * 智谱 GLM-4.5V API 客户端
3
+ */
4
+ import axios from 'axios';
5
+ import { logger } from './utils/logger.js';
6
+ /**
7
+ * 智谱 API 客户端
8
+ */
9
+ export class ZhipuClient {
10
+ config;
11
+ apiEndpoint = 'https://open.bigmodel.cn/api/paas/v4/chat/completions';
12
+ constructor(config) {
13
+ this.config = config;
14
+ }
15
+ /**
16
+ * 分析图片
17
+ */
18
+ async analyzeImage(imageDataUrl, prompt, enableThinking) {
19
+ const requestBody = {
20
+ model: this.config.model,
21
+ messages: [
22
+ {
23
+ role: 'user',
24
+ content: [
25
+ {
26
+ type: 'image_url',
27
+ image_url: {
28
+ url: imageDataUrl,
29
+ },
30
+ },
31
+ {
32
+ type: 'text',
33
+ text: prompt,
34
+ },
35
+ ],
36
+ },
37
+ ],
38
+ temperature: this.config.temperature,
39
+ max_tokens: this.config.maxTokens,
40
+ top_p: this.config.topP,
41
+ thinking: { type: 'enabled' }, // 默认启用 thinking
42
+ };
43
+ // 允许显式禁用 thinking
44
+ if (enableThinking === false) {
45
+ delete requestBody.thinking;
46
+ }
47
+ logger.info('Calling GLM-4.5V API', {
48
+ model: this.config.model,
49
+ thinking: !!requestBody.thinking
50
+ });
51
+ try {
52
+ const response = await axios.post(this.apiEndpoint, requestBody, {
53
+ headers: {
54
+ 'Authorization': `Bearer ${this.config.apiKey}`,
55
+ 'Content-Type': 'application/json',
56
+ },
57
+ timeout: 60000, // 60秒超时
58
+ });
59
+ if (!response.data.choices || response.data.choices.length === 0) {
60
+ throw new Error('No response from GLM-4.5V');
61
+ }
62
+ const result = response.data.choices[0].message.content;
63
+ const usage = response.data.usage;
64
+ logger.info('GLM-4.5V API call successful', {
65
+ tokens: usage?.total_tokens || 0,
66
+ model: response.data.model
67
+ });
68
+ return result;
69
+ }
70
+ catch (error) {
71
+ logger.error('GLM-4.5V API call failed', {
72
+ error: error instanceof Error ? error.message : String(error)
73
+ });
74
+ if (axios.isAxiosError(error)) {
75
+ const message = error.response?.data?.error?.message || error.message;
76
+ const status = error.response?.status;
77
+ throw new Error(`GLM-4.5V API error (${status || 'unknown'}): ${message}`);
78
+ }
79
+ throw error;
80
+ }
81
+ }
82
+ }
83
+ //# sourceMappingURL=zhipu-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zhipu-client.js","sourceRoot":"","sources":["../src/zhipu-client.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "luma-mcp",
3
+ "version": "1.0.0",
4
+ "description": "A vision understanding MCP server powered by GLM-4.5V",
5
+ "type": "module",
6
+ "bin": {
7
+ "luma-mcp": "./build/index.js"
8
+ },
9
+ "main": "./build/index.js",
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "watch": "tsc --watch",
13
+ "prepare": "npm run build",
14
+ "test:local": "tsx test/test-local.ts"
15
+ },
16
+ "keywords": [
17
+ "mcp",
18
+ "vision",
19
+ "ai",
20
+ "glm-4.5v",
21
+ "zhipu",
22
+ "image-understanding"
23
+ ],
24
+ "author": "Jochen",
25
+ "license": "MIT",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/JochenYang/luma-mcp.git"
29
+ },
30
+ "bugs": {
31
+ "url": "https://github.com/JochenYang/luma-mcp/issues"
32
+ },
33
+ "homepage": "https://github.com/JochenYang/luma-mcp#readme",
34
+ "dependencies": {
35
+ "@modelcontextprotocol/sdk": "^1.0.4",
36
+ "axios": "^1.7.9",
37
+ "sharp": "^0.33.5",
38
+ "zod": "^3.25.76"
39
+ },
40
+ "devDependencies": {
41
+ "@types/node": "^22.10.2",
42
+ "tsx": "^4.20.6",
43
+ "typescript": "^5.7.2"
44
+ }
45
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Luma MCP 本地测试脚本
3
+ * 直接测试图片分析功能,不需要MCP客户端
4
+ */
5
+
6
+ import { loadConfig } from '../src/config.js';
7
+ import { ZhipuClient } from '../src/zhipu-client.js';
8
+ import { imageToBase64, validateImageSource } from '../src/image-processor.js';
9
+ import { buildAnalysisPrompt } from '../src/prompts.js';
10
+ import { logger } from '../src/utils/logger.js';
11
+
12
+ async function testImageAnalysis(imagePath: string, question?: string) {
13
+ console.log('\n==========================================');
14
+ console.log('🧪 测试 Luma MCP 图片分析');
15
+ console.log('==========================================\n');
16
+
17
+ try {
18
+ // 1. 加载配置
19
+ console.log('📝 加载配置...');
20
+ const config = loadConfig();
21
+ console.log(`✅ 配置加载成功: 模型 ${config.model}\n`);
22
+
23
+ // 2. 验证图片
24
+ console.log('🔍 验证图片来源...');
25
+ await validateImageSource(imagePath);
26
+ console.log(`✅ 图片验证通过: ${imagePath}\n`);
27
+
28
+ // 3. 处理图片
29
+ console.log('🖼️ 处理图片...');
30
+ const imageDataUrl = await imageToBase64(imagePath);
31
+ const isUrl = imagePath.startsWith('http');
32
+ console.log(`✅ 图片处理完成: ${isUrl ? 'URL' : 'Base64编码'}\n`);
33
+
34
+ // 4. 构建提示词
35
+ console.log('💬 构建提示词...');
36
+ const prompt = buildAnalysisPrompt(question);
37
+ console.log(`✅ 提示词: ${question || '通用描述'}\n`);
38
+
39
+ // 5. 调用API
40
+ console.log('🤖 调用 GLM-4.5V API...');
41
+ const client = new ZhipuClient(config);
42
+ const result = await client.analyzeImage(imageDataUrl, prompt);
43
+
44
+ // 6. 显示结果
45
+ console.log('\n==========================================');
46
+ console.log('📊 分析结果');
47
+ console.log('==========================================\n');
48
+ console.log(result);
49
+ console.log('\n==========================================');
50
+ console.log('✅ 测试完成!');
51
+ console.log('==========================================\n');
52
+
53
+ } catch (error) {
54
+ console.error('\n❌ 测试失败:');
55
+ console.error(error instanceof Error ? error.message : String(error));
56
+ process.exit(1);
57
+ }
58
+ }
59
+
60
+ // 解析命令行参数
61
+ const args = process.argv.slice(2);
62
+
63
+ if (args.length === 0) {
64
+ console.log(`
65
+ 使用方法:
66
+ npm run test:local <图片路径或URL> [问题]
67
+
68
+ 示例:
69
+ # 分析本地图片
70
+ npm run test:local ./test.png
71
+
72
+ # 分析本地图片并提问
73
+ npm run test:local ./code-error.png "这段代码为什么报错?"
74
+
75
+ # 分析远程图片
76
+ npm run test:local https://example.com/image.jpg
77
+
78
+ 环境变量:
79
+ ZHIPU_API_KEY=your-api-key # 必需
80
+ ZHIPU_MODEL=glm-4.5v # 可选
81
+ `);
82
+ process.exit(1);
83
+ }
84
+
85
+ const imagePath = args[0];
86
+ const question = args.slice(1).join(' ') || undefined;
87
+
88
+ testImageAnalysis(imagePath, question);