pm-export
Installation
SKILL.md
PubMed Export to Zotero
Export PubMed paper citation data and push to Zotero desktop via local Connector API, or generate MEDLINE/RIS format.
Arguments
$ARGUMENTS contains one or more PMIDs (space-separated), e.g.:
35641793— single paper35641793 27741350 35362092— batch export
Steps
Step 1: Fetch paper metadata (evaluate_script)
Use mcp__chrome-devtools__evaluate_script on any PubMed page. Replace PMID_LIST with the actual PMIDs:
async () => {
const pmids = "PMID_LIST".split(/[\s,]+/).filter(Boolean);
// Batch esummary for basic metadata
const sumResp = await fetch(
`https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esummary.fcgi?db=pubmed&id=${pmids.join(',')}&retmode=json`
);
const sumData = await sumResp.json();
// efetch XML for full records (abstracts, keywords)
const fetchResp = await fetch(
`https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=pubmed&id=${pmids.join(',')}&retmode=xml`
);
const xml = await fetchResp.text();
const xmlDoc = new DOMParser().parseFromString(xml, 'text/xml');
const papers = pmids.map(pmid => {
const r = sumData.result?.[pmid] || {};
const doi = (r.articleids || []).find(a => a.idtype === 'doi')?.value || '';
// Find this paper's article in the XML
const articles = xmlDoc.querySelectorAll('PubmedArticle');
let abstract = '', keywords = [], meshTerms = [], pubTypes = [];
let issn = '', pmcid = '';
for (const art of articles) {
if (art.querySelector('PMID')?.textContent === pmid) {
abstract = Array.from(art.querySelectorAll('AbstractText'))
.map(el => el.textContent).join(' ');
keywords = Array.from(art.querySelectorAll('Keyword'))
.map(k => k.textContent);
meshTerms = Array.from(art.querySelectorAll('MeshHeading DescriptorName'))
.map(d => d.textContent);
pubTypes = Array.from(art.querySelectorAll('PublicationType'))
.map(p => p.textContent);
issn = art.querySelector('ISSN')?.textContent || '';
pmcid = art.querySelector('ArticleId[IdType="pmc"]')?.textContent || '';
break;
}
}
return {
pmid,
title: r.title || '',
authors: (r.authors || []).map(a => ({
lastName: a.name?.split(' ')[0] || '',
firstName: a.name?.split(' ').slice(1).join(' ') || '',
})),
journal: r.fulljournalname || '',
journalAbbr: r.source || '',
pubdate: r.pubdate || '',
volume: r.volume || '',
issue: r.issue || '',
pages: r.pages || '',
doi,
issn,
pmcid,
abstract,
keywords,
pubtype: pubTypes,
language: (r.lang || ['eng'])[0] === 'eng' ? 'en' : (r.lang || ['eng'])[0]
};
});
return papers;
}
Step 2: Push to Zotero
Save the returned JSON array to a temp file, then run the Python script:
python "e:/pm-skills/.claude/skills/pm-export/scripts/push_to_zotero.py" /tmp/pm_papers.json
The Python script runs a 3-step flow:
- saveItems — saves metadata to Zotero (
POST /connector/saveItems). Note: Zotero 7.x ignores theattachmentsfield in saveItems. - download_pdf — downloads PDF binary via Python urllib (if
pdfUrlorpmcidis available) - saveAttachment — uploads PDF binary to Zotero (
POST /connector/saveAttachment), linked to the parent item viaparentItemIDin theX-Metadataheader
Other details:
- Builds Zotero-compatible item JSON (
itemType: journalArticle,libraryCatalog: PubMed) - Parses PubMed author format into Zotero's
{firstName, lastName}structure - Sets DOI, PMID (in extra field), URL to PubMed page
- Uses deterministic session ID for idempotency (same papers → same session → no duplicates)
- PDF download may fail for some publishers (403, JS-redirect pages); these are reported as "PDF skip"
Step 3: Report
Single paper:
Exported to Zotero:
Title: {title}
Authors: {authors}
Journal: {journal} ({pubdate})
DOI: {doi}
PMID: {pmid}
Batch:
Exported {count} papers to Zotero:
1. {title1} ({journal1}, {pubdate1})
2. {title2} ({journal2}, {pubdate2})
...
MEDLINE Export (alternative)
If the user wants a file instead of Zotero, use efetch to get MEDLINE format directly:
async () => {
const pmids = "PMID_LIST";
const resp = await fetch(
`https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=pubmed&id=${pmids}&rettype=medline&retmode=text`
);
return await resp.text();
}
Save the returned text as a .nbib file. This can be imported into Zotero, EndNote, or any reference manager.
Zotero Troubleshooting
- Zotero not running: Script returns "Zotero is not running". User must start Zotero desktop.
- Read-only library: Script returns "Target library is read-only". User must select a writable collection.
- Already saved: Script returns 409, treated as success (idempotent).
Notes
- This skill uses 2 tool calls:
evaluate_script+bash python - Batch export works for any number of PMIDs in a single call
- The Python script is at
e:/pm-skills/.claude/skills/pm-export/scripts/push_to_zotero.py - Zotero must be running with the Connector API on localhost:23119