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.
201 lines • 7.89 kB
JavaScript
/**
* ScienceDirect (Elsevier) Searcher
*
* Documentation: https://dev.elsevier.com/
* API Endpoints:
* - Search API: https://api.elsevier.com/content/search/sciencedirect
* - Article API: https://api.elsevier.com/content/article/doi/
*
* Required API Key: Yes (X-ELS-APIKey header)
* Get API key from: https://dev.elsevier.com/apikey/manage
*/
import axios from 'axios';
import { PaperSource } from './PaperSource.js';
import { PaperFactory } from '../models/Paper.js';
import { RateLimiter } from '../utils/RateLimiter.js';
export class ScienceDirectSearcher extends PaperSource {
client;
rateLimiter;
constructor(apiKey) {
super('sciencedirect', 'https://api.elsevier.com', apiKey);
this.client = axios.create({
baseURL: 'https://api.elsevier.com',
headers: {
'Accept': 'application/json',
...(apiKey ? { 'X-ELS-APIKey': apiKey } : {})
}
});
// Elsevier rate limits:
// - Without key: 20 requests per minute
// - With key: 10 requests per second (600 per minute)
const requestsPerSecond = apiKey ? 10 : 0.33;
this.rateLimiter = new RateLimiter({
requestsPerSecond,
burstCapacity: apiKey ? 20 : 5
});
}
async search(query, options = {}) {
const customOptions = options;
if (!this.apiKey) {
throw new Error('ScienceDirect API key is required');
}
const maxResults = options.maxResults || 10;
const papers = [];
try {
// Build search query
let searchQuery = query;
if (options.author) {
searchQuery += ` AND AUTHOR(${options.author})`;
}
if (options.journal) {
searchQuery += ` AND SRCTITLE(${options.journal})`;
}
if (options.year) {
if (options.year.includes('-')) {
const [startYear, endYear] = options.year.split('-');
searchQuery += ` AND PUBYEAR > ${parseInt(startYear) - 1}`;
if (endYear) {
searchQuery += ` AND PUBYEAR < ${parseInt(endYear) + 1}`;
}
}
else {
searchQuery += ` AND PUBYEAR = ${options.year}`;
}
}
if (customOptions.openAccess) {
searchQuery += ' AND OPENACCESS(1)';
}
await this.rateLimiter.waitForPermission();
const response = await this.client.get('/content/search/sciencedirect', {
params: {
query: searchQuery,
count: maxResults,
start: 0,
view: 'COMPLETE'
}
});
const entries = response.data['search-results']?.entry || [];
for (const entry of entries) {
const paper = await this.parseEntry(entry);
if (paper) {
papers.push(paper);
}
}
return papers;
}
catch (error) {
console.error('ScienceDirect search error:', error.message);
if (error.response?.status === 401) {
throw new Error('Invalid or missing ScienceDirect API key');
}
if (error.response?.status === 429) {
throw new Error('ScienceDirect rate limit exceeded. Please try again later.');
}
throw error;
}
}
async parseEntry(entry) {
try {
// Extract PDF URL if available
let pdfUrl;
if (entry.link) {
const pdfLink = entry.link.find(l => l['@type'] === 'application/pdf');
if (pdfLink) {
pdfUrl = pdfLink['@href'];
}
}
// Build paper URL
const paperUrl = entry['prism:url'] ||
(entry['prism:doi'] ? `https://doi.org/${entry['prism:doi']}` : undefined);
return PaperFactory.create({
paperId: entry['prism:doi'] || entry['dc:identifier'] || entry.pii || '',
title: entry['dc:title'] || '',
authors: entry['dc:creator'] ? [entry['dc:creator']] : [],
abstract: entry['dc:description'] || '',
doi: entry['prism:doi'],
publishedDate: entry['prism:coverDate'] ? new Date(entry['prism:coverDate']) : null,
pdfUrl: pdfUrl,
url: paperUrl,
source: 'ScienceDirect',
journal: entry['prism:publicationName'],
volume: entry['prism:volume'],
issue: entry['prism:issueIdentifier'],
pages: entry['prism:pageRange'],
extra: {
pii: entry.pii,
openAccess: entry.openaccess || entry.openaccessFlag || false
}
});
}
catch (error) {
console.error('Error parsing ScienceDirect entry:', error);
return null;
}
}
async getArticleDetails(doi) {
if (!this.apiKey) {
throw new Error('ScienceDirect API key is required');
}
try {
await this.rateLimiter.waitForPermission();
const response = await this.client.get(`/content/article/doi/${doi}`, {
params: {
view: 'FULL'
}
});
const coredata = response.data['full-text-retrieval-response']?.coredata;
if (!coredata)
return null;
// Extract authors
let authors = '';
if (coredata['dc:creator']) {
if (Array.isArray(coredata['dc:creator'])) {
authors = coredata['dc:creator'].map((a) => a.$).join(', ');
}
else if (typeof coredata['dc:creator'] === 'string') {
authors = coredata['dc:creator'];
}
}
return PaperFactory.create({
paperId: doi,
title: coredata['dc:title'] || '',
authors: authors ? [authors] : [],
abstract: coredata['dc:description'] || '',
doi: coredata['prism:doi'] || doi,
publishedDate: coredata['prism:coverDate'] ? new Date(coredata['prism:coverDate']) : null,
url: `https://doi.org/${doi}`,
source: 'ScienceDirect',
journal: coredata['prism:publicationName'],
volume: coredata['prism:volume'],
issue: coredata['prism:issueIdentifier'],
pages: coredata['prism:pageRange'],
citationCount: coredata['citedby-count'] ? parseInt(coredata['citedby-count']) : undefined
});
}
catch (error) {
console.error('ScienceDirect article details error:', error.message);
return null;
}
}
getCapabilities() {
return {
search: true,
download: false, // Requires institutional access
fullText: false,
citations: true,
requiresApiKey: true,
supportedOptions: ['maxResults', 'year', 'author', 'journal']
};
}
async downloadPdf(paperId, options = {}) {
throw new Error('PDF download requires institutional access for ScienceDirect');
}
async readPaper(paperId, options = {}) {
const paper = await this.getArticleDetails(paperId);
if (!paper) {
throw new Error('Paper not found');
}
return paper.abstract || 'Abstract not available';
}
}
//# sourceMappingURL=ScienceDirectSearcher.js.map