diff --git a/.gitignore b/.gitignore index 5cc4022..a8acfc8 100644 --- a/.gitignore +++ b/.gitignore @@ -47,5 +47,4 @@ logs/ docs/ # Zotero plugin specific -zotero-mcp-plugin/addon/ zotero-mcp-plugin/build/ \ No newline at end of file diff --git a/README-zh.md b/README-zh.md index 1a70f71..c37da31 100644 --- a/README-zh.md +++ b/README-zh.md @@ -6,7 +6,7 @@ _This README is also available in: [:gb: English](./README.md) | :cn: 简体中 [![zotero target version](https://img.shields.io/badge/Zotero-7-green?style=flat-square&logo=zotero&logoColor=CC2936)](https://www.zotero.org) [![Node.js](https://img.shields.io/badge/Node.js-18%2B-green)](https://nodejs.org) [![TypeScript](https://img.shields.io/badge/TypeScript-5.4-blue)](https://www.typescriptlang.org) -[![Version](https://img.shields.io/badge/Version-1.5.0-brightgreen)]() +[![Version](https://img.shields.io/badge/Version-1.5.2-brightgreen)]() [![EN doc](https://img.shields.io/badge/Document-English-blue.svg)](README.md) [![中文文档](https://img.shields.io/badge/文档-中文-blue.svg)](README-zh.md) @@ -27,6 +27,7 @@ Zotero MCP 服务器是一个基于 Model Context Protocol 的工具服务器, - 🧠 **语义搜索**:基于 AI 向量嵌入的概念匹配,发现跨语言的相关文献 - ✏️ **写入操作**:创建笔记、管理标签、更新元数据、创建新条目并关联附件 - 💾 **全文数据库**:访问和搜索缓存的 PDF 全文内容 +- 📑 **引文导出**:通过 Better BibTeX 导出 BibLaTeX/BibTeX 条目,生成 CSL 格式的参考文献和文内引用,同步 `.bib` 文件,在 LaTeX/Markdown 草稿中插入引用 这使得 AI 助手能够帮助您进行文献综述、引用管理、内容分析、批注整理、知识库管理等学术工作。 @@ -160,6 +161,7 @@ AI 客户端 ↔ Streamable HTTP ↔ Zotero 插件(集成 MCP 服务器) - **语义搜索**: 基于 AI 向量嵌入的语义搜索,支持 OpenAI/Ollama API,发现概念相关的文献 - **写入功能**: 创建/修改笔记、管理标签、更新元数据字段、创建新条目并关联独立 PDF - **全文数据库**: 缓存的 PDF 全文数据库,支持列表、搜索、获取和统计操作 +- **引文与参考文献导出**: 通过 Better BibTeX JSON-RPC 导出 BibLaTeX/BibTeX 条目,生成可自定义 CSL 样式的参考文献和文内引用,列出可用引文样式 - **独立附件管理**: 搜索和管理只有 PDF 没有元数据信息的独立条目 - **客户端配置生成器**: 自动为各种 AI 客户端生成配置 - **安全性**: 仅本地操作,确保数据完全隐私 @@ -312,7 +314,7 @@ MCP 服务器已集成在插件内,位于 `src/modules/streamableMCPServer.ts` ## 🔧 API 参考(MCP 工具列表) -插件集成的 MCP 服务器提供以下 **20 个工具**,分为 5 大类: +插件集成的 MCP 服务器提供以下 **25 个工具**,分为 6 大类: ### 一、搜索与查询(7 个) @@ -464,6 +466,141 @@ MCP 服务器已集成在插件内,位于 `src/modules/streamableMCPServer.ts` | `attachmentKeys` | string[] | 要关联的独立附件 Key 列表 | | `parentKey` | string | reparent 操作的目标父条目 Key | +### 六、引文与参考文献导出(5 个) + +#### `export_bibliography` +通过 zotero-better-bibtex (BBT) 插件将一个或多个条目导出为 BibLaTeX/BibTeX(或 CSL-JSON/CSL-YAML)条目。需安装并启用 Better BibTeX。 + +| 参数 | 类型 | 描述 | +|---|---|---| +| `itemKeys` | string[] | **必需**,要导出的条目 Key 列表 | +| `format` | string | 导出格式:biblatex(默认)/bibtex/csljson/cslyaml | +| `libraryID` | number | 目标库 ID(默认用户库) | +| `exportNotes` | boolean | 是否导出笔记(默认 false) | +| `useJournalAbbreviation` | boolean | 是否使用期刊缩写(默认 false) | + +#### `get_citation` +使用 CSL 引文样式为条目生成格式化的参考文献条目或文内引用。未指定样式时使用 Zotero 默认 Quick Copy 样式。无需 Better BibTeX。 + +| 参数 | 类型 | 描述 | +|---|---|---| +| `itemKeys` | string[] | **必需**,要引用的条目 Key 列表 | +| `style` | string | CSL 样式 ID 或标题(如 "apa"/"ieee"),省略则使用默认样式 | +| `contentType` | string | 输出格式:html(默认)/text | +| `mode` | string | 生成模式:bibliography(默认,参考文献条目)/citation(文内引用) | +| `libraryID` | number | 目标库 ID(默认用户库) | + +#### `list_citation_styles` +列出 Zotero 中可用的 CSL 引文样式(供 `get_citation` 使用),每项包含 id 和 title,支持关键字过滤。 + +| 参数 | 类型 | 描述 | +|---|---|---| +| `filter` | string | 可选关键字,按标题或 ID 过滤(如 "apa"/"ieee"/"chicago") | + +#### `sync_bib` +将整个 Zotero 文献库同步导出到磁盘上的 `.bib` 文件。通过 BBT 导出所有顶级条目为 BibTeX(或 BibLaTeX/CSL-JSON/CSL-YAML)格式,并写入指定路径。需安装并启用 Better BibTeX。 + +| 参数 | 类型 | 描述 | +|---|---|---| +| `bibPath` | string | **必需**,输出 `.bib` 文件的绝对路径 | +| `format` | string | 导出格式:bibtex(默认)/biblatex/csljson/cslyaml | +| `libraryID` | number | 目标库 ID(默认用户库) | +| `includeChildren` | boolean | 是否包含子条目(笔记、附件等),默认 false | + +#### `cite` +在 LaTeX(`.tex`)或 Markdown 草稿中插入引用,并同步 `.bib` 文件。通过 itemKey 或搜索关键词查找 Zotero 条目,导出为 BibTeX,追加到指定 `.bib` 文件(若引用键已存在则跳过),然后在草稿中插入 `\cite{key}`(LaTeX)或 `[@key]`(Markdown)。需安装并启用 Better BibTeX。 + +| 参数 | 类型 | 描述 | +|---|---|---| +| `bibPath` | string | **必需**,`.bib` 文件路径(不存在则创建) | +| `itemKey` | string | Zotero 条目 Key(与 query 二选一) | +| `query` | string | 搜索关键词(匹配标题和作者,与 itemKey 二选一) | +| `texPath` | string | LaTeX 草稿路径(与 markdownPath 二选一) | +| `markdownPath` | string | Markdown 草稿路径(与 texPath 二选一) | +| `marker` | string | 占位符,若提供则替换为引用;否则在文件末尾追加 | +| `libraryID` | number | 目标库 ID(默认用户库) | + +--- + +## 🔌 外部工具注册 API + +其他 Zotero 插件可以通过本插件的 MCP 服务器注册自定义 MCP 工具。这样第三方插件无需运行自己的 HTTP 服务器即可扩展 AI 可调用的工具集。 + +### 工作原理 + +1. 你的插件调用 `Zotero.ZoteroMCP.api.registerTool(...)` 注册工具。 +2. 该工具会与内置工具一起出现在 `tools/list` 中。 +3. AI 客户端调用该工具时,MCP 服务器将请求转发到你的 handler。 +4. 插件禁用/卸载时,调用 `Zotero.ZoteroMCP.api.unregisterTool(...)` 或 `unregisterAllTools(pluginID)` 清理。 + +### API 方法 + +| 方法 | 说明 | +|---|---| +| `registerTool(def)` | 注册自定义 MCP 工具,参数非法或名称冲突时抛出异常 | +| `unregisterTool(name)` | 按名称注销工具,返回是否成功移除 | +| `unregisterAllTools(pluginID)` | 注销某插件注册的所有工具,返回移除数量 | +| `getRegisteredTools()` | 列出已注册工具(只读,不含 handler) | +| `isToolRegistered(name)` | 检查某工具名是否已注册 | +| `onToolListChanged(cb)` | 订阅工具列表变更,返回取消订阅函数 | + +### 工具定义 + +```typescript +{ + name: string; // 唯一名称,匹配 /^[a-z][a-z0-9_]*$/,不能与内置工具冲突 + description: string; // 显示给 AI 客户端的描述 + inputSchema: object; // 输入参数的 JSON Schema + handler: (args: any) => Promise | any; // AI 调用工具时执行的函数 + pluginID?: string; // 可选:注册方插件 ID(用于批量清理) + enabled?: boolean; // 可选:是否在 tools/list 中可见(默认 true) +} +``` + +### 示例(来自其他插件) + +```javascript +// 从你的 Zotero 插件注册自定义 MCP 工具 +const mcp = Zotero.ZoteroMCP; +if (mcp && mcp.api && mcp.api.registerTool) { + mcp.api.registerTool({ + name: 'my_plugin_count_items', + description: '按类型统计 Zotero 库中的条目数量', + inputSchema: { + type: 'object', + properties: { + itemType: { type: 'string', description: '按条目类型过滤(如 journalArticle)' } + } + }, + handler: async (args) => { + const items = await Zotero.Items.getAll(Zotero.Libraries.userLibraryID); + const filtered = args.itemType + ? items.filter(i => i.itemType === args.itemType) + : items; + return { total: filtered.length, itemType: args.itemType || 'all' }; + }, + pluginID: 'my-plugin@example.com' + }); +} +``` + +### 卸载时清理 + +```javascript +// 在你的插件关闭钩子中: +const mcp = Zotero.ZoteroMCP; +if (mcp && mcp.api && mcp.api.unregisterAllTools) { + mcp.api.unregisterAllTools('my-plugin@example.com'); +} +``` + +### 注意事项 + +- 工具名称不能与内置工具冲突(见上方列表),建议使用前缀如 `my_plugin_*`。 +- 注册中心在 MCP 服务器重启(如端口变更)后仍然保留。服务器启动前注册的工具会在服务器启动后出现。 +- handler 中的异常会被捕获并以 MCP 错误响应返回,无需自行 try/catch。 +- 返回结果会自动 JSON 序列化并包装为 MCP `text` 内容。 + --- ## 🐛 常见问题 (FAQ) @@ -504,6 +641,75 @@ MCP 服务器已集成在插件内,位于 `src/modules/streamableMCPServer.ts` --- +## 🔍 对比:OpenAI Codex Zotero Skill vs. 本插件 + +v1.5.2 新增的 `sync_bib` 和 `cite` 工具灵感来自 [OpenAI Codex 的 Zotero 技能](https://github.com/openai/codex),后者提供了一个独立的 Python CLI 助手(`zotero.py`)来操作 Zotero 的本地 HTTP API。以下是两种实现方案的详细对比。 + +### 架构对比 + +| 维度 | OpenAI Codex Zotero Skill | 本插件(Zotero MCP) | +|---|---|---| +| **运行位置** | 外部 Python 脚本,由 Codex CLI 调用 | Zotero 插件进程内(TypeScript/JS) | +| **通信方式** | HTTP 请求到 Zotero 本地 API(`127.0.0.1:23119`)和连接器服务 | 直接访问 Zotero JavaScript API + BBT JSON-RPC | +| **协议** | CLI 子命令(`zotero.py sync-bib`、`zotero.py cite ...`) | MCP(模型上下文协议)over Streamable HTTP | +| **依赖** | 仅需 Python 3 标准库(无需 `pip install`) | Zotero 7 插件运行时、Better BibTeX 插件 | +| **文件 I/O** | Python `pathlib` / `os` | Mozilla `IOUtils`(Gecko/Firefox 运行时) | +| **BibTeX 导出** | Zotero 本地 API `?format=bibtex` 端点 | BBT JSON-RPC `item.export`(支持更多格式) | +| **引用键解析** | 从导出的 BibTeX 文本中用正则提取 | BBT `item.citationkey` JSON-RPC(规范、可靠) | + +### 功能对比 + +| 功能 | OpenAI Codex(`zotero.py`) | 本插件 | +|---|---|---| +| `sync-bib`(全库导出为 `.bib`) | ✅ `sync-bib --out references.bib` | ✅ `sync_bib` 工具,`bibPath` 参数 | +| `cite`(草稿中插入引用 + 同步 `.bib`) | ✅ `cite --query "..." --tex paper.tex --bib references.bib` | ✅ `cite` 工具,支持 `itemKey`/`query`、`texPath`/`markdownPath`、`marker` | +| `.bib` 文件去重检测 | ✅ 正则匹配 `@type{key,` | ✅ 正则匹配 `@type{key,` | +| 基于标记的引用替换 | ✅ `--marker ''` | ✅ `marker` 参数 | +| Markdown 引用格式 | ✅ `[@key]`(Pandoc) | ✅ `[@key]`(Pandoc) | +| LaTeX 引用格式 | ✅ `\cite{key}` | ✅ `\cite{key}` | +| 按关键词搜索(`itemKey` 的备选) | ✅ `--query` 通过本地 API `?q=` | ✅ `query` 通过进程内 `Zotero.Search` | +| 多种导出格式 | 仅 BibTeX(通过 API `?format=bibtex`) | BibTeX、BibLaTeX、CSL-JSON、CSL-YAML(通过 BBT) | +| 语义搜索 | ❌ | ✅ 内置向量嵌入搜索 | +| 全文搜索 | ❌ | ✅ 内置全文数据库 | +| 批注搜索 | ❌ | ✅ 按颜色/标签/关键词过滤 | +| 外部工具注册 | ❌ | ✅ 其他插件可注册 MCP 工具 | +| 文献库写入操作 | ✅ `import-bibtex`、`import-ris`(通过连接器) | ✅ `write_note`、`write_tag`、`write_metadata`、`write_item` | + +### 优缺点 + +**OpenAI Codex 方案:** +- ✅ 零安装:Python 3 标准库即可运行,无需 Zotero 插件 +- ✅ 兼容任何开启本地 API 的 Zotero 版本 +- ✅ 简单、可审计的 CLI 命令 +- ✅ 基本 BibTeX 导出不依赖 Better BibTeX +- ❌ 需要外部 Python 进程管理 +- ❌ 仅限于 Zotero 本地 HTTP API 接口(无法直接访问 JS API) +- ❌ 无语义搜索、批注搜索或全文数据库 +- ❌ 无法通过其他插件扩展自定义工具 +- ❌ 正则提取引用键较脆弱(依赖 BBT 输出格式) + +**本插件方案:** +- ✅ 进程内运行:无外部进程,延迟更低,API 访问更丰富 +- ✅ 完整 MCP 协议支持(Claude、Cherry Studio、Cursor 等 AI 客户端) +- ✅ 通过 BBT JSON-RPC 获取规范引用键(非正则解析) +- ✅ 支持多种导出格式(BibLaTeX、CSL-JSON、CSL-YAML),超越纯 BibTeX +- ✅ 可扩展:其他 Zotero 插件可注册额外的 MCP 工具 +- ✅ 集成语义搜索、全文搜索、批注分析 +- ✅ 文件 I/O 使用 Gecko 的 `IOUtils`(与 Zotero 安全模型一致) +- ❌ 需要安装 Zotero 7 插件 +- ❌ `sync_bib` 和 `cite` 依赖 Better BibTeX 插件(BBT 提供规范引用键) +- ❌ 需要插件生命周期管理(启用/禁用/重启) + +### 设计理念 + +OpenAI Codex 技能优先考虑**简洁与可移植性**:一个仅依赖标准库的 Python 脚本,任何 CLI 代理都能调用。它将 Zotero 视为可通过 HTTP 访问的黑盒。 + +本插件优先考虑**深度集成与可扩展性**:在 Zotero 进程内运行,可以访问完整的 JavaScript API、BBT 的 JSON-RPC,以及从其他插件注册自定义工具的能力。它将 Zotero 视为一个平台。 + +两种方案在用户可见的能力上趋同(`sync-bib`、`cite`),但根本区别在于智能的位置——在进程外部(OpenAI)还是在进程内部(本插件)。 + +--- + ## 🤝 贡献指南 欢迎贡献代码、报告问题或提出建议! diff --git a/README.md b/README.md index 45defcd..6e1da75 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ _This README is also available in: [:cn: 简体中文](./README-zh.md) | :gb: En [![zotero target version](https://img.shields.io/badge/Zotero-7-green?style=flat-square&logo=zotero&logoColor=CC2936)](https://www.zotero.org) [![Node.js](https://img.shields.io/badge/Node.js-18%2B-green)](https://nodejs.org) [![TypeScript](https://img.shields.io/badge/TypeScript-5.4-blue)](https://www.typescriptlang.org) -[![Version](https://img.shields.io/badge/Version-1.5.0-brightgreen)]() +[![Version](https://img.shields.io/badge/Version-1.5.2-brightgreen)]() [![EN doc](https://img.shields.io/badge/Document-English-blue.svg)](README.md) [![中文文档](https://img.shields.io/badge/文档-中文-blue.svg)](README-zh.md) @@ -26,9 +26,14 @@ The Zotero MCP server is a tool server based on the Model Context Protocol that - 🧠 **Semantic Search**: AI-powered concept matching via embedding vectors, discover related literature across languages - ✏️ **Write Operations**: Create notes, manage tags, update metadata, create new items and attach PDFs - 💾 **Full-text Database**: Access and search cached PDF full-text content +- 📑 **Citation Export**: Export BibLaTeX/BibTeX entries via Better BibTeX, generate CSL-formatted references and in-text citations, sync `.bib` files, and insert citations into LaTeX/Markdown drafts This enables AI assistants to help you with literature reviews, citation management, content analysis, annotation organization, knowledge base management, and more. +### Acknowledgment + +The semantic-search dialog UI and bundled `zotseek-favicon@0.5x.png` are adapted from [ZotSeek](https://github.com/introfini/ZotSeek) by José Fernandes and are used under the MIT License. + ## 🚀 Project Structure This project now features a **unified architecture** with an integrated MCP server: @@ -171,6 +176,7 @@ Example configuration for Claude Desktop: - Collection/item context menu for index management - **Write Operations**: Create/modify notes, manage tags, update metadata fields, create new items and reparent standalone PDFs - **Full-text Database**: Cached PDF full-text database with list, search, get, and stats operations +- **Citation & Bibliography Export**: Export BibLaTeX/BibTeX entries via Better BibTeX JSON-RPC, generate CSL-formatted references and in-text citations with customizable styles, list available citation styles - **Standalone Attachment Management**: Search and manage standalone PDF items without parent metadata - **Client Configuration Generator**: Automatically generates configuration for various AI clients - **Security**: Local-only operation ensuring complete data privacy @@ -196,7 +202,7 @@ Here are some screenshots demonstrating the functionality of Zotero MCP: ## 🔧 API Reference (MCP Tools) -The integrated MCP server provides **20 tools** in 5 categories: +The integrated MCP server provides **25 tools** in 6 categories: ### 1. Search & Query (7 tools) @@ -276,6 +282,178 @@ Update metadata fields on items (title, abstract, date, DOI, creators, etc.). Create new items or reparent existing attachments. - `action` (required: create/reparent), `itemType`, `fields`, `creators`, `tags`, `attachmentKeys`, `parentKey` +### 6. Citation & Bibliography Export (5 tools) + +#### `export_bibliography` +Export one or more Zotero items as BibLaTeX/BibTeX (or CSL-JSON/CSL-YAML) entries via the zotero-better-bibtex (BBT) plugin. Requires Better BibTeX to be installed and running. +- `itemKeys` (required: string[]), `format` (biblatex/bibtex/csljson/cslyaml), `libraryID`, `exportNotes`, `useJournalAbbreviation` + +#### `get_citation` +Generate a formatted reference (bibliography entry) or in-text citation for items using a CSL citation style. If no style is specified, uses the Zotero default Quick Copy style. Works without Better BibTeX. +- `itemKeys` (required: string[]), `style` (CSL style ID or title, e.g. "apa"/"ieee"), `contentType` (html/text), `mode` (bibliography/citation), `libraryID` + +#### `list_citation_styles` +List CSL citation styles available in Zotero (for use with `get_citation`). Each entry includes an id and title. Supports optional keyword filtering. +- `filter` (optional keyword to filter by title or id) + +#### `sync_bib` +Sync (export) the entire Zotero library to a `.bib` file on disk. Exports all top-level items as BibTeX (or BibLaTeX/CSL-JSON/CSL-YAML) via BBT and writes the result to the specified file path. Requires Better BibTeX. +- `bibPath` (required: absolute path to output `.bib` file), `format` (bibtex/biblatex/csljson/cslyaml, default: bibtex), `libraryID`, `includeChildren` (boolean, default: false) + +#### `cite` +Insert a citation from Zotero into a LaTeX (`.tex`) or Markdown draft, and keep the `.bib` file in sync. Finds a Zotero item by itemKey or search query, exports it as BibTeX, appends the entry to the specified `.bib` file (skips if the citation key already exists), then inserts `\cite{key}` (LaTeX) or `[@key]` (Markdown) into the draft file. Requires Better BibTeX. +- `bibPath` (required: path to `.bib` file), `itemKey` (Zotero item key) or `query` (search query, at least one required), `texPath` (LaTeX file) or `markdownPath` (Markdown file, at least one required), `marker` (placeholder to replace with citation, optional), `libraryID` + +--- + +## 🔌 External Tool Registration API + +Other Zotero plugins can register custom MCP tools that are exposed through this plugin's MCP server. This allows third-party plugins to extend the AI-accessible toolset without running their own HTTP server. + +### How It Works + +1. Your plugin calls `Zotero.ZoteroMCP.api.registerTool(...)` to register a tool. +2. The tool appears in `tools/list` alongside built-in tools. +3. When an AI client calls the tool, the MCP server dispatches the call to your handler. +4. On disable/uninstall, call `Zotero.ZoteroMCP.api.unregisterTool(...)` or `unregisterAllTools(pluginID)`. + +### API Methods + +| Method | Description | +|---|---| +| `registerTool(def)` | Register a custom MCP tool. Throws on invalid input or name collision. | +| `unregisterTool(name)` | Unregister a tool by name. Returns `true` if removed. | +| `unregisterAllTools(pluginID)` | Unregister all tools from a specific plugin. Returns count removed. | +| `getRegisteredTools()` | List registered tools (readonly, handler excluded). | +| `isToolRegistered(name)` | Check if a tool name is already registered. | +| `onToolListChanged(cb)` | Subscribe to tool list changes. Returns an unsubscribe function. | + +### Tool Definition + +```typescript +{ + name: string; // Unique, matches /^[a-z][a-z0-9_]*$/, must not collide with built-in tools + description: string; // Shown to AI clients + inputSchema: object; // JSON Schema for input parameters + handler: (args: any) => Promise | any; // Called when AI invokes the tool + pluginID?: string; // Optional: your plugin ID (for bulk cleanup) + enabled?: boolean; // Optional: visibility toggle (default: true) +} +``` + +### Example (from another plugin) + +```javascript +// Register a custom MCP tool from your Zotero plugin +const mcp = Zotero.ZoteroMCP; +if (mcp && mcp.api && mcp.api.registerTool) { + mcp.api.registerTool({ + name: 'my_plugin_count_items', + description: 'Count items in the Zotero library by type', + inputSchema: { + type: 'object', + properties: { + itemType: { type: 'string', description: 'Filter by item type (e.g. journalArticle)' } + } + }, + handler: async (args) => { + const items = await Zotero.Items.getAll(Zotero.Libraries.userLibraryID); + const filtered = args.itemType + ? items.filter(i => i.itemType === args.itemType) + : items; + return { total: filtered.length, itemType: args.itemType || 'all' }; + }, + pluginID: 'my-plugin@example.com' + }); +} +``` + +### Cleanup on Uninstall + +```javascript +// In your plugin's shutdown hook: +const mcp = Zotero.ZoteroMCP; +if (mcp && mcp.api && mcp.api.unregisterAllTools) { + mcp.api.unregisterAllTools('my-plugin@example.com'); +} +``` + +### Notes + +- Tool names must not collide with built-in tools (see list above). Use a prefix like `my_plugin_*`. +- The registry survives MCP server restarts (e.g. port changes). Tools registered before the server starts will appear once it does. +- Handler errors are caught and returned as MCP error responses — no need to wrap in try/catch. +- Results are JSON-serialised and wrapped in MCP `text` content automatically. + +--- + +## 🔍 Comparison: OpenAI Codex Zotero Skill vs. This Plugin + +The `sync_bib` and `cite` tools added in v1.5.2 are inspired by [OpenAI's Codex Zotero skill](https://github.com/openai/codex), which ships a standalone Python CLI helper (`zotero.py`) for operating Zotero's local HTTP API. Below is a detailed comparison of the two implementation approaches. + +### Architecture + +| Dimension | OpenAI Codex Zotero Skill | This Plugin (Zotero MCP) | +|---|---|---| +| **Runtime location** | External Python script invoked by the Codex CLI | In-process Zotero plugin (TypeScript/JS) | +| **Communication** | HTTP requests to Zotero's local API (`127.0.0.1:23119`) and connector server | Direct access to Zotero's JavaScript API + BBT JSON-RPC | +| **Protocol** | CLI subcommands (`zotero.py sync-bib`, `zotero.py cite ...`) | MCP (Model Context Protocol) over Streamable HTTP | +| **Dependencies** | Python 3 stdlib only (no `pip install` needed) | Zotero 7 plugin runtime, Better BibTeX plugin | +| **File I/O** | Python `pathlib` / `os` | Mozilla `IOUtils` (Gecko/Firefox runtime) | +| **BibTeX export** | Zotero local API `?format=bibtex` endpoint | BBT JSON-RPC `item.export` (richer format support) | +| **Citation key resolution** | Extracted from exported BibTeX text via regex | BBT `item.citationkey` JSON-RPC (canonical, reliable) | + +### Feature Comparison + +| Feature | OpenAI Codex (`zotero.py`) | This Plugin | +|---|---|---| +| `sync-bib` (full library export to `.bib`) | ✅ `sync-bib --out references.bib` | ✅ `sync_bib` tool with `bibPath` param | +| `cite` (insert citation into draft + sync `.bib`) | ✅ `cite --query "..." --tex paper.tex --bib references.bib` | ✅ `cite` tool with `itemKey`/`query`, `texPath`/`markdownPath`, `marker` | +| Duplicate detection in `.bib` | ✅ Regex match on `@type{key,` | ✅ Regex match on `@type{key,` | +| Marker-based citation replacement | ✅ `--marker ''` | ✅ `marker` param | +| Markdown citation format | ✅ `[@key]` (Pandoc) | ✅ `[@key]` (Pandoc) | +| LaTeX citation format | ✅ `\cite{key}` | ✅ `\cite{key}` | +| Search by query (fallback to `itemKey`) | ✅ `--query` via local API `?q=` | ✅ `query` via in-process `Zotero.Search` | +| Multiple export formats | BibTeX only (via API `?format=bibtex`) | BibTeX, BibLaTeX, CSL-JSON, CSL-YAML (via BBT) | +| Semantic search | ❌ | ✅ Built-in embedding-based search | +| Full-text search | ❌ | ✅ Built-in full-text database | +| Annotation search | ❌ | ✅ Color/tag/keyword filtering | +| External tool registration | ❌ | ✅ Other plugins can register MCP tools | +| Library write operations | ✅ `import-bibtex`, `import-ris` via connector | ✅ `write_note`, `write_tag`, `write_metadata`, `write_item` | + +### Pros & Cons + +**OpenAI Codex approach:** +- ✅ Zero-installation: runs with Python 3 stdlib, no Zotero plugin needed +- ✅ Works with any Zotero version that exposes the local API +- ✅ Simple, auditable CLI commands +- ✅ No dependency on Better BibTeX for basic BibTeX export +- ❌ Requires external Python process management +- ❌ Limited to Zotero's local HTTP API surface (no direct JS API access) +- ❌ No semantic search, annotation search, or full-text database +- ❌ Cannot extend with custom tools from other plugins +- ❌ Citation key extraction via regex is fragile (depends on BBT output format) + +**This plugin approach:** +- ✅ In-process: no external process, lower latency, richer API access +- ✅ Full MCP protocol support for AI clients (Claude, Cherry Studio, Cursor, etc.) +- ✅ Canonical citation key resolution via BBT JSON-RPC (not regex parsing) +- ✅ Multiple export formats (BibLaTeX, CSL-JSON, CSL-YAML) beyond plain BibTeX +- ✅ Extensible: other Zotero plugins can register additional MCP tools +- ✅ Integrated with semantic search, full-text search, annotation analysis +- ✅ File I/O via Gecko's `IOUtils` (same security model as Zotero itself) +- ❌ Requires Zotero 7 plugin installation +- ❌ Requires Better BibTeX plugin for `sync_bib` and `cite` (BBT provides canonical citekeys) +- ❌ Plugin lifecycle management (enable/disable/restart) + +### Design Philosophy + +The OpenAI Codex skill prioritizes **simplicity and portability**: a single stdlib-only Python script that any CLI agent can invoke. It treats Zotero as a black box accessible via HTTP. + +This plugin prioritizes **deep integration and extensibility**: running inside Zotero's process gives it access to the full JavaScript API, BBT's JSON-RPC, and the ability to register custom tools from other plugins. It treats Zotero as a platform. + +Both approaches converge on the same user-facing capabilities (`sync-bib`, `cite`) but differ fundamentally in where the intelligence lives — outside the process (OpenAI) or inside it (this plugin). + --- ## 🤝 Contributing diff --git a/zotero-mcp-plugin/addon/content/icons/icon-toolbar.svg b/zotero-mcp-plugin/addon/content/icons/icon-toolbar.svg new file mode 100644 index 0000000..cf2fbd3 --- /dev/null +++ b/zotero-mcp-plugin/addon/content/icons/icon-toolbar.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/zotero-mcp-plugin/addon/content/icons/zotseek-favicon@0.5x.png b/zotero-mcp-plugin/addon/content/icons/zotseek-favicon@0.5x.png new file mode 100644 index 0000000..7daf3ab Binary files /dev/null and b/zotero-mcp-plugin/addon/content/icons/zotseek-favicon@0.5x.png differ diff --git a/zotero-mcp-plugin/addon/content/preferences.xhtml b/zotero-mcp-plugin/addon/content/preferences.xhtml index 814363e..a836aac 100644 --- a/zotero-mcp-plugin/addon/content/preferences.xhtml +++ b/zotero-mcp-plugin/addon/content/preferences.xhtml @@ -20,6 +20,7 @@ + @@ -308,6 +309,20 @@ + + + + + + + + + + + diff --git a/zotero-mcp-plugin/addon/content/searchDialog.css b/zotero-mcp-plugin/addon/content/searchDialog.css new file mode 100644 index 0000000..4418c4d --- /dev/null +++ b/zotero-mcp-plugin/addon/content/searchDialog.css @@ -0,0 +1,689 @@ +/* Semantic search dialog styles adapted from ZotSeek (https://github.com/introfini/ZotSeek), MIT License. */ + +#zotero-mcp-semantic-search-window { + min-width: 600px; + min-height: 400px; +} + +#search-area { + background-color: var(--material-background); + border-bottom: 1px solid var(--material-border-quarternary); +} + +#search-box { + font-size: 14px; + padding: 6px 10px; + border: 1px solid var(--material-border-quarternary); + border-radius: 4px; + background: var(--material-sidepane); +} + +#search-box:focus { + border-color: var(--accent-blue); + outline: none; +} + +#search-button { + padding: 6px 16px; + background: var(--accent-blue); + color: white; + border: none; + border-radius: 4px; + font-weight: 500; + cursor: pointer; +} + +#search-button:hover { + background: var(--accent-blue-darkest); +} + +#progress-box { + padding: 15px; + background: var(--material-mix-quinary); + border-radius: 4px; + margin: 10px; +} + +#results-count { + padding: 10px 15px; + font-size: 14px; + font-weight: 600; + color: var(--fill-secondary); +} + +#results-list { + background: var(--material-sidepane); + border: none; + padding: 5px; +} + +.search-result-item { + padding: 10px; + margin: 5px; + background: var(--material-background); + border: 1px solid var(--material-border-quarternary); + border-radius: 4px; + cursor: pointer; + transition: all 0.2s; +} + +.search-result-item:hover { + background: var(--material-mix-quinary); + border-color: var(--material-border-secondary); +} + +.search-result-item[selected="true"] { + background: var(--accent-blue-10); + border-color: var(--accent-blue); +} + +.result-title { + font-size: 14px; + font-weight: 600; + color: var(--fill-primary); +} + +.result-similarity { + font-size: 13px; + font-weight: 600; + padding: 2px 8px; + border-radius: 12px; + background: var(--material-mix-quinary); +} + +.result-creators { + font-size: 13px; + color: var(--fill-secondary); + margin-top: 4px; +} + +.result-year { + font-size: 12px; + color: var(--fill-tertiary); + margin-top: 2px; +} + +/* VirtualizedTable Styles */ +#zotseek-results-container { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + font-size: 13px; +} + +#zotseek-results-container .virtualized-table { + height: 100%; + width: 100%; +} + +/* Only apply border to the actual header element, not its children */ +#zotseek-results-container .tree-header, +#similar-documents-results-container .tree-header { + background: #f5f5f5; + border-bottom: 1px solid #ddd !important; + border-top: none !important; + font-weight: 600; + height: 32px; + min-height: 32px; + max-height: 32px; + display: flex; + align-items: center; + flex-shrink: 0; /* CRITICAL: Don't let header shrink */ + overflow: hidden; /* Official pattern: no scroll on header */ + box-sizing: border-box; +} + +/* Ensure no borders on child elements */ +#zotseek-results-container .tree-header * { + border: none !important; +} + +#zotseek-results-container .virtualized-table-header .row, +#zotseek-results-container thead .row { + font-weight: 600; + background: transparent; + border: none !important; + padding: 8px 12px; +} + +#zotseek-results-container .virtualized-table-header .cell, +#zotseek-results-container thead .cell { + font-weight: 600; + color: #424242; +} + +#zotseek-results-container .tree-children .row, +#zotseek-results-container .row:not(.tree-header .row) { + display: flex; + align-items: center; + min-height: 28px; + height: 28px; + padding: 4px 12px; + border-bottom: 1px solid #eee; + border-top: none !important; + cursor: pointer; + position: relative; +} + +#zotseek-results-container .row:hover { + background: #f0f8ff !important; + z-index: 1; +} + +#zotseek-results-container .row.selected { + background: #d1e7fd !important; + border-bottom-color: #b8d4f0 !important; +} + +#zotseek-results-container .row.focused { + outline: 2px solid #2196F3; + outline-offset: -2px; + z-index: 2; +} + +#zotseek-results-container .cell { + padding: 0 8px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* Column-specific styles */ +#zotseek-results-container .cell[data-key="similarity"], +#zotseek-results-container [data-key="similarity"] { + font-weight: 600; + color: #1976D2; +} + +#zotseek-results-container .cell[data-key="title"], +#zotseek-results-container [data-key="title"] { + font-weight: 500; + color: #212121; +} + +#zotseek-results-container .cell[data-key="authors"], +#zotseek-results-container [data-key="authors"] { + color: #616161; +} + +#zotseek-results-container .cell[data-key="year"], +#zotseek-results-container [data-key="year"] { + color: #757575; + text-align: center; +} + +#zotseek-results-container .cell[data-key="source"], +#zotseek-results-container [data-key="source"] { + color: #9E9E9E; + font-size: 12px; +} + +/* Virtualized table body - let parent container handle scrolling */ +#zotseek-results-container .virtualized-table-body, +#similar-documents-results-container .virtualized-table-body { + flex: 1 0; + max-width: 100%; + min-height: 0; +} + +/* VirtualizedTable React component fills container and uses flex layout */ +#zotseek-results-container .virtualized-table, +#similar-documents-results-container .virtualized-table { + display: flex; + flex-direction: column; + height: 100% !important; + min-height: 0 !important; + max-height: 100% !important; + overflow: hidden; +} + +/* Tree view container - CRITICAL: bounded height for scrolling to work */ +#zotseek-results-container .tree-view, +#similar-documents-results-container .tree-view { + display: flex !important; + flex-direction: column !important; + flex: 1 1 auto !important; + min-height: 0 !important; + max-height: 100% !important; + overflow: hidden !important; + height: 100% !important; +} + +/* Tree children - this is where scrolling actually happens */ +#zotseek-results-container .tree-children, +#similar-documents-results-container .tree-children { + flex: 1 1 auto; + min-height: 0; + position: relative; + overflow-y: auto !important; + overflow-x: hidden; +} + +#zotseek-results-container tr { + display: flex; + width: 100%; + align-items: center; + min-height: 28px; + height: 28px; +} + +#zotseek-results-container td, +#zotseek-results-container .cell { + padding: 2px 8px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + display: flex; + align-items: center; + height: 100%; +} + +/* Virtualized table container - XUL-compatible bounded height approach */ +.virtualized-table-container { + border: 1px solid #ddd; + background: white; + overflow: hidden; + width: 100%; + min-height: 150px; + -moz-box-flex: 1; + box-sizing: border-box; + position: relative; +} + +/* Override Zotero's default tree-view borders */ +.virtualized-table-container .tree-view { + border: none !important; +} + +.virtualized-table-container .tree-view .tree-header { + border-top: none !important; + border-left: none !important; + border-right: none !important; +} + +/* Results container uses absolute positioning to get bounded height in XUL */ +#zotseek-results-container, +#similar-documents-results-container { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + overflow: hidden; + box-sizing: border-box; +} + +/* The VirtualizedTable React component - ensure it fills the container */ +#zotseek-results-container > div, +#similar-documents-results-container > div { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; +} + +/* Fix virtualized table row heights */ +#zotseek-results-container .virtualized-table .row, +#zotseek-results-container [role="treeitem"] { + min-height: 28px !important; + height: 28px !important; + box-sizing: border-box; +} + +/* Remove double border between header and first row */ +#zotseek-results-container .virtualized-table .row:first-child, +#zotseek-results-container [role="treeitem"]:first-child, +#zotseek-results-container tbody tr:first-child, +#zotseek-results-container .tree-children > :first-child { + border-top: none !important; +} + +#zotseek-results-container .tree-children .row:first-of-type { + border-top: none !important; +} + +/* Ensure hover states work on all row elements */ +#zotseek-results-container .virtualized-table .row:hover, +#zotseek-results-container [role="treeitem"]:hover, +#zotseek-results-container tr:hover { + background: #f0f8ff !important; +} + +/* Selected state for all row types */ +#zotseek-results-container .virtualized-table .row.selected, +#zotseek-results-container [role="treeitem"][aria-selected="true"], +#zotseek-results-container tr.selected { + background: #d1e7fd !important; + border-bottom-color: #b8d4f0 !important; +} + +/* Text color on selected rows - ensure good contrast */ +#zotseek-results-container .row.selected .cell, +#zotseek-results-container [role="treeitem"][aria-selected="true"] .cell, +#zotseek-results-container tr.selected .cell { + color: #1a1a1a !important; +} + +/* Keep similarity column distinctive but readable */ +#zotseek-results-container .row.selected .cell[data-key="similarity"], +#zotseek-results-container .row.selected [data-key="similarity"] { + color: #0d47a1 !important; +} + +/* Multi-Query Styles */ +#search-queries-container { + margin-bottom: 8px; +} + +.query-row { + margin-bottom: 4px; +} + +.query-label { + min-width: 55px; + margin-right: 8px; +} + +.zotseek-query-input { + flex: 1; + padding: 6px; + font-size: 14px; + border: 1px solid #ccc; + border-radius: 4px; +} + +#query-operator-row { + padding: 4px 0; +} + +#query-combine-operator { + font-weight: 600; +} + +.query-action-btn { + min-width: 28px; + margin-left: 4px; + padding: 4px 8px; +} + +#zotseek-add-query-btn:hover { + background: #e8f5e9; +} + +#zotseek-remove-query-2-btn:hover { + background: #ffebee; +} + +#search-mode-row { + padding: 4px 0; +} + +#search-mode-select { + background: var(--material-background, #fff); + border: 1px solid var(--material-border-quarternary, #ccc); + border-radius: 4px; + padding: 4px 8px; + font-size: 13px; +} + +#search-mode-hint { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; +} + +/* Source Indicator Column */ +#zotseek-results-container .cell[data-key="indicator"], +#zotseek-results-container [data-key="indicator"] { + font-size: 14px; + text-align: center; + padding: 0 4px; + min-width: 24px; +} + +#zotseek-results-container .cell[data-key="indicator"][title] { + cursor: help; +} + +/* Highlight rows found by both searches */ +#zotseek-results-container .row[data-source="both"] { + background: rgba(76, 175, 80, 0.08); +} + +#zotseek-results-container .row[data-source="both"]:hover { + background: rgba(76, 175, 80, 0.15) !important; +} + +#zotseek-results-container .row[data-source="both"].selected { + background: rgba(76, 175, 80, 0.25) !important; +} + +/* Column Header Sorting Styles */ +#zotseek-results-container .tree-header .cell, +#similar-documents-results-container .tree-header .cell { + cursor: pointer; + user-select: none; + position: relative; + padding-right: 18px; +} + +#zotseek-results-container .tree-header .cell[data-key="indicator"], +#zotseek-results-container .tree-header .cell[data-key="page"] { + cursor: default; +} + +#zotseek-results-container .tree-header .cell:hover:not([data-key="indicator"]):not([data-key="page"]), +#similar-documents-results-container .tree-header .cell:hover:not([data-key="indicator"]):not([data-key="page"]) { + background: rgba(0, 0, 0, 0.05); +} + +/* Sort direction indicator (arrow) */ +#zotseek-results-container .tree-header .cell.sorted-ascending::after, +#similar-documents-results-container .tree-header .cell.sorted-ascending::after { + content: '\25B2'; + position: absolute; + right: 4px; + top: 50%; + transform: translateY(-50%); + font-size: 8px; + color: #1976D2; +} + +#zotseek-results-container .tree-header .cell.sorted-descending::after, +#similar-documents-results-container .tree-header .cell.sorted-descending::after { + content: '\25BC'; + position: absolute; + right: 4px; + top: 50%; + transform: translateY(-50%); + font-size: 8px; + color: #1976D2; +} + +/* Dark mode adjustments */ +@media (prefers-color-scheme: dark) { + #search-box { + background: var(--material-background); + color: var(--fill-primary); + } + + .search-result-item { + background: var(--material-button); + } + + .search-result-item:hover { + background: var(--material-button-hover); + } + + #zotseek-results-container .virtualized-table-header { + background: #2a2a2a; + border-bottom-color: #444; + } + + #zotseek-results-container .row { + border-bottom-color: #333; + } + + #zotseek-results-container .row:hover, + #zotseek-results-container .virtualized-table .row:hover, + #zotseek-results-container [role="treeitem"]:hover { + background: #3a3a3a !important; + } + + #zotseek-results-container .row.selected, + #zotseek-results-container .virtualized-table .row.selected, + #zotseek-results-container [role="treeitem"][aria-selected="true"] { + background: #1e3a5f !important; + border-bottom-color: #2a4a6f !important; + } + + #zotseek-results-container .row.selected .cell, + #zotseek-results-container [role="treeitem"][aria-selected="true"] .cell { + color: #ffffff !important; + } + + #zotseek-results-container .row.selected .cell[data-key="similarity"], + #zotseek-results-container .row.selected [data-key="similarity"] { + color: #90caf9 !important; + } + + #zotseek-results-container .cell[data-key="title"] { + color: #e0e0e0; + } + + #zotseek-results-container .cell[data-key="authors"] { + color: #b0b0b0; + } + + #search-mode-select { + background: var(--material-background, #2a2a2a); + border-color: #444; + color: #e0e0e0; + } + + #search-mode-hint { + color: #888; + } + + #zotseek-results-container .row[data-source="both"] { + background: rgba(76, 175, 80, 0.12); + } + + #zotseek-results-container .row[data-source="both"]:hover { + background: rgba(76, 175, 80, 0.2) !important; + } + + #zotseek-results-container .row[data-source="both"].selected { + background: rgba(76, 175, 80, 0.3) !important; + } + + #zotseek-results-container .tree-header .cell:hover:not([data-key="indicator"]):not([data-key="page"]), + #similar-documents-results-container .tree-header .cell:hover:not([data-key="indicator"]):not([data-key="page"]) { + background: rgba(255, 255, 255, 0.08); + } + + #zotseek-results-container .tree-header .cell.sorted-ascending::after, + #zotseek-results-container .tree-header .cell.sorted-descending::after, + #similar-documents-results-container .tree-header .cell.sorted-ascending::after, + #similar-documents-results-container .tree-header .cell.sorted-descending::after { + color: #64B5F6; + } + + .zotseek-query-input { + background: var(--material-background, #2a2a2a); + border-color: #444; + color: #e0e0e0; + } + + #query-operator-hint { + color: #888; + } + + #zotseek-add-query-btn:hover { + background: rgba(76, 175, 80, 0.2); + } + + #zotseek-remove-query-2-btn:hover { + background: rgba(244, 67, 54, 0.2); + } +} + +/* Snippet hover tooltip (issue #36): shows the matched chunk text on row hover */ +.zotseek-snippet-tooltip { + position: fixed; + z-index: 9999; + max-width: 380px; + box-sizing: border-box; + padding: 9px 11px; + border: 1px solid var(--fill-quinary, #d8dade); + border-radius: 7px; + background: var(--material-toolbar, #ffffff); + color: var(--fill-primary, #1a1a1a); + box-shadow: 0 6px 22px rgba(0, 0, 0, 0.18); + font-size: 12.5px; + line-height: 1.5; + pointer-events: none; + opacity: 0; + transform: translateY(4px); + transition: opacity 0.09s ease, transform 0.09s ease; +} + +.zotseek-snippet-tooltip.show { + opacity: 1; + transform: translateY(0); +} + +.zotseek-snippet-tooltip .zotseek-snippet-meta { + margin-bottom: 5px; + font-size: 11px; + color: var(--fill-secondary, #6b7280); + white-space: nowrap; +} + +.zotseek-snippet-tooltip .zotseek-snippet-meta .sep { + margin: 0 6px; + opacity: 0.6; +} + +.zotseek-snippet-tooltip .zotseek-snippet-body { + border-left: 3px solid var(--color-accent, #2f6fed); + padding-left: 9px; +} + +.zotseek-snippet-tooltip mark { + background: #fff3bf; + color: #7a5c00; + border-radius: 2px; + padding: 0 1px; +} + +@media (prefers-color-scheme: dark) { + .zotseek-snippet-tooltip { + border-color: #3a3d42; + background: var(--material-toolbar, #2a2c30); + color: #e6e6e6; + box-shadow: 0 6px 22px rgba(0, 0, 0, 0.45); + } + + .zotseek-snippet-tooltip .zotseek-snippet-meta { + color: #9aa0a6; + } + + .zotseek-snippet-tooltip mark { + background: #5a4a12; + color: #ffe48a; + } +} + +/* Match Type Statistics Bar (our addition) */ +#zotseek-match-stats { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + font-size: 12px; + border: 1px solid var(--material-border-quarternary, #e0e0e0); +} + +#zotseek-match-stats span { + display: inline-block; +} + +@media (prefers-color-scheme: dark) { + #zotseek-match-stats { + border-color: #444; + } +} diff --git a/zotero-mcp-plugin/addon/content/searchDialog.xhtml b/zotero-mcp-plugin/addon/content/searchDialog.xhtml new file mode 100644 index 0000000..3042535 --- /dev/null +++ b/zotero-mcp-plugin/addon/content/searchDialog.xhtml @@ -0,0 +1,1041 @@ + + + + + + + + + + + + + + + + + + + +