{t.localapi.note}
+ + {status?.enabled && ( +{u}
+ ))}
+ {t.localapi.passwordRequired}
+ )} +{t.localapi.lanNote}
+ +
+ {`http://127.0.0.1:${status.port}/mcp`}
+
+
+ {status.mcpToken}
+
+
+
+ {t.localapi.mcpNote}
+
@@ -113,6 +115,33 @@ means full support (ARM: `grep -o asimddp /proc/cpuinfo`).
Full deployment guide (compose, Synology walkthrough, permissions, reverse proxy / HTTPS,
backup, limitations): [docs/server/README.md](docs/server/README.md).
+## MCP (let AI clients query your library)
+
+`vixel mcp` runs a **stdio MCP server** with read-only tools (`search_media` / `get_media` /
+`find_similar`), so Claude Desktop, Cursor or Claude Code can search your library **without
+any network service**. Results include `vixel://` deep links that open the item in the app
+(videos jump to the matched timestamp). It needs a source checkout (`npm install &&
+npm run build:cli`; models inside an installed Vixel.app are found automatically):
+
+```json
+{
+ "mcpServers": {
+ "vixel": {
+ "command": "/absolute/path/Vixel/node_modules/.bin/electron",
+ "args": ["/absolute/path/Vixel/dist/cli/cli/index.js", "mcp"],
+ "env": { "ELECTRON_RUN_AS_NODE": "1" }
+ }
+ }
+}
+```
+
+> Why Electron as the runtime: the repo's better-sqlite3 is compiled for Electron's ABI;
+> with `ELECTRON_RUN_AS_NODE=1` Electron behaves as plain Node and opens no window.
+
+No source checkout? The desktop app's Settings → General → **Local API** exposes the same
+tools over Streamable HTTP at `http://127.0.0.1:41721/mcp` (bearer token shown in Settings);
+the server edition enables it with `VIXEL_MCP_TOKEN` (see [docs/server/README.md](docs/server/README.md)).
+
## Build from source
Node.js 20+.
diff --git a/README.md b/README.md
index 4cac266..d4b22f8 100644
--- a/README.md
+++ b/README.md
@@ -92,6 +92,8 @@ SCRFD 人脸检测 + MobileFaceNet 512 维特征 + sqlite-vec 近邻索引。
- **HEIC / RAW** —— macOS 上通过系统 `sips` 解码 CR2 / CR3 / NEF / ARW / DNG / RAF / ORF / RW2,保留 EXIF。
- **快速查看** —— 空格预览,⌘1–5 切换 全部 / 图片 / 视频 / 音频 / 地图。
- **命令行** —— `vixel search "海边" --json`,方便脚本和 Agent 调用(见下文);结果里的 `matchedBy` 说明是画面、图中文字还是文件名命中。
+- **链接进笔记** —— 右键「拷贝链接 / 拷贝 Markdown 链接」得到 `vixel://` 深链接,贴进 Obsidian、备忘录等任意笔记,点击回到 Vixel 定位到那一项(视频 / 音频直接跳到时间点)。链接按内容哈希定位,清库重建、移动文件后仍然有效。
+- **本地接口(默认关闭)** —— 设置 → 通用 → 本地接口:在本机开出网页界面和 HTTP API,手机浏览器也能访问桌面的资料库(局域网访问必须设口令);同一服务的 `/mcp` 是给 AI 客户端的只读 MCP 接口(见下文)。
- **中英文界面** —— 跟随系统语言,也可在 设置 → 通用 里切换。
- **诊断** —— `vixel doctor` / `vixel bench` 体检和测速,`VIXEL_PROFILE=1` 打印慢操作,见 [docs/debugging.md](docs/debugging.md)。
@@ -252,9 +254,36 @@ npm run cli -- people
npm run cli -- stats
```
-支持 `search` · `info` · `similar` · `people` · `stats` · `caption` · `folders`,
+支持 `search` · `info` · `similar` · `people` · `stats` · `caption` · `folders` · `mcp`,
`--json` 输出便于脚本和 AI Agent 调用。需要先运行一次应用以创建照片库。
+### MCP(让 AI 客户端查你的库)
+
+`vixel mcp` 把同一套查询变成 **stdio 形态的 MCP 服务器**(工具全部只读:`search_media` /
+`get_media` / `find_similar`),Claude Desktop、Cursor、Claude Code 等本地 AI 客户端
+**不开任何网络服务**就能搜你的库;结果里的 `vixel://` 链接点开直接回到应用并定位(视频跳到时间点)。
+需要源码仓库(`npm install && npm run build:cli`;已安装的 Vixel.app 里的语义模型会被自动找到),
+客户端里这样配置(Claude Desktop 的 `claude_desktop_config.json`、Cursor 的 `mcp.json` 同形):
+
+```json
+{
+ "mcpServers": {
+ "vixel": {
+ "command": "/绝对路径/Vixel/node_modules/.bin/electron",
+ "args": ["/绝对路径/Vixel/dist/cli/cli/index.js", "mcp"],
+ "env": { "ELECTRON_RUN_AS_NODE": "1" }
+ }
+ }
+}
+```
+
+> 为什么用 Electron 当运行时:仓库的 better-sqlite3 按 Electron 的 ABI 构建,普通 `node` 加载不了;
+> `ELECTRON_RUN_AS_NODE=1` 下 Electron 就是一个纯 Node,不会弹窗口。
+
+不想碰源码:桌面应用 设置 → 通用 → **本地接口** 打开后,同样的工具在 `http://127.0.0.1:41721/mcp`
+(Streamable HTTP + Bearer 令牌,设置页里拷贝);服务器版设 `VIXEL_MCP_TOKEN` 同理,
+见 [docs/server/README.md](docs/server/README.md)。
+
## 开发
```
diff --git a/src/cli/index.ts b/src/cli/index.ts
index c3e64d5..908e851 100644
--- a/src/cli/index.ts
+++ b/src/cli/index.ts
@@ -10,6 +10,7 @@ import { initDatabase, type DatabaseInstance } from '../core/db'
import { SearchEngine } from '../core/search'
import { initEmbeddingServicePath, getEmbeddingService } from '../core/embedding'
import { cmdDoctor, cmdBench } from './doctor'
+import { cmdMcp } from './mcp'
import { litertPlatformDir } from '../core/embedding/providers/litert/native'
// 跨平台 userData 路径。打包版目录名取 productName(Vixel),开发版取 package.json name(vixel);
@@ -366,6 +367,7 @@ Usage:
vixel folders List watched folders
vixel doctor Health check: counts, queue errors, query plans
vixel bench Time hot queries and searches on this library
+ vixel mcp MCP server on stdio for AI clients (read-only; see README)
Flags:
--json JSON output (for scripts/agents)
@@ -387,6 +389,11 @@ Environment:
const { command, positional, flags } = parseArgs(args)
+ // MCP:stdout 是 JSON-RPC 协议通道,任何日志(迁移提示、模型加载)都改走 stderr
+ if (command === 'mcp') {
+ console.log = (...a: unknown[]) => console.error(...a)
+ }
+
// 检查数据库
const dbPath = getDbPath()
if (!existsSync(dbPath)) {
@@ -398,7 +405,7 @@ Environment:
// 初始化。只有改 caption 需要写库;其余只读打开,不跑迁移,应用开着也安全
const modelsDir = getModelsDir()
initEmbeddingServicePath(getUserDataPath(), modelsDir, getLiteRtDir())
- if (!modelsDir && (command === 'search' || command === 'similar' || command === 'bench')) {
+ if (!modelsDir && (command === 'search' || command === 'similar' || command === 'bench' || command === 'mcp')) {
console.error('Note: models not found — semantic search disabled (set VIXEL_MODELS_DIR). Text and filename matching still work.')
}
const writes = command === 'caption' && flags.set !== undefined
@@ -440,6 +447,9 @@ Environment:
case 'bench':
await cmdBench(db, flags)
break
+ case 'mcp':
+ await cmdMcp(db)
+ break
default:
console.error(`Unknown command: ${command}`)
console.error('Run "vixel --help" for usage.')
diff --git a/src/cli/mcp.ts b/src/cli/mcp.ts
new file mode 100644
index 0000000..ecf9f7e
--- /dev/null
+++ b/src/cli/mcp.ts
@@ -0,0 +1,60 @@
+/**
+ * `vixel mcp` —— stdio 传输的 MCP 服务器:本地 AI 客户端(Claude Desktop / Cursor / Claude Code)
+ * 不开任何网络服务就能查询媒体库。工具与桌面"本地接口"、服务器版 /mcp 完全相同
+ * (core/mcp:search_media / get_media / find_similar,全部只读),数据库按 CLI 惯例只读打开。
+ *
+ * 传输即 MCP 规范的 stdio 形态:stdin 每行一条 JSON-RPC 消息,响应单行 JSON 写 stdout;
+ * 日志一律走 stderr(cli/index.ts 在 mcp 命令下已把 console.log 重定向过去)。
+ * stdin 关闭(客户端退出)即结束。
+ */
+import { createInterface } from 'readline'
+import { join } from 'path'
+import type { DatabaseInstance } from '../core/db'
+import { SearchEngine } from '../core/search'
+import { getEmbeddingService } from '../core/embedding'
+import { createMcpServer, errorResponse, JSONRPC_PARSE_ERROR } from '../core/mcp/server'
+import { createToolContext } from '../core/mcp/tools'
+
+/** serverInfo 用的应用版本(dist/cli/cli → 仓库根在三级之上) */
+function appVersion(): string {
+ try {
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
+ return (require(join(__dirname, '..', '..', '..', 'package.json')) as { version?: string }).version ?? '0.0.0'
+ } catch {
+ return '0.0.0'
+ }
+}
+
+export async function cmdMcp(db: DatabaseInstance): Promise