@tan-yong-sheng/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.
247 lines • 10.4 kB
JavaScript
/**
* Scopus (Elsevier) Searcher
*
* Documentation: https://dev.elsevier.com/documentation/SCOPUSSearchAPI.wadl
* API Endpoints:
* - Search API: https://api.elsevier.com/content/search/scopus
* - Abstract API: https://api.elsevier.com/content/abstract/scopus_id/
*
* Required API Key: Yes (X-ELS-APIKey header or apikey parameter)
* Get API key from: https://dev.elsevier.com/apikey/manage
*
* Scopus is the largest abstract and citation database of peer-reviewed literature
*/
import axios from 'axios';
import { PaperSource } from './PaperSource.js';
import { PaperFactory } from '../models/Paper.js';
import { RateLimiter } from '../utils/RateLimiter.js';
export class ScopusSearcher extends PaperSource {
client;
rateLimiter;
constructor(apiKey) {
super('scopus', 'https://api.elsevier.com', apiKey);
this.client = axios.create({
baseURL: 'https://api.elsevier.com',
headers: {
'Accept': 'application/json',
...(apiKey ? { 'X-ELS-APIKey': apiKey } : {})
}
});
// Scopus rate limits (same as Elsevier):
// - 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('Scopus API key is required');
}
const maxResults = Math.min(options.maxResults || 10, 25); // Scopus max is 25 per request
const papers = [];
try {
// Build Scopus search query
let searchQuery = `TITLE-ABS-KEY(${query})`;
if (options.author) {
searchQuery += ` AND AUTHOR(${options.author})`;
}
if (options.journal) {
searchQuery += ` AND SRCTITLE(${options.journal})`;
}
if (customOptions.affiliation) {
searchQuery += ` AND AFFIL(${customOptions.affiliation})`;
}
if (customOptions.subject) {
searchQuery += ` AND SUBJAREA(${customOptions.subject})`;
}
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)';
}
if (customOptions.documentType) {
const docTypeMap = {
'ar': 'Article',
'cp': 'Conference Paper',
're': 'Review',
'bk': 'Book',
'ch': 'Book Chapter'
};
searchQuery += ` AND DOCTYPE(${docTypeMap[customOptions.documentType]})`;
}
await this.rateLimiter.waitForPermission();
const response = await this.client.get('/content/search/scopus', {
params: {
query: searchQuery,
count: maxResults,
start: 0,
view: 'COMPLETE',
field: 'dc:identifier,dc:title,dc:creator,prism:publicationName,prism:coverDate,prism:doi,prism:url,prism:volume,prism:issueIdentifier,prism:pageRange,citedby-count,dc:description,authkeywords,author,affiliation,openaccess,eid'
}
});
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('Scopus search error:', error.message);
if (error.response?.status === 401) {
throw new Error('Invalid or missing Scopus API key');
}
if (error.response?.status === 429) {
throw new Error('Scopus rate limit exceeded. Please try again later.');
}
throw error;
}
}
async parseEntry(entry) {
try {
// Extract authors
let authors = '';
if (entry.author && entry.author.length > 0) {
authors = entry.author.map(a => a.authname).join(', ');
}
else if (entry['dc:creator']) {
authors = entry['dc:creator'];
}
// Extract affiliations
let affiliations = [];
if (entry.affiliation) {
affiliations = entry.affiliation.map(a => a.affilname);
}
// Build paper URL
const paperUrl = entry['prism:url'] ||
(entry['prism:doi'] ? `https://doi.org/${entry['prism:doi']}` : undefined);
// Extract keywords
const keywords = entry.authkeywords?.split(' | ') || [];
return PaperFactory.create({
paperId: entry.eid || entry['dc:identifier'] || '',
title: entry['dc:title'] || '',
authors: authors ? authors.split(', ') : [],
abstract: '', // Abstract not included in search results, need separate API call
doi: entry['prism:doi'],
publishedDate: entry['prism:coverDate'] ? new Date(entry['prism:coverDate']) : null,
url: paperUrl,
source: 'Scopus',
journal: entry['prism:publicationName'],
volume: entry['prism:volume'],
issue: entry['prism:issueIdentifier'],
pages: entry['prism:pageRange'],
citationCount: entry['citedby-count'] ? parseInt(entry['citedby-count']) : undefined,
keywords: keywords,
extra: {
scopusId: entry['dc:identifier'],
eid: entry.eid,
affiliations: affiliations,
documentType: entry.subtypeDescription,
issn: entry['prism:issn'],
eIssn: entry['prism:eIssn'],
openAccess: entry.openaccess === '1' || entry.openaccessFlag === true
}
});
}
catch (error) {
console.error('Error parsing Scopus entry:', error);
return null;
}
}
async getAbstract(scopusId) {
if (!this.apiKey) {
throw new Error('Scopus API key is required');
}
try {
await this.rateLimiter.waitForPermission();
const response = await this.client.get(`/content/abstract/scopus_id/${scopusId}`, {
params: {
view: 'FULL'
}
});
const coredata = response.data['abstracts-retrieval-response']?.coredata;
if (!coredata)
return null;
// Extract authors from detailed response
let authors = '';
const authorsData = response.data['abstracts-retrieval-response']?.authors;
if (authorsData && authorsData.author) {
authors = authorsData.author
.map(a => `${a['preferred-name']['ce:given-name']} ${a['preferred-name']['ce:surname']}`)
.join(', ');
}
else if (coredata['dc:creator']) {
authors = coredata['dc:creator'].map((c) => c.$).join(', ');
}
// Extract subjects/keywords
let keywords = [];
const subjectData = response.data['abstracts-retrieval-response']?.subject;
if (subjectData && subjectData.subject) {
keywords = subjectData.subject.map(s => s.$);
}
return PaperFactory.create({
paperId: scopusId,
title: coredata['dc:title'] || '',
authors: authors ? authors.split(', ') : [],
abstract: coredata['dc:description'] || '',
doi: coredata['prism:doi'],
publishedDate: coredata['prism:coverDate'] ? new Date(coredata['prism:coverDate']) : null,
url: coredata['prism:doi'] ? `https://doi.org/${coredata['prism:doi']}` : undefined,
source: 'Scopus',
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,
keywords: keywords,
extra: {
scopusId: coredata['dc:identifier'],
eid: coredata.eid,
pubmedId: coredata['pubmed-id'],
issn: coredata['prism:issn']
}
});
}
catch (error) {
console.error('Scopus abstract retrieval 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 Scopus');
}
async readPaper(paperId, options = {}) {
const paper = await this.getAbstract(paperId);
if (!paper) {
throw new Error('Paper not found');
}
return paper.abstract || 'Abstract not available';
}
}
//# sourceMappingURL=ScopusSearcher.js.map