<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Rui Bai</title>
    <description>The latest articles on DEV Community by Rui Bai (@ruicbai).</description>
    <link>https://dev.to/ruicbai</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4219210%2Fb1260adb-80ed-4f51-932a-b54e761bbe19.png</url>
      <title>DEV Community: Rui Bai</title>
      <link>https://dev.to/ruicbai</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ruicbai"/>
    <language>en</language>
    <item>
      <title>Building a Zotero AI plugin with a built-in Zotero MCP server — and making every LLM citation clickable back to the exact PDF passage</title>
      <dc:creator>Rui Bai</dc:creator>
      <pubDate>Sat, 31 Oct 2026 14:57:03 +0000</pubDate>
      <link>https://dev.to/ruicbai/building-a-zotero-ai-plugin-with-a-built-in-zotero-mcp-server-and-making-every-llm-citation-h3l</link>
      <guid>https://dev.to/ruicbai/building-a-zotero-ai-plugin-with-a-built-in-zotero-mcp-server-and-making-every-llm-citation-h3l</guid>
      <description>&lt;p&gt;&lt;em&gt;Disclosure: we're the team of researchers behind &lt;a href="https://bibgenie.com" rel="noopener noreferrer"&gt;BibGenie&lt;/a&gt;, the Zotero plugin described here. This post is about the engineering, not a product tour — including the parts that don't work as well as we'd like.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We built BibGenie because we kept hitting the same wall with the new wave of Zotero AI tools: the answer reads fine, but checking it costs more time than reading the paper would have. Two design problems came out of that frustration, and they turned out to be the same problem — &lt;strong&gt;an AI that works on your library should be inspectable&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Its claims should be one click away from the evidence.&lt;/li&gt;
&lt;li&gt;Its access to your library should be a protocol you control, not a black box.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The first led us to text-block citations. The second led us to ship an MCP server inside the plugin. Here's how both work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 1: A Zotero MCP server that lives inside the plugin
&lt;/h2&gt;

&lt;p&gt;Most Zotero MCP setups you'll find are separate Python services that talk to Zotero's local API. That works, but it means users install and babysit a second runtime, keep it version-matched, and debug two programs when something breaks. We wanted the server to be a feature of the plugin, not a sidecar.&lt;/p&gt;

&lt;p&gt;BibGenie's MCP server runs inside the Zotero plugin, on top of Zotero's own local HTTP server, using &lt;strong&gt;Streamable HTTP&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://127.0.0.1:23119/bibgenie/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(The port follows your Zotero HTTP server config — the settings panel shows the exact URL to copy, and there's a built-in &lt;em&gt;Test Connection&lt;/em&gt; button, because opening the URL in a browser sends a GET and tells you nothing. MCP clients speak POST + JSON-RPC.)&lt;/p&gt;

&lt;p&gt;Clients that support Streamable HTTP connect directly. Cursor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ZoteroBibGenie"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://127.0.0.1:23119/bibgenie/mcp"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude Code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add ZoteroBibGenie &lt;span class="nt"&gt;--transport&lt;/span&gt; http http://127.0.0.1:23119/bibgenie/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One wrinkle worth knowing: some Claude Desktop builds are happier with stdio servers than local HTTP endpoints, so for Claude Desktop we point users at &lt;code&gt;mcp-remote&lt;/code&gt; as a small bridge. Not elegant, but honest — and it works today.&lt;/p&gt;

&lt;h3&gt;
  
  
  The tool surface
&lt;/h3&gt;

&lt;p&gt;We kept the tools close to what researchers actually do in Zotero, rather than exposing a generic database API:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Item search&lt;/strong&gt; by title, author, publication, keyword, or topic&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Semantic search&lt;/strong&gt; over the library (more on what that index contains below)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PDF reading&lt;/strong&gt; — text extraction from attachments, plus &lt;em&gt;visual&lt;/em&gt; reading that renders pages as images when figures, tables, or scanned pages need eyes, not a text layer&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Notes and snapshots&lt;/strong&gt; — read Zotero notes and saved web snapshots&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tags and collections&lt;/strong&gt; — list, create, and manage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Metadata access and review&lt;/strong&gt; — including a read-only &lt;code&gt;review_metadata&lt;/code&gt; tool that aggregates facts from arXiv, Semantic Scholar, OpenAlex, DBLP, and OpenReview with rule-based diagnostics, so an external agent can &lt;em&gt;audit&lt;/em&gt; a record before touching it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Controlled writes&lt;/strong&gt; — create notes, and validate/apply metadata edits through &lt;code&gt;edit_metadata&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The uncomfortable part: write access
&lt;/h3&gt;

&lt;p&gt;Read tools are easy to feel good about. Write tools are where we spent the arguments.&lt;/p&gt;

&lt;p&gt;Inside BibGenie's own chat, any proposed library change is shown as a preview/diff first and only written after the user approves. External MCP clients, however, have their own permission models — and they don't necessarily show the same approval cards before writing to your library. We can't fix another client's UX from inside our server, so the documentation is blunt about it: check your client's approval settings before letting it edit your library, only share the server URL with clients you trust, and turn the server off when you're not using it (it's off by default; you enable it explicitly in settings).&lt;/p&gt;

&lt;p&gt;There's a second leak to be honest about: the MCP connection is local, but the &lt;em&gt;client&lt;/em&gt; may send whatever it retrieved to its own model provider. "Local server" does not mean "nothing leaves the machine" once an external agent is in the loop. We say this in the docs, in roughly these words, because users consistently assume otherwise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 2: Citations that carry their own coordinates
&lt;/h2&gt;

&lt;p&gt;The second half of "inspectable" is the citation itself. A citation that names a paper is almost useless for verification — the paper is 14 pages. A citation that names a page is better. What you actually want is: &lt;em&gt;click, land on the passage, see it highlighted, read the surrounding paragraph.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So in BibGenie, a citation isn't a string. It's a reference plus &lt;strong&gt;location information&lt;/strong&gt;, and we think in three levels:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Level&lt;/th&gt;
&lt;th&gt;What it identifies&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Work&lt;/td&gt;
&lt;td&gt;Which paper or source was used&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Page&lt;/td&gt;
&lt;td&gt;Which page the evidence is on&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Passage / text block&lt;/td&gt;
&lt;td&gt;The relevant part of the source, with a jump and highlight when its position resolves&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The source type determines what "location" can mean:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PDF text&lt;/strong&gt; — opens the cited page; when valid text-position data exists, it also locates and highlights the relevant text block&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PDF page image&lt;/strong&gt; — returns to the source page (an exact figure or table region is &lt;em&gt;not&lt;/em&gt; necessarily highlighted — anyone who tells you otherwise hasn't shipped this)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EPUB&lt;/strong&gt; — returns to the relevant section or text location, with a temporary highlight when the passage resolves&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Web snapshot&lt;/strong&gt; — opens the saved snapshot and locates/highlights the passage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Existing annotation&lt;/strong&gt; — navigates to the annotation you already made&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Note&lt;/strong&gt; — opens the source note&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The highlight is temporary, by design: following a citation is a reading act, not an editing act. It doesn't silently create annotations in your library.&lt;/p&gt;

&lt;h3&gt;
  
  
  Design for degradation, not for the demo
&lt;/h3&gt;

&lt;p&gt;The most important engineering decision here was what to do when the coordinates &lt;em&gt;don't&lt;/em&gt; resolve — because with real-world PDFs, they often don't. Scanned PDFs, poor text extraction, unusual EPUB structures, and messy snapshot markup all limit positioning.&lt;/p&gt;

&lt;p&gt;Our rule: &lt;strong&gt;fall back a level, never fake precision.&lt;/strong&gt; If the text block can't be resolved, the citation opens the document at the page or section. If that fails, it opens the document. A citation that confidently jumps to the &lt;em&gt;wrong&lt;/em&gt; paragraph is worse than one that honestly opens the right paper, because the wrong jump manufactures trust.&lt;/p&gt;

&lt;p&gt;The same honesty applies one level up. A real source does not guarantee a correct interpretation — a model can cite a genuine passage and still summarize it wrong, merge two findings, or drop the qualification that came one sentence later. Traceable citations don't solve that. What they change is the &lt;em&gt;cost of catching it&lt;/em&gt;: verification goes from "re-read the paper" to "read the paragraph and its neighbours," which is the difference between checking every claim and checking none. We're also careful to keep the author's statements, an annotator's comments, and the AI's interpretation distinguishable — a comment attached to a passage is not part of the paper.&lt;/p&gt;

&lt;p&gt;One more boundary we keep: chat citations are for reading and research notes. For a manuscript, you cite the original work through Zotero's normal citation workflow in your required style. A highlighted text block is evidence, not a bibliography entry.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 3: What "local-first" actually commits you to
&lt;/h2&gt;

&lt;p&gt;Both features above only make sense because of where the data lives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Chat history is stored in the user's local Zotero database.&lt;/li&gt;
&lt;li&gt;The semantic index is built locally, over titles, abstracts, and metadata — notably &lt;em&gt;not&lt;/em&gt; a cloud index of your PDF full text.&lt;/li&gt;
&lt;li&gt;Bring-your-own-key requests go from the user's machine directly to their chosen provider.&lt;/li&gt;
&lt;li&gt;With local chat and embedding models (Ollama / LM Studio), the whole loop can run without leaving the machine.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The hosted route exists too (that's how most users start), but the architecture treats it as the exception, not the default — and the MCP/citation design doesn't change depending on which route you use, because both run against the same local library.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we'd tell another team building this
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Ship the server inside the host app if you can.&lt;/strong&gt; Every external runtime you ask researchers to install halves your audience and doubles your support matrix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make citations data, not decoration.&lt;/strong&gt; If the location isn't captured when the answer is generated, you can't retrofit it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Degrade honestly.&lt;/strong&gt; Fall back a level instead of pretending precision you don't have — in tooling for researchers, manufactured confidence is the one unforgivable bug.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat write access as a separate product decision.&lt;/strong&gt; Read tools, review tools, and write tools deserve different defaults, different warnings, and different levels of user friction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Say the quiet part in the docs.&lt;/strong&gt; Local server ≠ nothing leaves the machine; real source ≠ correct interpretation. Users forgive limitations. They don't forgive discovering them.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you're building MCP tooling for reference managers — or for any personal knowledge base, really — we'd genuinely like to compare notes. The interesting problems (position resolution across formats, write-safety for external agents, what an index should and shouldn't contain) are nowhere near solved.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;BibGenie works with Zotero 7–10 on macOS, Windows, and Linux. Docs: &lt;a href="https://bibgenie.com/docs/mcp" rel="noopener noreferrer"&gt;MCP server&lt;/a&gt; · &lt;a href="https://bibgenie.com/docs/bibgenie-citations" rel="noopener noreferrer"&gt;Traceable citations&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>zotero</category>
      <category>mcp</category>
      <category>ai</category>
      <category>llm</category>
    </item>
  </channel>
</rss>
