Claude Desktop (local MCP)

Claude Desktop can call tools on your machine via MCP. The setup below uses uv so you do not need a global Python venv — uv installs dependencies when the server starts.

veruscite_submit reads a file from the computer where uv / Claude Desktop runs. It cannot see Claude chat upload sandbox paths such as /mnt/user-data/uploads/…. Put the file on a real disk location (e.g. Downloads) and pass that absolute path.

1. Install uv

Skip if you already have uv on your PATH.

# macOS / Linux
curl -LsSf https://astral.sh/uv/install.sh | sh

# Windows (PowerShell)
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"

2. Save veruscite_mcp.py

Anywhere on disk is fine (for example C:\Users\You\veruscite_mcp.py or /home/you/veruscite_mcp.py).

"""Local MCP server for VerusCite. Set VERUSCITE_API_KEY in the environment."""
from __future__ import annotations

import os
import re
from pathlib import Path

import httpx
from mcp.server.fastmcp import FastMCP

API_ROOT = "https://veruscite-data.com"
KEY = os.environ["VERUSCITE_API_KEY"]
HEADERS = {"Authorization": f"Bearer {KEY}"}
mcp = FastMCP("veruscite")


def _client() -> httpx.Client:
    return httpx.Client(base_url=API_ROOT, headers=HEADERS, timeout=120.0)


def _resolve_local_file(file_path: str) -> Path | None:
    """Find a real local file from a path Claude may have mangled.

    Claude Desktop on Windows sometimes turns /mnt/... into C:\\mnt\\...
    We try a few normalizations; none of them invent paths that are not on disk.
    """
    raw = (file_path or "").strip().strip('"').strip("'")
    if not raw:
        return None

    candidates: list[str] = [raw]

    # C:\mnt\user-data\...  ->  /mnt/user-data/...
    m = re.match(r"^[A-Za-z]:[\\/](.+)$", raw)
    if m:
        unixish = "/" + m.group(1).replace("\\", "/")
        candidates.append(unixish)

    # /mnt/user-data/... already unix
    if "\\" in raw and not re.match(r"^[A-Za-z]:", raw):
        candidates.append(raw.replace("\\", "/"))

    seen: set[str] = set()
    for c in candidates:
        if c in seen:
            continue
        seen.add(c)
        try:
            p = Path(c).expanduser()
            # resolve() can fail on broken Windows→Unix hybrids; try both ways
            for try_p in (p, p.resolve(strict=False)):
                if try_p.is_file():
                    return try_p
        except OSError:
            continue
    return None


@mcp.tool()
def veruscite_me() -> dict:
    """Return account email and credit balance."""
    with _client() as c:
        r = c.get("/api/v1/me")
        r.raise_for_status()
        return r.json()


@mcp.tool()
def veruscite_submit(file_path: str) -> dict:
    """Upload a file from this computer for citation verification.

    file_path must be a real absolute path on the machine running this MCP
    server (where Claude Desktop / uv run). Examples:
      Windows: C:\\Users\\You\\Downloads\\paper.pdf
      WSL/Linux: /home/you/Documents/paper.pdf

    Do NOT use Claude chat sandbox paths like /mnt/user-data/uploads/... —
    those are not visible to this tool. Copy the file to Downloads (or similar)
    and pass that path instead.
    """
    path = _resolve_local_file(file_path)
    if path is None:
        return {
            "error": "file_not_found",
            "message": (
                "No readable file at that path on the computer running the "
                "VerusCite MCP server. Use a real local absolute path "
                "(e.g. C:\\\\Users\\\\You\\\\Downloads\\\\paper.pdf), not a "
                "Claude chat upload path like /mnt/user-data/uploads/..."
            ),
            "tried": file_path,
        }
    with path.open("rb") as f, _client() as c:
        r = c.post("/api/v1/documents", files={"file": (path.name, f)})
        r.raise_for_status()
        return r.json()


@mcp.tool()
def veruscite_status(document_id: int) -> dict:
    """Poll job status for a document id until finished is true."""
    with _client() as c:
        r = c.get(f"/api/v1/documents/{document_id}")
        r.raise_for_status()
        return r.json()


@mcp.tool()
def veruscite_results(document_id: int) -> dict:
    """Fetch citation verification results for a document."""
    with _client() as c:
        r = c.get(f"/api/v1/documents/{document_id}/results")
        r.raise_for_status()
        return r.json()


@mcp.tool()
def veruscite_jobs() -> dict:
    """List active (pending / in-process) jobs only."""
    with _client() as c:
        r = c.get("/api/v1/jobs")
        r.raise_for_status()
        return r.json()


@mcp.tool()
def veruscite_list_documents(limit: int = 50, status: str | None = None) -> dict:
    """List your documents (filenames, status, upload time, citation counts).

    Optional status filter: pending, in-process, completed, or failed.
    """
    params: dict = {"limit": min(max(limit, 1), 200)}
    if status:
        params["status"] = status
    with _client() as c:
        r = c.get("/api/v1/documents", params=params)
        r.raise_for_status()
        return r.json()


if __name__ == "__main__":
    mcp.run()

3. Claude Desktop config

macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
Windows: %APPDATA%\Claude\claude_desktop_config.json
Linux: ~/.config/Claude/claude_desktop_config.json

{
  "mcpServers": {
    "veruscite": {
      "command": "uv",
      "args": [
        "run",
        "--with", "mcp",
        "--with", "httpx",
        "...path...\\veruscite_mcp.py"
      ],
      "env": {
        "VERUSCITE_API_KEY": "vc_your_key_here"
      }
    }
  }
}

Put the full absolute path to your script in place of ...path...\veruscite_mcp.py (Linux/macOS style: /home/you/veruscite_mcp.py). On Windows, if Claude cannot find uv, set command to the full path of uv.exe. Restart Claude Desktop after saving.

4. How to ask Claude to submit a paper

Tell Claude a path that exists on your PC, for example:

Use VerusCite to submit C:\Users\You\Downloads\references.bib
and poll until finished.

If you only attached the file in the Claude chat, first save/copy it to Downloads (or another real folder), then give Claude that path.

5. Tools

  • veruscite_me — credits / account
  • veruscite_submit — upload from a local absolute path
  • veruscite_status / veruscite_results — one document
  • veruscite_jobs — active jobs only
  • veruscite_list_documents — list all your papers (filename, status, when uploaded, citation counts)