unpaywall_get_fulltext_links
Retrieve open-access PDF links and metadata for academic papers by providing a DOI, enabling access to scholarly literature for research.
Instructions
Given a DOI, return best open-access links (best PDF URL and open URL) plus Unpaywall locations metadata.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| doi | Yes | DOI string or DOI URL | |
| No | Email to identify your requests to Unpaywall (optional override) |
Implementation Reference
- src/index.ts:246-274 (handler)Executes the unpaywall_get_fulltext_links tool: validates args, normalizes DOI, fetches Unpaywall data, extracts best PDF/open URLs and metadata.if (tool === TOOL_GET_FULLTEXT_LINKS) { const args = (req.params.arguments ?? {}) as Partial<GetByDoiArgs>; const rawDoi = (args.doi ?? "").toString().trim(); if (!rawDoi) { return { content: [{ type: "text", text: "Missing required argument: 'doi'" }], isError: true }; } const email = (args.email || process.env.UNPAYWALL_EMAIL || "").toString().trim(); if (!email) { return { content: [{ type: "text", text: "Unpaywall requires an email. Set UNPAYWALL_EMAIL or pass 'email'." }], isError: true }; } const doi = normalizeDoi(rawDoi); const obj = await fetchUnpaywallByDoi(doi, email); const best = obj?.best_oa_location ?? null; const locations: any[] = Array.isArray(obj?.oa_locations) ? obj.oa_locations : []; const pickPdfFrom = (locs: any[]) => locs.find(l => l?.url_for_pdf) || locs.find(l => l?.url); const bestPdfUrl = best?.url_for_pdf || best?.url || (pickPdfFrom(locations)?.url_for_pdf || pickPdfFrom(locations)?.url) || null; const bestOpenUrl = best?.url || (locations.find(l => l?.url)?.url) || null; const result = { doi: obj?.doi ?? doi, title: obj?.title ?? null, is_oa: obj?.is_oa ?? null, oa_status: obj?.oa_status ?? null, best_pdf_url: bestPdfUrl, best_open_url: bestOpenUrl, best_oa_location: best, oa_locations: locations, }; return { content: [{ type: "json", json: result }] }; }
- src/index.ts:165-177 (schema)Input schema definition for the unpaywall_get_fulltext_links tool, registered in ListTools response.{ name: TOOL_GET_FULLTEXT_LINKS, description: "Given a DOI, return best open-access links (best PDF URL and open URL) plus Unpaywall locations metadata.", inputSchema: { type: "object", properties: { doi: { type: "string", description: "DOI string or DOI URL" }, email: { type: "string", description: "Email to identify your requests to Unpaywall (optional override)" }, }, required: ["doi"], additionalProperties: false, }, },
- src/index.ts:133-195 (registration)Registration of all tools including unpaywall_get_fulltext_links in the ListToolsRequestHandler.server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: TOOL_GET_BY_DOI, description: "Fetch Unpaywall metadata for a DOI (accepts DOI, DOI URL, or 'doi:' prefix). Requires an email address via env UNPAYWALL_EMAIL or the optional 'email' argument.", inputSchema: { type: "object", properties: { doi: { type: "string", description: "DOI string or DOI URL, e.g. 10.1038/nphys1170 or https://doi.org/10.1038/nphys1170" }, email: { type: "string", description: "Email to identify your requests to Unpaywall (optional override)" }, }, required: ["doi"], additionalProperties: false, }, }, { name: TOOL_SEARCH_TITLES, description: "Search Unpaywall for article titles matching a query. Supports optional is_oa filter and pagination (50 results per page).", inputSchema: { type: "object", properties: { query: { type: "string", description: "Title search query (supports phrase, boolean operators per Unpaywall docs)" }, is_oa: { type: "boolean", description: "If true, only return OA results; if false, only closed; omit for all" }, page: { type: "integer", minimum: 1, description: "Page number (50 results per page)" }, email: { type: "string", description: "Email to identify your requests to Unpaywall (optional override)" }, }, required: ["query"], additionalProperties: false, }, }, { name: TOOL_GET_FULLTEXT_LINKS, description: "Given a DOI, return best open-access links (best PDF URL and open URL) plus Unpaywall locations metadata.", inputSchema: { type: "object", properties: { doi: { type: "string", description: "DOI string or DOI URL" }, email: { type: "string", description: "Email to identify your requests to Unpaywall (optional override)" }, }, required: ["doi"], additionalProperties: false, }, }, { name: TOOL_FETCH_PDF_TEXT, description: "Download and extract text from best OA PDF for a DOI, or from a provided PDF URL.", inputSchema: { type: "object", properties: { doi: { type: "string", description: "DOI string or DOI URL. Used if pdf_url is not provided." }, pdf_url: { type: "string", description: "Direct PDF URL to download and parse (takes precedence over DOI)." }, email: { type: "string", description: "Email to identify requests to Unpaywall (required when resolving via DOI)." }, truncate_chars: { type: "integer", minimum: 1000, description: "Max characters of extracted text to return (default 20000)." }, }, required: [], additionalProperties: false, }, }, ], }; });
- src/index.ts:47-61 (helper)Helper function to fetch Unpaywall metadata by DOI, used by the tool handler.async function fetchUnpaywallByDoi(doi: string, email: string) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 20_000); try { const url = `https://api.unpaywall.org/v2/${encodeURIComponent(doi)}?email=${encodeURIComponent(email)}`; const resp = await fetch(url, { signal: controller.signal, headers: { "Accept": "application/json" } }); if (!resp.ok) { const text = await resp.text().catch(() => ""); throw new Error(`Unpaywall HTTP ${resp.status}: ${text.slice(0, 400)}`); } return await resp.json(); } finally { clearTimeout(timeout); } }
- src/index.ts:38-45 (helper)Helper function to normalize DOI input by stripping prefixes.function normalizeDoi(input: string): string { let doi = input.trim(); // Strip common DOI URL prefixes doi = doi.replace(/^https?:\/\/(dx\.)?doi\.org\//i, ""); // Strip leading 'doi:' prefix doi = doi.replace(/^doi:/i, ""); return doi.trim(); }