UNPKG

paper-search-mcp-nodejs

Version:

A Node.js MCP server for searching and downloading academic papers from multiple sources, including arXiv, PubMed, bioRxiv, Web of Science, and more.

147 lines 6.22 kB
/** * Wiley TDM (Text and Data Mining) API - PDF Download Only * * Documentation: https://onlinelibrary.wiley.com/library-info/resources/text-and-datamining * GitHub Client: https://github.com/WileyLabs/tdm-client * * IMPORTANT: Wiley TDM API does NOT support keyword search. * It only supports downloading PDFs by DOI. * For searching Wiley content, use Crossref API with publisher filter. * * API Endpoint: https://api.wiley.com/onlinelibrary/tdm/v1/articles/{DOI} * Header: Wiley-TDM-Client-Token: <token> * * Rate limits: * - Up to 3 articles per second * - Up to 60 requests per 10 minutes (build in 10 second delay between requests) */ import axios from 'axios'; import { PaperSource } from './PaperSource.js'; import { PaperFactory } from '../models/Paper.js'; import { RateLimiter } from '../utils/RateLimiter.js'; import { sanitizeDoi } from '../utils/SecurityUtils.js'; import { TIMEOUTS } from '../config/constants.js'; export class WileySearcher extends PaperSource { client; rateLimiter; constructor(tdmToken) { super('wiley', 'https://api.wiley.com/onlinelibrary/tdm/v1', tdmToken); this.client = axios.create({ baseURL: 'https://api.wiley.com/onlinelibrary/tdm/v1', headers: { 'Accept': 'application/pdf', ...(tdmToken ? { 'Wiley-TDM-Client-Token': tdmToken } : {}) }, maxRedirects: 5, timeout: TIMEOUTS.EXTENDED }); // Wiley rate limits: 3 articles/sec, 60 requests/10min this.rateLimiter = new RateLimiter({ requestsPerSecond: 0.1, // Conservative: ~6 per minute burstCapacity: 3 }); } /** * Search is NOT supported by Wiley TDM API. * Use Crossref API to search for Wiley articles, then use download() to get PDFs. */ async search(query, options = {}) { throw new Error('Wiley TDM API does not support keyword search. ' + 'Use Crossref API (search_crossref) to find Wiley articles by DOI, ' + 'then use download_paper with platform="wiley" to download PDFs.'); } /** * Download PDF by DOI using Wiley TDM API * @param doi - The DOI of the article (e.g., "10.1111/jtsb.12390") * @param options - Download options including savePath */ async downloadPdf(doi, options = {}) { if (!this.apiKey) { throw new Error('Wiley TDM token is required. Set WILEY_TDM_TOKEN environment variable.'); } // Clean and validate DOI format const doiResult = sanitizeDoi(doi); if (!doiResult.valid) { throw new Error(`Invalid DOI format: ${doi}. ${doiResult.error || ''}`); } const cleanDoi = doiResult.sanitized; const fs = await import('fs'); const path = await import('path'); const savePath = options.savePath || './downloads'; if (!fs.existsSync(savePath)) { fs.mkdirSync(savePath, { recursive: true }); } // Encode DOI for URL (replace / with %2F) const encodedDoi = encodeURIComponent(cleanDoi); const url = `/articles/${encodedDoi}`; await this.rateLimiter.waitForPermission(); try { const response = await this.client.get(url, { responseType: 'stream', headers: { 'Wiley-TDM-Client-Token': this.apiKey, 'Accept': 'application/pdf' } }); // Generate filename from DOI const fileName = `${cleanDoi.replace(/[\/\\:*?"<>|]/g, '_')}.pdf`; const filePath = path.join(savePath, fileName); const writer = fs.createWriteStream(filePath); response.data.pipe(writer); return new Promise((resolve, reject) => { writer.on('finish', () => resolve(filePath)); writer.on('error', reject); }); } catch (error) { const status = error.response?.status; const errorMessages = { 400: 'No TDM Client Token was found in the request', 403: 'TDM Client Token is invalid or not registered', 404: 'Access denied - you or your institution does not have access to this content. Check your subscription.', 429: 'Rate limit exceeded. Please reduce request frequency (max 60 requests per 10 minutes).' }; if (status && errorMessages[status]) { throw new Error(`Wiley TDM Error (${status}): ${errorMessages[status]}`); } throw new Error(`Failed to download PDF: ${error.message}`); } } /** * Get article metadata and download link (without downloading) */ async getArticleInfo(doi) { // Clean and validate DOI const doiResult = sanitizeDoi(doi); const cleanDoi = doiResult.valid ? doiResult.sanitized : doi; // Since TDM API only provides PDF download, we create basic paper info from DOI return PaperFactory.create({ paperId: cleanDoi, title: `Wiley Article: ${cleanDoi}`, authors: [], abstract: '', doi: cleanDoi, publishedDate: null, pdfUrl: `https://api.wiley.com/onlinelibrary/tdm/v1/articles/${encodeURIComponent(cleanDoi)}`, url: `https://doi.org/${cleanDoi}`, source: 'wiley', extra: { note: 'Use Crossref API for full metadata. This endpoint only provides PDF download.' } }); } getCapabilities() { return { search: false, // TDM API does not support search download: true, // PDF download by DOI fullText: true, // Full PDF available citations: false, requiresApiKey: true, supportedOptions: [] // No search options - only DOI-based download }; } async readPaper(paperId, options = {}) { return 'Wiley TDM API only supports PDF download. Use downloadPdf() method with a DOI to get the full PDF.'; } } //# sourceMappingURL=WileySearcher.js.map