rubien-mcp-server
v0.3.6
Published
MCP server wrapping rubien-cli so Claude Code and claude.ai can manage the Rubien reference library.
Maintainers
Readme
rubien-mcp-server
A Model Context Protocol server that wraps rubien-cli, exposing your Rubien library to Claude Code, Claude Desktop, and claude.ai (web) as a tool catalog. It spawns rubien-cli under the hood and speaks MCP over two transports:
- stdio — Claude Code and Claude Desktop. The client spawns the server locally. No network, no auth. What most people want; see Install.
- Streamable HTTP + bearer token — claude.ai (web) connects as a remote MCP server (it can't spawn a local process), so it needs a tunnel. See claude.ai web.
Requirement: rubien-cli on the host
The server is only a wrapper. It needs Node.js ≥ 20 and the rubien-cli binary:
- Mac: install Rubien.app (the DMG). The entitled
rubien-cliships inside it and is found automatically. - Linux: install the runtime libs and extract the CLI tarball, keeping
rubien-cliand its*.resourcesfolders together (the CLI loads citation styles from beside the binary):
Tarball: https://github.com/devzhk/Rubien/releases. Don't put the bare binary onsudo apt install libsqlite3-0 libcurl4 libxml2 libpoppler-glib8 libcairo2 libgdk-pixbuf-2.0-0 libglib2.0-0 ca-certificates # download rubien-cli-*-linux-x86_64.tar.gz, extract, then: export RUBIEN_CLI=/path/to/extracted/rubien-cliPATHalone — it loses its resource bundles.
The server checks the CLI's build (MIN_CLI_BUILD). In stdio mode it starts either way: if the CLI is missing or too old, every tool call returns the update instruction as its result, re-checked per call — so updating Rubien mid-session recovers on the next call, with no client restart. (Versions ≤ 0.3.0 exited at startup instead, which Claude Desktop rendered as an opaque "Server disconnected" — see Troubleshooting.) In --http mode the server still exits at startup, where stderr is visible in your terminal. Update via Rubien.app / Sparkle on Mac, or rubien-cli self-update on Linux.
Install
Both stdio clients use the published npm package via npx -y — no global install.
Claude Code
claude mcp add rubien -- npx -y rubien-mcp-serverClaude Desktop
Settings → Developer → Edit Config (or edit ~/Library/Application Support/Claude/claude_desktop_config.json), then restart Claude Desktop:
{
"mcpServers": {
"rubien": {
"command": "npx",
"args": ["-y", "rubien-mcp-server"]
}
}
}macOS PATH gotcha: Claude Desktop launches servers with launchd's minimal PATH, not your shell's. If npx isn't found, set "command" to its absolute path (which npx → e.g. /opt/homebrew/bin/npx). Affects every npx-based server; Claude Code is unaffected. No .dxt one-click package yet.
Upgrading
The npx -y rubien-mcp-server install is unpinned, so there is nothing to reinstall — npx resolves the latest published version each time the client spawns the server. Upgrade in this order:
- Update Rubien first — Sparkle on Mac (Rubien → Settings → Check Now),
rubien-cli self-updateon Linux. The server needs the newrubien-cli. - Restart your MCP client — quit/reopen Claude Desktop, or start a new Claude Code session. The respawned server is the latest release.
The pairing is guarded in one direction: a new server against a too-old Rubien refuses every tool call with an update instruction (MIN_CLI_BUILD), so a half-upgrade fails loudly instead of half-working. The other direction is why upgrading matters — an old server against a new Rubien breaks at tool-call time when it shells subcommands that no longer exist (server < 0.3.0 vs Rubien ≥ 0.4.0: the 0.4.0 CLI cutover removed import, add --identifier, views --query, and the legacy properties write flags that 0.1.x/0.2.x servers shell out to; those server versions are deprecated on npm for this reason).
If a stale npx cache ever keeps serving an old version after a restart, clear it (rm -rf ~/.npm/_npx) or pin rubien-mcp-server@latest in the config. HTTP mode (below) runs as a long-lived process you started yourself — it does not auto-upgrade; restart it after updating Rubien.
Troubleshooting: "Server disconnected" in Claude Desktop
Claude Desktop never shows an MCP server's stderr — the actual error is in ~/Library/Logs/Claude/mcp-server-rubien.log. Two known causes, both typically surfacing right after a reboot, because Claude Desktop only respawns MCP servers when the app itself relaunches:
- Stale Rubien vs auto-upgraded server. The unpinned
npx -yinstall picks up new server releases at respawn; if Rubien.app hasn't been updated, the log showsneeds build >= N. Fix: open Rubien.app and let Sparkle update it. Server ≥ 0.3.1 no longer disconnects for this — the instruction is returned to Claude on each tool call instead — but older servers exited at startup with only this log line as evidence. - No network at spawn.
npxresolves the package version against the npm registry on every spawn, even with a warm cache. If Claude Desktop launches at login before Wi-Fi/VPN is up, the spawn fails (ENOTFOUND/ECONNREFUSED registry.npmjs.orgin the log) and Claude Desktop does not retry. Fix: quit and reopen Claude Desktop once online. To remove the network dependency entirely, install globally and point the config at the absolute path (npm install -g rubien-mcp-server, thenwhich rubien-mcp-server) — at the cost of upgrading by hand withnpm update -g rubien-mcp-server.
claude.ai web (remote MCP server)
claude.ai connects to rubien as a remote MCP server over Streamable HTTP — it can't spawn a local process. It's the same npm package in HTTP mode: the server still runs on your Mac (where the library and rubien-cli live), and claude.ai reaches it through a Cloudflare Tunnel, with a bearer token to keep strangers out.
# 1. Start the server in HTTP mode with a bearer token
RUBIEN_MCP_BEARER=$(openssl rand -hex 32) \
npx -y rubien-mcp-server --http --port 4000 --bearer-token "$RUBIEN_MCP_BEARER"
# 2. Open a tunnel
cloudflared tunnel --url http://localhost:4000
# → copy the https://*.trycloudflare.com URL
# 3. claude.ai → Settings → Connectors → Add custom MCP connector
# URL: the tunnel URL; Token: $RUBIEN_MCP_BEARERSave the token — you need it to reconnect after restarts.
Security model
The remote path is single-user personal use only.
- The bearer token is a long-lived static secret — no rotation, revocation, rate limiting, or replay protection. Treat it like an SSH key; regenerate if leaked.
- Cloudflare Tunnel terminates TLS, so the token travels over HTTPS. A plaintext tunnel (e.g. raw ngrok HTTP) exposes it to sniffing.
- A leaked URL without the token is safe (every request gets a 401) — but rotate both if in doubt.
Multi-user would need a real auth story: OAuth, short-lived tokens, per-client revocation.
Optional configuration
Pin the library directory
By default the CLI resolves its own library location. To force one, set RUBIEN_LIBRARY_ROOT to the directory containing library.sqlite:
- Claude Code:
claude mcp add rubien --env RUBIEN_LIBRARY_ROOT=<dir> -- npx -y rubien-mcp-server - Claude Desktop: add
"env": { "RUBIEN_LIBRARY_ROOT": "<dir>" }besidecommand/args.
| Host | Directory |
|---|---|
| Mac (sandboxed app) | ~/Library/Group Containers/9TXK4V3SS8.com.rubien.shared/Rubien |
| Mac (unsandboxed dev) | ~/Library/Application Support/Rubien |
| Linux | ~/.local/share/rubien (or $XDG_DATA_HOME/rubien) |
How the server finds rubien-cli
First hit wins:
$RUBIEN_CLI(explicit path)/Applications/Rubien.app/Contents/Helpers/rubien-cli(Mac)~/Applications/Rubien.app/Contents/Helpers/rubien-cli(Mac)./build/Rubien.app/Contents/Helpers/rubien-cli(dev build, Mac)rubien-clionPATH(Linux installs land here)
On Mac, prefer the bundled helper (2–4): it's signed with the App Group entitlement and reads the same library.sqlite as the app. A bare rubien-cli on PATH is usually an unentitled dev build hitting a different database (see "Database location" in Docs/CLI-Reference.md).
On Linux, everything works except rubien_get_sync_status (no CloudKit — the sync subcommand isn't built).
Run a local checkout
For development, point the client at a built dist/ (see Development):
claude mcp add rubien -- node $(pwd)/dist/index.js # Claude Code{ "mcpServers": { "rubien": { "command": "node", "args": ["/abs/path/to/mcp-server/dist/index.js"] } } }Tool catalog
36 tools — CRUD operations always carry the target suffix (create_reference, update_property); a bare verb appears only when the operation is unique and unambiguous (cite, export):
| Surface | Tools |
|---|---|
| References | rubien_search_references, rubien_list_references, rubien_get_reference, rubien_create_reference, rubien_update_reference, rubien_delete_reference |
| Citations | rubien_cite, rubien_list_styles |
| Export | rubien_export |
| Reading | rubien_read_text, rubien_read_annotations, rubien_grep_text |
| Activity | rubien_reading_activity |
| Attachments | rubien_attachment_list, rubien_attachment_add, rubien_attachment_status, rubien_attachment_retry, rubien_attachment_read, rubien_attachment_export, rubien_attachment_rename, rubien_attachment_remove |
| PDFs | rubien_get_pdf_info, rubien_render_pdf_page, rubien_download_pdf |
| Properties (columns + options, incl. Tags) | rubien_list_properties, rubien_create_property, rubien_update_property, rubien_delete_property, rubien_create_option, rubien_update_option, rubien_delete_option |
| Saved views | rubien_list_views, rubien_create_view, rubien_update_view, rubien_delete_view |
| Sync | rubien_get_sync_status (Mac-only — errors on Linux) |
rubien_create_reference is the one door in: pass exactly one of source (any locator — DOI / arXiv / PMID / PMCID / ISBN, paper URL, PDF/Markdown file URL, absolute file path, or folder path; the CLI routes it), bibtex, or title. Resolver sources attempt an open-access PDF fetch by default; pass downloadPdf: false to opt out. The reference remains saved if the fetch is unavailable or fails. It may return multiple items (multi-entry BibTeX, folders) and status: "existing" when dedup matched, in the unified {items, summary, diagnostics} envelope. Stdin ("-") is intentionally unavailable through MCP.
Per-reference property values (cells) are edited through rubien_update_reference's properties payload — {"Status": "Reading", "Tags": {"add": ["12"]}, "7": ["ml", "nlp"], "Themes": null} — atomically alongside metadata fields. The option tools (create_option / update_option / delete_option) manage the choices themselves (including Tag rows for the built-in Tags property).
rubien_export accepts format: "json" | "bibtex" | "ris" and either
ids: [42, 57] (preserving that order) or view: 7. Omit both for the whole
library; supplying both is rejected. MCP never accepts an output path. Captured
output retains the server-wide 32 MiB limit; BibTeX/RIS use the existing
{format,text} result envelope.
Reading tools operate on any reference without your knowing whether it holds a
PDF or a clipped web page. rubien_read_text returns the readable body text,
routed automatically: pages/sections imply the PDF, start/format imply
the web body, and otherwise PDF wins when both exist. Every response reports
source (what was read) and available (which sources are readable now). PDF
results are page-keyed (pages[] items with text + sectionPath). Web reads
return compact Markdown by default; pass format: "html" to request the
canonical extracted HTML fragment when available. Web responses distinguish
the returned contentFormat from the canonical sourceFormat and paginate
with start/maxChars.
rubien_read_annotations merges the user's highlights, underlines, and
anchored notes across both kinds into one array, each item tagged source.
rubien_grep_text locates a phrase or regex in the same representation used by
rubien_read_text: PDF hits are grouped by page, while web hits carry exact
character offsets. Use the same format for a web grep and its follow-up read
so those offsets stay aligned. All three tools are library-only and never hit
the network.
PDF tools: rubien_get_pdf_info (page count, hasTextLayer, outline sections — call it before selecting rubien_read_text by sections), rubien_render_pdf_page (render a page to an image, for tables/figures/equations), rubien_download_pdf (fetch an open-access PDF and attach it to an existing reference).
Destructive tools carry destructiveHint: true so Claude Code's permission UI flags them. rubien_delete_reference passes --force; confirmation happens in the client UI, not a CLI prompt (see src/tools/references.ts).
Contract pinning
Argument shapes are zod schemas in each tool file; response shapes are in src/schemas.ts, mirroring the Swift *DTO types in Sources/RubienCLI/RubienCLI.swift. Convention (tested in test/schemas.test.ts):
.optional()for SwiftOptionalfields —JSONEncoderomits nil optionals..nullable()only forAlwaysEncodedOptional<T>(currently justDatabaseViewDTO.groupBy).
Change a Swift DTO → update src/schemas.ts in the same commit (CLAUDE.md CLI/data-layer lockstep rule).
Development
cd mcp-server
npm install
npm run build # → dist/
npm test # vitest
npm run dev # tsc --watchPoke at tool schemas by hand with MCP Inspector:
npx @modelcontextprotocol/inspector node dist/index.jsSupplementary attachments
The attachment tools manage local PDF and UTF-8 Markdown files belonging to a
reference. List/add use referenceId; status/retry/read/export/rename/remove use an
attachment UUID in id. Add takes a files array and reports added, duplicate,
or error for each input. Import limits are 250 MiB for PDFs and 50 MiB for Markdown.
Read uses PDF pages or Markdown start, plus maxChars (default 50000, maximum
500000). It reads the supplement itself, without substituting the parent paper.
Export writes original bytes to a new output path; rename changes only the display
name. Add/retry/export/rename/remove are write tools; remove is marked destructive.
Status includes verified local availability and the active session's transfer state.
pendingUpload is retained intent, not a successful upload. Retry resets transfer
backoff and repeats recovery lookups without recreating previously synced annotations.
Attachment sync is opt-in for Development verification; the default status remains
notEnabled. These tools require a CLI build
containing the attachment commands; see CLI reference.
