Skip to main content
Glama
README.md
# qwen-vision-mcp

让任何**文本模型**(Claude、GLM、GPT、Gemini...)通过 MCP 按需"看图和看视频"。后端走阿里云 DashScope 的 qwen3.7-plus,通过 Anthropic 兼容接口调用——qwen 原生支持图片和视频理解。

## 为什么需要这个

文本模型(GLM-4、Claude Haiku、GPT-4o-mini 等便宜/快的模型)不会直接看图。但很多任务(截图报错、UI 调试、图表分析、文档 OCR)需要视觉理解。

`qwen-vision-mcp` 把 qwen 视觉模型包装成一个 MCP 工具 `look(image, question)`,让任何 MCP 兼容的 Agent 按需调用——只在需要时才"看一眼",主对话仍由你便宜的文本模型驱动。

## 特色

- **🧠 有状态多轮持久**:同一会话内 qwen 会记得之前所有图片和问答,支持"再看一眼刚才那张图"。
- **🚆 自动 trim 防超限**:单次请求体超过 DashScope 6MB 上限时,自动把较早的 image/video block 替换成文字指针(保留文字 question + assistant 回复),长程多模态对话不崩。
- **🎬 视频原生支持**:qwen3.7-plus 通过 anthropic 兼容接口直接吃整个视频(不是逐帧),52s 视频 ≈ 31K tokens。硬 cap 60s / base64 后 6.25MB,超限直接报错不压缩。
- **🖼️ 大图自动压缩**:本地大图 / 剪贴板截图(> 1.5MB)发送前自动用系统 `sips` 缩到长边 1568px + 转 JPEG q85,避免 base64 膨胀后撑爆 6MB 上限(25MB HEIC 实测压到 ~330KB)。macOS 自带 sips 零依赖;非 macOS fallback 到直接 base64。
- **🔌 按进程隔离**:每个客户端会话独立一个 MCP server 进程,messages 不串台。
- **📦 极简依赖**:只依赖 `@modelcontextprotocol/sdk`,Bun 直接跑 .ts,无编译步骤。
- **🔁 失败干净回滚**:网络异常 / HTTP 错误时自动 pop 掉刚 push 的 user message,历史不污染。

## 工作原理

```
文本模型 (GLM/Claude/GPT)  ──MCP stdio──▶  qwen-vision-mcp server
                                                  │
                                                  ▼
                                          DashScope qwen 视觉模型
                                         (Anthropic 兼容接口)
```

主模型决定"什么时候需要看图、问什么",qwen 只负责"看一眼并回答具体问题"。模型行为由工具 `description` 引导(要拆解、要聚焦、要多轮追问)。

## 安装

需要 [Bun](https://bun.sh/) 运行时 + DashScope API Key([阿里云百炼控制台](https://bailian.console.aliyun.com/) 获取)。

```bash
git clone <your-fork-url> qwen-vision-mcp
cd qwen-vision-mcp
bun install                    # 装 @modelcontextprotocol/sdk
cp .env.example .env           # 填入 QWEN_API_KEY
```

`.env` 内容:

```env
QWEN_BASE_URL=https://dashscope.aliyuncs.com/apps/anthropic/v1
QWEN_API_KEY=sk-你的-key
QWEN_MODEL=qwen3.7-plus
QWEN_MAX_TOKENS=1024
QWEN_MAX_REQUEST_BYTES=5000000
QWEN_COMPRESS_THRESHOLD=1500000
QWEN_IMAGE_MAX_EDGE=1568
```

## 接入 MCP 客户端

### Claude Code

在 `~/.claude.json`(或项目级 `.mcp.json`)的 `mcpServers` 里加:

```json
{
  "mcpServers": {
    "qwen-vision": {
      "command": "bun",
      "args": ["run", "/absolute/path/to/qwen-vision-mcp/index.ts"],
      "env": { "QWEN_API_KEY": "sk-..." }
    }
  }
}
```

### Cursor / Continue / 其它 MCP 客户端

任何支持 stdio MCP server 的客户端都能接入,配置格式大同小异——指定 `bun` 为 command,`run /path/to/index.ts` 为 args,把 `QWEN_API_KEY` 放到 env 里。

### Proma

在 Proma 工作区的 `mcp.json` 里加:

```json
"qwen-vision": {
  "type": "stdio",
  "command": "bun",
  "args": ["run", "/absolute/path/to/qwen-vision-mcp/index.ts"],
  "env": { "QWEN_API_KEY": "sk-...", "QWEN_MODEL": "qwen3.7-plus" },
  "enabled": true
}
```

## 暴露的工具

### `look(image, question)` — 有状态

让 qwen 看一张图片回答你提出的具体问题。

- **image**: 本地绝对路径 / http(s) URL / `data:image;base64,...`
- **question**: 针对该图片的具体、聚焦的问题

**重要**:这个工具是**有状态的**。同一会话内 qwen 会记得前面所有图片和问答。
- 同一张图追问细节 → 直接连续调用,qwen 自带上下文
- 切换到**完全无关的新图** → 先调 `reset_vision_session` 清空历史,避免旧图污染

### `watch(video, question)` — 有状态,视频

让 qwen 看一段视频回答问题。qwen 直接吃整个视频(不是逐帧),原生支持视频理解。

- **video**: 本地绝对路径 / http(s) URL / `data:video;base64,...`
- **question**: 针对该视频的具体、聚焦的问题

**硬 cap**(超限直接报错,不自动压缩):
- 时长 ≤ 60 秒
- 原始 ≤ ~4.7MB(base64 后 < 6.25MB,DashScope 请求体 6MB 硬限制)

**上下文成本警告**:1 分钟视频 ≈ 30K tokens。复杂视频分析建议**委托子 agent**(如 Proma 的 `collaboration.delegate_agent`)——子 agent 在独立上下文里多轮追问,只把最终结论返回主对话,避免视频 token 撑爆主上下文。

### `reset_vision_session()` — 清空历史

何时调:当前图片讨论完、要切到无关新图时,或想从干净状态重启时。
何时不调:同一张图的多轮追问、相关图片延续讨论时。

## 配置项

| 环境变量 | 默认 | 说明 |
|---|---|---|
| `QWEN_BASE_URL` | `https://dashscope.aliyuncs.com/apps/anthropic/v1` | DashScope Anthropic 兼容入口 |
| `QWEN_API_KEY` | 必填 | DashScope API Key |
| `QWEN_MODEL` | `qwen3.7-plus` | 视觉模型 id(可改 `qwen-vl-max-latest` 等) |
| `QWEN_MAX_TOKENS` | `1024` | 单次回复 token 上限 |
| `QWEN_MAX_REQUEST_BYTES` | `5000000` | 单次请求体字节上限(DashScope 硬上限 6MB) |
| `QWEN_ENABLE_THINKING` | `false` | 推理模型 thinking 开关。默认关(视觉问答 7-10x 提速);复杂图表分析/推理题场景设 `true` |
| `QWEN_COMPRESS_THRESHOLD` | `1500000` | 原图字节超过此值则用 sips 缩放压缩成 JPEG(防 base64 膨胀撑爆 6MB 请求体) |
| `QWEN_IMAGE_MAX_EDGE` | `1568` | 压缩时长边像素上限(qwen-vl 推荐尺寸,再高也是浪费 token) |
| `QWEN_MAX_VIDEO_SECONDS` | `60` | 视频时长上限(ffprobe 不可用时跳过时长检查) |
| `QWEN_MAX_VIDEO_BYTES` | `6250000` | 视频 base64 后字节上限(DashScope 请求体 6MB 硬限留余量) |

## 已知限制

- **进程隔离 ≠ 跨重启持久化**:进程被杀(客户端退出、crash)→ 内存里的 messages 丢失 = 自动 reset。同一会话**活跃期间**的持久化没问题,跨重启做不到。
- **URL 图片需 DashScope 服务端能访问**:内网图床 / 被墙的 URL 会报 `Failed to download multimodal content`,**用本地路径最稳**。
- **历史图片 trim 策略**:超过 `QWEN_MAX_REQUEST_BYTES` 时从最早的 image 开始替换成文字指针,保留最后 1 张图和所有文字。这意味着 qwen 对"很久之前的图"会失去视觉记忆(但仍能从对话历史里"读"自己之前的描述)。
- **压缩依赖 macOS sips**:大图缩放走系统命令 `sips`(`/usr/bin/sips`),macOS 自带。Linux/Windows 没有 sips,会 fallback 到直接 base64 原图 —— 此时单张超大图仍可能触发 6MB 上限,需要换 `sharp` 等跨平台方案。
- **视频不做自动压缩**:cap 60s / base64 后 6.25MB,超限直接报错。设计取舍——视频压缩不 general(sips 只能处理图,ffmpeg 要额外装),让用户自行用 ffmpeg 处理。时长检查依赖 ffprobe(ffmpeg 套件),不可用时只检查大小。
- **视频上下文成本高**:1 分钟视频 ≈ 30K tokens,多次 watch 会迅速占满主对话上下文。复杂场景请委托子 agent 承载。

## 故障排查

| 错误 | 原因 | 解决 |
|---|---|---|
| 工具不注入 / 启动无报错 | 调用此 MCP 的 agent 对 `index.ts` 所在目录无读权限(command 写绝对路径只是必要条件,agent 的运行环境还必须能访问到该文件) | 确保调用方 agent 能读到 `.ts` 文件本身。Proma 下若脚本放在 `~/Documents` 等 macOS 保护目录,把该目录加入工作区可访问目录即可(无需改「系统设置 → 隐私」);终端 / Claude Code 等加载用户 shell 的客户端一般无此问题 |
| `404` | BASE_URL 错了 | 确认用 `/apps/anthropic/v1` 而不是 `/compatible-mode/v1` |
| `model not found` | QWEN_MODEL id 错了 | 查 [DashScope 文档](https://help.aliyun.com/zh/model-studio/) 确认 id |
| `Failed to download multimodal content` | URL 图片 DashScope 服务端访问不到 | 改用本地路径 |
| `Exceeded limit on max bytes to request body : 6291456` | 请求体超 6MB(DashScope 硬上限) | 单张大图会自动 sips 压缩;若仍报错说明历史累积太多 → 调 `reset_vision_session` 清历史,或调小 `QWEN_MAX_REQUEST_BYTES` / `QWEN_COMPRESS_THRESHOLD` |
| 响应慢(每次 10s+) | `qwen3.7-plus` 是推理模型,默认每轮生成几百 token thinking | 已默认关 thinking;确认 `QWEN_ENABLE_THINKING=false`(或换非推理模型如 `qwen-vl-max-latest`) |
| `视频过大 / 视频过长` | 超过 cap(60s 或 base64 后 6.25MB) | 用 ffmpeg 压缩:`ffmpeg -i in.mp4 -b:v 500k -t 60 out.mp4`;或截短 |

## 开发与测试

```bash
# 启动 server(stdio 模式,等待客户端连接)
bun run index.ts

# 协议层冒烟测试(发 initialize + tools/list)
( \
  printf '%s\n' \
    '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' \
    '{"jsonrpc":"2.0","method":"notifications/initialized"}' \
    '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' ; \
  sleep 2 \
) | bun run index.ts

# 端到端测试(看一张本地图)
( \
  printf '%s\n' \
    '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' \
    '{"jsonrpc":"2.0","method":"notifications/initialized"}' \
    '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"look","arguments":{"image":"/tmp/test.png","question":"一句话描述这张图"}}}' ; \
  sleep 15 \
) | bun run index.ts

# 验证 sips 大图压缩(用系统桌面图模拟极端大图输入;期望输出 < 1MB)
SRC=$(ls -S "/System/Library/Desktop Pictures/"*.heic 2>/dev/null | head -1)
sips -Z 1568 -s format jpeg -s formatOptions 85 "$SRC" --out /tmp/qwen-compress-test.jpg
ls -lh /tmp/qwen-compress-test.jpg   # 25MB HEIC 实测 ~330KB
rm -f /tmp/qwen-compress-test.jpg
```

## License

[MIT](./LICENSE)