Proving Lab · Recipes
Short, complete instructions for the citation endpoint at
/mcp — in a terminal, in WSL, in Python, and in AI tools that
speak MCP. Every one of them was run on 3 August 2026 before it was written
down; an untested recipe is a claim.
For a page that is a work: authors, title, journal, year, volume, pages,
DOI, ISSN and licence — plus a ready-to-import RIS record
and a BibTeX entry. For a page that is a paywall, an error
or a bot check: complete: false and a warning naming the wall.
Test complete before you file the result — a
refused record still carries a title, and it will read like a work.
The recipe most people actually want. One URL per line in
reading-list.txt, one importable file out. Sources that cannot be
read are named on stderr and left out of the file rather than half-imported.
while read -r u; do
curl -sX POST https://provinglab.dev/mcp \
-H 'content-type: application/json' \
-d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/call\",
\"params\":{\"name\":\"extract_citation\",\"arguments\":{\"url\":\"$u\"}}}" \
| python3 -c 'import json,sys
d = json.loads(json.load(sys.stdin)["result"]["content"][0]["text"])
sys.stdout.write(d["ris"]) if d.get("complete") else \
sys.stderr.write("skipped: " + d.get("warning","") + "\n")'
done < reading-list.txt > literature.ris
Then Zotero → File → Import, or Citavi → Import → RIS. Measured on three scholarly URLs: three records, under two seconds, imported without editing.
claude mcp add --transport http provinglab https://provinglab.dev/mcp
claude mcp list then reports ✔ Connected. After that
you can simply say: "cite these four links for my bibliography" — the
tool is called for each one, and the ones behind a wall are reported as such
instead of invented.
Add https://provinglab.dev/mcp as a remote MCP
server (transport: streamable HTTP). Authentication is offered but not
required; anonymous requests get identical answers. In clients that only accept
local servers, the usual bridge works:
{
"mcpServers": {
"provinglab": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://provinglab.dev/mcp"]
}
}
}
The standard library identifies itself as Python-urllib, and the
CDN in front of this site answers that with HTTP 403 before the
worker ever sees the request. Any user agent of your own is enough. This is not
a rule against automation — it is a filter that does not know the difference.
import json, urllib.request
def cite(url):
body = json.dumps({"jsonrpc": "2.0", "id": 1, "method": "tools/call",
"params": {"name": "extract_citation", "arguments": {"url": url}}}).encode()
req = urllib.request.Request("https://provinglab.dev/mcp", body, {
"content-type": "application/json",
"user-agent": "my-bibliography-script/1.0", # <- without this: 403
})
answer = json.loads(urllib.request.urlopen(req, timeout=60).read())
return json.loads(answer["result"]["content"][0]["text"])
record = cite("https://doi.org/10.1038/s41586-020-2649-2")
if record.get("complete"):
print(record["ris"])
else:
print("not usable:", record["warning"])
The two halves of the work sit on different sides of the filesystem boundary. A source behind a university login can only be captured in the browser, and the file then has to be found from a shell.
In Full Page PDF Snap, switch on Copy file path after saving and set the format to WSL under Settings. After a capture the path is on the clipboard in the shape a Linux shell understands:
/mnt/c/Users/<you>/Downloads/Full Page PDF Snap/pubmed_2026-08-03_0911_0001.pdf
Paste it straight after a command, or into a chat with an AI tool that can read
files. The RIS record for the same capture sits next to the PDF with the same
name and a .ris extension.
| The endpoint | The extension | |
|---|---|---|
| Runs | on a server, anonymous | in your browser, logged in |
| Gives you | the reference | the reference and the document |
| Behind a login | no | yes |
| Cost per source | none, scriptable | one click |
| Output | RIS + BibTeX | PDF with the fields inside, plus RIS |
So the division is not a compromise: the endpoint for volume, the extension for the ones it refuses. Both emit the same RIS format, so everything lands in one Zotero or Citavi library regardless of the route. The refusal list from the first pass tells you which sources need the second.
These recipes are also published as a machine-readable skill, alongside the measurement methods:
A citation record says what a page declares about itself. It is not a check that the work exists, that the DOI resolves to it, or that the page is honest — for the eight of eighteen platforms where the data is thin, that matters. A screen capture, likewise, is a picture of a screen and not a qualified electronic document. Where the content decides something, read the source.