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.
337 lines • 16 kB
JavaScript
import { parseToolArgs } from './schemas.js';
import { PaperFactory } from '../models/Paper.js';
import { logDebug } from '../utils/Logger.js';
function jsonTextResponse(text) {
return {
content: [
{
type: 'text',
text
}
]
};
}
export async function handleToolCall(toolNameRaw, rawArgs, searchers) {
const toolName = toolNameRaw;
const args = parseToolArgs(toolName, rawArgs);
switch (toolName) {
case 'search_papers': {
const { query, platform, maxResults, year, author, journal, category, days, fetchDetails, fieldsOfStudy, sortBy, sortOrder } = args;
const results = [];
const searchOptions = {
maxResults,
year,
author,
journal,
category,
days,
fetchDetails,
fieldsOfStudy,
sortBy,
sortOrder
};
if (platform === 'all') {
try {
const platformResults = await searchers.crossref.search(query, searchOptions);
results.push(...platformResults.map((paper) => PaperFactory.toDict(paper)));
}
catch (error) {
logDebug('Error searching crossref:', error);
try {
const platformResults = await searchers.arxiv.search(query, searchOptions);
results.push(...platformResults.map((paper) => PaperFactory.toDict(paper)));
}
catch (fallbackError) {
logDebug('Error with arxiv fallback:', fallbackError);
}
}
}
else {
const searcher = searchers[platform];
if (!searcher) {
throw new Error(`Unsupported platform: ${platform}`);
}
const platformResults = await searcher.search(query, searchOptions);
results.push(...platformResults.map((paper) => PaperFactory.toDict(paper)));
}
return jsonTextResponse(`Found ${results.length} papers.\n\n${JSON.stringify(results, null, 2)}`);
}
case 'search_arxiv': {
const { query, maxResults, category, author, year, sortBy, sortOrder } = args;
const results = await searchers.arxiv.search(query, {
maxResults,
category,
author,
year,
sortBy,
sortOrder
});
return jsonTextResponse(`Found ${results.length} arXiv papers.\n\n${JSON.stringify(results.map((paper) => PaperFactory.toDict(paper)), null, 2)}`);
}
case 'search_webofscience': {
const { query, maxResults, year, author, journal, sortBy, sortOrder } = args;
if (!process.env.WOS_API_KEY) {
throw new Error('Web of Science API key not configured. Please set WOS_API_KEY environment variable.');
}
const results = await searchers.webofscience.search(query, {
maxResults,
year,
author,
journal,
sortBy,
sortOrder
});
return jsonTextResponse(`Found ${results.length} Web of Science papers.\n\n${JSON.stringify(results.map((paper) => PaperFactory.toDict(paper)), null, 2)}`);
}
case 'search_pubmed': {
const { query, maxResults, year, author, journal, publicationType, sortBy } = args;
const results = await searchers.pubmed.search(query, {
maxResults,
year,
author,
journal,
publicationType,
sortBy
});
const rateStatus = searchers.pubmed.getRateLimiterStatus();
const apiKeyStatus = searchers.pubmed.hasApiKey() ? 'configured' : 'not configured';
const rateLimit = searchers.pubmed.hasApiKey() ? '10 requests/second' : '3 requests/second';
return jsonTextResponse(`Found ${results.length} PubMed papers.\n\nAPI Status: ${apiKeyStatus} (${rateLimit})\nRate Limiter: ${rateStatus.availableTokens}/${rateStatus.maxTokens} tokens available\n\n${JSON.stringify(results.map((paper) => PaperFactory.toDict(paper)), null, 2)}`);
}
case 'search_biorxiv': {
const { query, maxResults, days, category } = args;
const results = await searchers.biorxiv.search(query, {
maxResults,
days,
category
});
return jsonTextResponse(`Found ${results.length} bioRxiv papers.\n\n${JSON.stringify(results.map((paper) => PaperFactory.toDict(paper)), null, 2)}`);
}
case 'search_medrxiv': {
const { query, maxResults, days, category } = args;
const results = await searchers.medrxiv.search(query, {
maxResults,
days,
category
});
return jsonTextResponse(`Found ${results.length} medRxiv papers.\n\n${JSON.stringify(results.map((paper) => PaperFactory.toDict(paper)), null, 2)}`);
}
case 'search_semantic_scholar': {
const { query, maxResults, year, fieldsOfStudy } = args;
const results = await searchers.semantic.search(query, {
maxResults,
year,
fieldsOfStudy
});
const rateStatus = searchers.semantic.getRateLimiterStatus();
const apiKeyStatus = searchers.semantic.hasApiKey()
? 'configured'
: 'not configured (using free tier)';
const rateLimit = searchers.semantic.hasApiKey() ? '200 requests/minute' : '20 requests/minute';
return jsonTextResponse(`Found ${results.length} Semantic Scholar papers.\n\nAPI Status: ${apiKeyStatus} (${rateLimit})\nRate Limiter: ${rateStatus.availableTokens}/${rateStatus.maxTokens} tokens available\n\n${JSON.stringify(results.map((paper) => PaperFactory.toDict(paper)), null, 2)}`);
}
case 'search_iacr': {
const { query, maxResults, fetchDetails } = args;
const results = await searchers.iacr.search(query, { maxResults, fetchDetails });
return jsonTextResponse(`Found ${results.length} IACR ePrint papers.\n\n${JSON.stringify(results.map((paper) => PaperFactory.toDict(paper)), null, 2)}`);
}
case 'download_paper': {
const { paperId, platform, savePath } = args;
const resolvedSavePath = savePath || './downloads';
const searcher = searchers[platform];
if (!searcher) {
throw new Error(`Unsupported platform for download: ${platform}`);
}
if (!searcher.getCapabilities().download) {
throw new Error(`Platform ${platform} does not support PDF download`);
}
const filePath = await searcher.downloadPdf(paperId, { savePath: resolvedSavePath });
return jsonTextResponse(`PDF downloaded successfully to: ${filePath}`);
}
case 'search_google_scholar': {
const { query, maxResults, yearLow, yearHigh, author } = args;
const results = await searchers.googlescholar.search(query, {
maxResults,
yearLow,
yearHigh,
author
});
return jsonTextResponse(`Found ${results.length} Google Scholar papers.\n\n${JSON.stringify(results.map((paper) => PaperFactory.toDict(paper)), null, 2)}`);
}
case 'get_paper_by_doi': {
const { doi, platform } = args;
const results = [];
if (platform === 'all') {
for (const [platformName, searcher] of Object.entries(searchers)) {
if (platformName === 'wos' || platformName === 'scholar')
continue;
try {
const paper = await searcher.getPaperByDoi(doi);
if (paper) {
results.push(PaperFactory.toDict(paper));
}
}
catch (error) {
logDebug(`Error getting paper by DOI from ${platformName}:`, error);
}
}
}
else {
const searcher = searchers[platform];
if (!searcher) {
throw new Error(`Unsupported platform: ${platform}`);
}
const paper = await searcher.getPaperByDoi(doi);
if (paper) {
results.push(PaperFactory.toDict(paper));
}
}
if (results.length === 0) {
return jsonTextResponse(`No paper found with DOI: ${doi}`);
}
return jsonTextResponse(`Found ${results.length} paper(s) with DOI ${doi}:\n\n${JSON.stringify(results, null, 2)}`);
}
case 'search_scihub': {
const { doiOrUrl, downloadPdf, savePath } = args;
const resolvedSavePath = savePath || './downloads';
const results = await searchers.scihub.search(doiOrUrl);
if (results.length === 0) {
return jsonTextResponse(`No paper found on Sci-Hub for: ${doiOrUrl}`);
}
const paper = results[0];
let responseText = `Found paper on Sci-Hub:\n\n${JSON.stringify(PaperFactory.toDict(paper), null, 2)}`;
if (downloadPdf && paper.pdfUrl) {
try {
const filePath = await searchers.scihub.downloadPdf(doiOrUrl, { savePath: resolvedSavePath });
responseText += `\n\nPDF downloaded successfully to: ${filePath}`;
}
catch (downloadError) {
responseText += `\n\nFailed to download PDF: ${downloadError.message}`;
}
}
return jsonTextResponse(responseText);
}
case 'check_scihub_mirrors': {
const { forceCheck } = args;
if (forceCheck) {
await searchers.scihub.forceHealthCheck();
}
const mirrorStatus = searchers.scihub.getMirrorStatus();
return jsonTextResponse(`Sci-Hub Mirror Status:\n\n${JSON.stringify(mirrorStatus, null, 2)}`);
}
case 'search_sciencedirect': {
const { query, maxResults, year, author, journal, openAccess } = args;
if (!process.env.ELSEVIER_API_KEY) {
throw new Error('Elsevier API key not configured. Please set ELSEVIER_API_KEY environment variable.');
}
const results = await searchers.sciencedirect.search(query, {
maxResults,
year,
author,
journal,
openAccess
});
return jsonTextResponse(`Found ${results.length} ScienceDirect papers.\n\n${JSON.stringify(results.map((paper) => PaperFactory.toDict(paper)), null, 2)}`);
}
case 'search_springer': {
const { query, maxResults, year, author, journal, subject, openAccess, type } = args;
if (!process.env.SPRINGER_API_KEY) {
throw new Error('Springer API key not configured. Please set SPRINGER_API_KEY environment variable.');
}
const results = await searchers.springer.search(query, {
maxResults,
year,
author,
journal,
subject,
openAccess,
type
});
return jsonTextResponse(`Found ${results.length} Springer papers.\n\n${JSON.stringify(results.map((paper) => PaperFactory.toDict(paper)), null, 2)}`);
}
case 'search_wiley': {
return jsonTextResponse(`DEPRECATED: Wiley TDM API does not support keyword search.\n\n` +
`To access Wiley content:\n` +
`1. Use search_crossref to find Wiley articles (filter by publisher if needed)\n` +
`2. Use download_paper with platform="wiley" and the DOI to download the PDF\n\n` +
`Example: download_paper(paperId="10.1111/jtsb.12390", platform="wiley")`);
}
case 'search_scopus': {
const { query, maxResults, year, author, journal, affiliation, subject, openAccess, documentType } = args;
if (!process.env.ELSEVIER_API_KEY) {
throw new Error('Elsevier API key not configured. Please set ELSEVIER_API_KEY environment variable.');
}
const results = await searchers.scopus.search(query, {
maxResults,
year,
author,
journal,
affiliation,
subject,
openAccess,
documentType
});
return jsonTextResponse(`Found ${results.length} Scopus papers.\n\n${JSON.stringify(results.map((paper) => PaperFactory.toDict(paper)), null, 2)}`);
}
case 'search_crossref': {
const { query, maxResults, year, author, sortBy, sortOrder } = args;
const results = await searchers.crossref.search(query, {
maxResults,
year,
author,
sortBy,
sortOrder
});
return jsonTextResponse(`Found ${results.length} Crossref papers.\n\n${JSON.stringify(results.map((paper) => PaperFactory.toDict(paper)), null, 2)}`);
}
case 'get_platform_status': {
const { validate } = args;
const statusInfo = [];
for (const [platformName, searcher] of Object.entries(searchers)) {
if (platformName === 'wos' || platformName === 'scholar')
continue;
const capabilities = searcher.getCapabilities();
const hasApiKey = searcher.hasApiKey();
let apiKeyStatus = 'not_required';
if (capabilities.requiresApiKey) {
if (hasApiKey) {
if (validate) {
try {
const isValid = await searcher.validateApiKey();
apiKeyStatus = isValid ? 'valid' : 'invalid';
}
catch {
apiKeyStatus = 'unknown';
}
}
else {
apiKeyStatus = 'configured';
}
}
else {
apiKeyStatus = 'missing';
}
}
let additionalInfo = {};
if (platformName === 'scihub') {
const mirrorStatus = searchers.scihub.getMirrorStatus();
additionalInfo = {
mirrorCount: mirrorStatus.length,
workingMirrors: mirrorStatus.filter(m => m.status === 'Working').length
};
}
statusInfo.push({
platform: platformName,
baseUrl: searcher.getBaseUrl(),
capabilities,
apiKeyStatus,
...additionalInfo
});
}
return jsonTextResponse(`Platform Status:\n\n${JSON.stringify(statusInfo, null, 2)}`);
}
default:
throw new Error(`Unknown tool: ${toolNameRaw}`);
}
}
//# sourceMappingURL=handleToolCall.js.map