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.

1,073 lines 47.9 kB
#!/usr/bin/env node /** * Paper Search MCP Server - Node.js Implementation * 支持多个学术平台的论文搜索和下载,包括 Web of Science */ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { ListToolsRequestSchema, CallToolRequestSchema, InitializeRequestSchema, PingRequestSchema } from '@modelcontextprotocol/sdk/types.js'; import * as dotenv from 'dotenv'; import { ArxivSearcher } from './platforms/ArxivSearcher.js'; import { WebOfScienceSearcher } from './platforms/WebOfScienceSearcher.js'; import { PubMedSearcher } from './platforms/PubMedSearcher.js'; import { BioRxivSearcher, MedRxivSearcher } from './platforms/BioRxivSearcher.js'; import { SemanticScholarSearcher } from './platforms/SemanticScholarSearcher.js'; import { IACRSearcher } from './platforms/IACRSearcher.js'; import { GoogleScholarSearcher } from './platforms/GoogleScholarSearcher.js'; import { SciHubSearcher } from './platforms/SciHubSearcher.js'; import { ScienceDirectSearcher } from './platforms/ScienceDirectSearcher.js'; import { SpringerSearcher } from './platforms/SpringerSearcher.js'; import { WileySearcher } from './platforms/WileySearcher.js'; import { ScopusSearcher } from './platforms/ScopusSearcher.js'; import { PaperFactory } from './models/Paper.js'; // 加载环境变量 dotenv.config(); // MCP静默模式检测 const isMCPMode = process.argv.includes('--mcp') || process.env.MCP_SERVER === 'true' || process.stdin.isTTY === false; // 静默日志函数 - 使用rest parameters支持多个参数 const debugLog = (...messages) => { if (!isMCPMode && process.env.NODE_ENV === 'development') { console.error(...messages); } }; // 创建MCP服务器实例 const server = new Server({ name: 'paper-search-mcp-nodejs', version: '0.2.2' }, { capabilities: { tools: { listChanged: true } } }); // 延迟初始化搜索器实例,避免阻塞服务器启动 let searchers = null; const initializeSearchers = () => { if (searchers) return searchers; debugLog('🔧 Initializing searchers...'); const arxivSearcher = new ArxivSearcher(); const wosSearcher = new WebOfScienceSearcher(process.env.WOS_API_KEY, process.env.WOS_API_VERSION || 'v1'); const pubmedSearcher = new PubMedSearcher(process.env.PUBMED_API_KEY); const biorxivSearcher = new BioRxivSearcher('biorxiv'); const medrxivSearcher = new MedRxivSearcher(); const semanticSearcher = new SemanticScholarSearcher(process.env.SEMANTIC_SCHOLAR_API_KEY); const iacrSearcher = new IACRSearcher(); const googleScholarSearcher = new GoogleScholarSearcher(); const sciHubSearcher = new SciHubSearcher(); const scienceDirectSearcher = new ScienceDirectSearcher(process.env.ELSEVIER_API_KEY); const springerSearcher = new SpringerSearcher(process.env.SPRINGER_API_KEY, process.env.SPRINGER_OPENACCESS_API_KEY); const wileySearcher = new WileySearcher(process.env.WILEY_TDM_TOKEN); const scopusSearcher = new ScopusSearcher(process.env.ELSEVIER_API_KEY); searchers = { arxiv: arxivSearcher, webofscience: wosSearcher, pubmed: pubmedSearcher, wos: wosSearcher, // 别名 biorxiv: biorxivSearcher, medrxiv: medrxivSearcher, semantic: semanticSearcher, iacr: iacrSearcher, googlescholar: googleScholarSearcher, scholar: googleScholarSearcher, // 别名 scihub: sciHubSearcher, sciencedirect: scienceDirectSearcher, springer: springerSearcher, wiley: wileySearcher, scopus: scopusSearcher }; debugLog('✅ Searchers initialized successfully'); return searchers; }; // 定义所有可用工具 const TOOLS = [ { name: 'debug_pubmed_test', description: 'Debug PubMed search with detailed logging to bypass MCP cache', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query string' }, maxResults: { type: 'number', minimum: 1, maximum: 5, description: 'Maximum number of results' } }, required: ['query'] } }, { name: 'search_papers', description: 'Search academic papers from multiple sources including arXiv, Web of Science, etc.', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query string' }, platform: { type: 'string', enum: ['arxiv', 'webofscience', 'pubmed', 'wos', 'biorxiv', 'medrxiv', 'semantic', 'iacr', 'googlescholar', 'scholar', 'scihub', 'sciencedirect', 'springer', 'wiley', 'scopus', 'all'], description: 'Platform to search (arxiv, webofscience/wos, pubmed, biorxiv, medrxiv, semantic, iacr, googlescholar/scholar, scihub, sciencedirect, springer, wiley, scopus, or all)' }, maxResults: { type: 'number', minimum: 1, maximum: 100, description: 'Maximum number of results to return' }, year: { type: 'string', description: 'Year filter (e.g., "2023", "2020-2023", "2020-")' }, author: { type: 'string', description: 'Author name filter' }, journal: { type: 'string', description: 'Journal name filter' }, category: { type: 'string', description: 'Category filter (e.g., cs.AI for arXiv)' }, days: { type: 'number', description: 'Number of days to search back (bioRxiv/medRxiv only)' }, fetchDetails: { type: 'boolean', description: 'Fetch detailed information (IACR only)' }, fieldsOfStudy: { type: 'array', items: { type: 'string' }, description: 'Fields of study filter (Semantic Scholar only)' }, sortBy: { type: 'string', enum: ['relevance', 'date', 'citations'], description: 'Sort results by relevance, date, or citations' }, sortOrder: { type: 'string', enum: ['asc', 'desc'], description: 'Sort order: ascending or descending' } }, required: ['query'] } }, { name: 'search_arxiv', description: 'Search academic papers specifically from arXiv preprint server', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query string' }, maxResults: { type: 'number', minimum: 1, maximum: 50, description: 'Maximum number of results to return' }, category: { type: 'string', description: 'arXiv category filter (e.g., cs.AI, physics.gen-ph)' }, author: { type: 'string', description: 'Author name filter' } }, required: ['query'] } }, { name: 'search_webofscience', description: 'Search academic papers from Web of Science database', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query string' }, maxResults: { type: 'number', minimum: 1, maximum: 50, description: 'Maximum number of results to return' }, year: { type: 'string', description: 'Publication year filter' }, author: { type: 'string', description: 'Author name filter' }, journal: { type: 'string', description: 'Journal name filter' } }, required: ['query'] } }, { name: 'search_pubmed', description: 'Search biomedical literature from PubMed/MEDLINE database using NCBI E-utilities API', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query string' }, maxResults: { type: 'number', minimum: 1, maximum: 100, description: 'Maximum number of results to return' }, year: { type: 'string', description: 'Publication year filter (e.g., "2023", "2020-2023")' }, author: { type: 'string', description: 'Author name filter' }, journal: { type: 'string', description: 'Journal name filter' }, publicationType: { type: 'array', items: { type: 'string' }, description: 'Publication type filter (e.g., ["Journal Article", "Review"])' } }, required: ['query'] } }, { name: 'search_biorxiv', description: 'Search bioRxiv preprint server for biology papers', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query string' }, maxResults: { type: 'number', minimum: 1, maximum: 100, description: 'Maximum number of results to return' }, days: { type: 'number', description: 'Number of days to search back (default: 30)' } }, required: ['query'] } }, { name: 'search_medrxiv', description: 'Search medRxiv preprint server for medical papers', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query string' }, maxResults: { type: 'number', minimum: 1, maximum: 100, description: 'Maximum number of results to return' }, days: { type: 'number', description: 'Number of days to search back (default: 30)' } }, required: ['query'] } }, { name: 'search_semantic_scholar', description: 'Search Semantic Scholar for academic papers with citation data', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query string' }, maxResults: { type: 'number', minimum: 1, maximum: 100, description: 'Maximum number of results to return' }, year: { type: 'string', description: 'Year filter (e.g., "2023", "2020-2023")' }, fieldsOfStudy: { type: 'array', items: { type: 'string' }, description: 'Fields of study filter (e.g., ["Computer Science", "Biology"])' } }, required: ['query'] } }, { name: 'search_iacr', description: 'Search IACR ePrint Archive for cryptography papers', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query string' }, maxResults: { type: 'number', minimum: 1, maximum: 50, description: 'Maximum number of results to return' }, fetchDetails: { type: 'boolean', description: 'Fetch detailed information for each paper (slower)' } }, required: ['query'] } }, { name: 'download_paper', description: 'Download PDF file of an academic paper', inputSchema: { type: 'object', properties: { paperId: { type: 'string', description: 'Paper ID (e.g., arXiv ID, DOI for Sci-Hub)' }, platform: { type: 'string', enum: ['arxiv', 'biorxiv', 'medrxiv', 'semantic', 'iacr', 'scihub', 'springer', 'wiley'], description: 'Platform where the paper is from' }, savePath: { type: 'string', description: 'Directory to save the PDF file' } }, required: ['paperId', 'platform'] } }, { name: 'search_google_scholar', description: 'Search Google Scholar for academic papers using web scraping', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query string' }, maxResults: { type: 'number', minimum: 1, maximum: 20, description: 'Maximum number of results to return' }, yearLow: { type: 'number', description: 'Earliest publication year' }, yearHigh: { type: 'number', description: 'Latest publication year' }, author: { type: 'string', description: 'Author name filter' } }, required: ['query'] } }, { name: 'get_paper_by_doi', description: 'Retrieve paper information using DOI from available platforms', inputSchema: { type: 'object', properties: { doi: { type: 'string', description: 'DOI (Digital Object Identifier)' }, platform: { type: 'string', enum: ['arxiv', 'webofscience', 'all'], description: 'Platform to search' } }, required: ['doi'] } }, { name: 'search_scihub', description: 'Search and download papers from Sci-Hub using DOI or paper URL. Automatically detects and uses the fastest available mirror.', inputSchema: { type: 'object', properties: { doiOrUrl: { type: 'string', description: 'DOI (e.g., "10.1038/nature12373") or full paper URL' }, downloadPdf: { type: 'boolean', description: 'Whether to download the PDF file', default: false }, savePath: { type: 'string', description: 'Directory to save the PDF file (if downloadPdf is true)' } }, required: ['doiOrUrl'] } }, { name: 'check_scihub_mirrors', description: 'Check the health status of all Sci-Hub mirror sites', inputSchema: { type: 'object', properties: { forceCheck: { type: 'boolean', description: 'Force a fresh health check even if recent data exists', default: false } } } }, { name: 'get_platform_status', description: 'Check the status and capabilities of available academic platforms', inputSchema: { type: 'object', properties: {} } }, { name: 'search_sciencedirect', description: 'Search academic papers from Elsevier ScienceDirect database (requires API key)', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query string' }, maxResults: { type: 'number', minimum: 1, maximum: 100, description: 'Maximum number of results to return' }, year: { type: 'string', description: 'Year filter (e.g., "2023", "2020-2023")' }, author: { type: 'string', description: 'Author name filter' }, journal: { type: 'string', description: 'Journal name filter' }, openAccess: { type: 'boolean', description: 'Filter for open access articles only' } }, required: ['query'] } }, { name: 'search_springer', description: 'Search academic papers from Springer Nature database. Uses Metadata API by default (all content) or OpenAccess API when openAccess=true (full text available). Same API key works for both.', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query string' }, maxResults: { type: 'number', minimum: 1, maximum: 100, description: 'Maximum number of results to return' }, year: { type: 'string', description: 'Year filter (e.g., "2023", "2020-2023")' }, author: { type: 'string', description: 'Author name filter' }, journal: { type: 'string', description: 'Journal name filter' }, subject: { type: 'string', description: 'Subject area filter' }, openAccess: { type: 'boolean', description: 'Search only open access content' }, type: { type: 'string', enum: ['Journal', 'Book', 'Chapter'], description: 'Publication type filter' } }, required: ['query'] } }, { name: 'search_wiley', description: 'Search academic papers from Wiley Online Library (requires TDM token)', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query string' }, maxResults: { type: 'number', minimum: 1, maximum: 100, description: 'Maximum number of results to return' }, year: { type: 'string', description: 'Year filter (e.g., "2023", "2020-2023")' }, author: { type: 'string', description: 'Author name filter' }, journal: { type: 'string', description: 'Journal name filter' }, subject: { type: 'string', description: 'Subject area filter' }, openAccess: { type: 'boolean', description: 'Filter for open access articles only' } }, required: ['query'] } }, { name: 'search_scopus', description: 'Search the Scopus abstract and citation database (requires Elsevier API key)', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query string' }, maxResults: { type: 'number', minimum: 1, maximum: 25, description: 'Maximum number of results (max 25 per request)' }, year: { type: 'string', description: 'Year filter (e.g., "2023", "2020-2023")' }, author: { type: 'string', description: 'Author name filter' }, journal: { type: 'string', description: 'Journal name filter' }, affiliation: { type: 'string', description: 'Institution/affiliation filter' }, subject: { type: 'string', description: 'Subject area filter' }, openAccess: { type: 'boolean', description: 'Filter for open access articles only' }, documentType: { type: 'string', enum: ['ar', 'cp', 're', 'bk', 'ch'], description: 'Document type: ar=article, cp=conference paper, re=review, bk=book, ch=chapter' } }, required: ['query'] } } ]; // 添加initialize请求处理器 - MCP协议的核心初始化 server.setRequestHandler(InitializeRequestSchema, async (request) => { debugLog('🤝 Received initialize request:', request.params); return { protocolVersion: '2024-11-05', capabilities: { tools: { listChanged: true } }, serverInfo: { name: 'paper-search-mcp-nodejs', version: '0.2.2' } }; }); // 添加ping请求处理器 - 连接保活 server.setRequestHandler(PingRequestSchema, async () => { debugLog('🏓 Received ping request'); return {}; }); // 添加tools/list请求处理器 server.setRequestHandler(ListToolsRequestSchema, async () => { debugLog('🔧 Received tools/list request'); return { tools: TOOLS }; }); // 添加tools/call请求处理器 server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; debugLog(`🔨 Received tools/call request: ${name}`); try { // 延迟初始化搜索器 const currentSearchers = initializeSearchers(); switch (name) { case 'debug_pubmed_test': { const params = args; const { query, maxResults = 2 } = params; try { // 直接创建新的PubMed搜索器实例进行测试 const testSearcher = new PubMedSearcher(process.env.PUBMED_API_KEY); const testResults = await testSearcher.search(query, { maxResults }); return { content: [{ type: 'text', text: `🧪 DEBUG PUBMED TEST 🧪\\nQuery: "${query}"\\nMaxResults: ${maxResults}\\nAPI Key: ${process.env.PUBMED_API_KEY ? 'SET' : 'NOT SET'}\\nResults: ${testResults.length}\\nFirst Title: ${testResults.length > 0 ? testResults[0].title : 'N/A'}\\n\\nFull Results:\\n${JSON.stringify(testResults.map(p => ({ title: p.title, paperId: p.paperId, authors: p.authors.slice(0, 2) })), null, 2)}` }] }; } catch (error) { return { content: [{ type: 'text', text: `🚨 DEBUG PUBMED ERROR 🚨\\nQuery: "${query}"\\nError: ${error.message}\\nStack: ${error.stack}` }] }; } } case 'search_papers': { const params = args; const { query, platform = 'all', maxResults = 10, year, author, sortBy = 'relevance', sortOrder = 'desc' } = params; const results = []; const searchOptions = { maxResults, year, author, sortBy, sortOrder }; if (platform === 'all') { // 随机选择一个平台进行搜索 const availablePlatforms = Object.keys(currentSearchers).filter(name => name !== 'wos' && name !== 'scholar'); // 跳过别名 const randomPlatform = availablePlatforms[Math.floor(Math.random() * availablePlatforms.length)]; debugLog(`🎲 Randomly selected platform: ${randomPlatform}`); try { const searcher = currentSearchers[randomPlatform]; const platformResults = await searcher.search(query, searchOptions); results.push(...platformResults.map((paper) => PaperFactory.toDict(paper))); } catch (error) { debugLog(`Error searching random platform ${randomPlatform}:`, error); // 如果随机平台失败,尝试 arxiv 作为备选 try { debugLog('🔄 Fallback to arXiv platform'); const platformResults = await currentSearchers.arxiv.search(query, searchOptions); results.push(...platformResults.map((paper) => PaperFactory.toDict(paper))); } catch (fallbackError) { debugLog('Error with arxiv fallback:', fallbackError); } } } else { // 搜索指定平台 const searcher = currentSearchers[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 { content: [{ type: 'text', text: `Found ${results.length} papers.\\n\\n${JSON.stringify(results, null, 2)}` }] }; } case 'search_arxiv': { const params = args; const { query, maxResults = 10, category, author } = params; const results = await currentSearchers.arxiv.search(query, { maxResults, category, author }); return { content: [{ type: 'text', text: `Found ${results.length} arXiv papers.\\n\\n${JSON.stringify(results.map((paper) => PaperFactory.toDict(paper)), null, 2)}` }] }; } case 'search_webofscience': { const params = args; const { query, maxResults = 10, year, author, journal } = params; 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 currentSearchers.webofscience.search(query, { maxResults, year, author, journal }); return { content: [{ type: 'text', text: `Found ${results.length} Web of Science papers.\\n\\n${JSON.stringify(results.map((paper) => PaperFactory.toDict(paper)), null, 2)}` }] }; } case 'search_pubmed': { const params = args; const { query, maxResults = 10, year, author, journal, publicationType } = params; debugLog(`🔍 MCP PubMed Search: query="${query}", maxResults=${maxResults}`); debugLog(`📋 MCP PubMed Search options:`, { maxResults, year, author, journal, publicationType }); debugLog(`🔧 MCP PubMed Searcher type:`, typeof currentSearchers.pubmed); debugLog(`🔧 MCP PubMed Searcher hasApiKey:`, currentSearchers.pubmed.hasApiKey()); debugLog(`⏳ MCP PubMed: About to call searcher.search()...`); const results = await currentSearchers.pubmed.search(query, { maxResults, year, author, journal, publicationType }); debugLog(`⚡ MCP PubMed: searcher.search() completed`); debugLog(`📄 MCP PubMed Results: Found ${results.length} papers`); if (results.length > 0) { debugLog(`📋 First paper title:`, results[0].title); debugLog(`📋 First paper paperId:`, results[0].paperId); } else { debugLog(`❌ MCP PubMed: No results returned from searcher`); } // 获取速率限制器状态信息 const rateStatus = currentSearchers.pubmed.getRateLimiterStatus(); const apiKeyStatus = currentSearchers.pubmed.hasApiKey() ? 'configured' : 'not configured'; const rateLimit = currentSearchers.pubmed.hasApiKey() ? '10 requests/second' : '3 requests/second'; // 创建详细的调试信息 const debugInfo = { searchParams: { query, maxResults, year, author, journal, publicationType }, searcherType: typeof currentSearchers.pubmed, hasApiKey: currentSearchers.pubmed.hasApiKey(), apiKeyStatus, rateLimit, rateLimiterStatus: rateStatus, resultCount: results.length, resultTypes: results.map(r => typeof r), firstResultTitle: results.length > 0 ? results[0].title : 'N/A' }; return { content: [{ type: 'text', text: `MCP DEBUG: query="${query}", searcher.hasApiKey()=${currentSearchers.pubmed.hasApiKey()}, typeof results=${typeof results}, results.length=${results.length}\\n\\nFound ${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 params = args; const { query, maxResults = 10, days } = params; const results = await currentSearchers.biorxiv.search(query, { maxResults, days }); return { content: [{ type: 'text', text: `Found ${results.length} bioRxiv papers.\\\\n\\\\n${JSON.stringify(results.map((paper) => PaperFactory.toDict(paper)), null, 2)}` }] }; } case 'search_medrxiv': { const params = args; const { query, maxResults = 10, days } = params; const results = await currentSearchers.medrxiv.search(query, { maxResults, days }); return { content: [{ type: 'text', text: `Found ${results.length} medRxiv papers.\\\\n\\\\n${JSON.stringify(results.map((paper) => PaperFactory.toDict(paper)), null, 2)}` }] }; } case 'search_semantic_scholar': { const params = args; const { query, maxResults = 10, year, fieldsOfStudy } = params; const results = await currentSearchers.semantic.search(query, { maxResults, year, fieldsOfStudy }); // 获取速率限制器状态信息 const rateStatus = currentSearchers.semantic.getRateLimiterStatus(); const apiKeyStatus = currentSearchers.semantic.hasApiKey() ? 'configured' : 'not configured (using free tier)'; const rateLimit = currentSearchers.semantic.hasApiKey() ? '200 requests/minute' : '20 requests/minute'; return { content: [{ type: 'text', text: `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 params = args; const { query, maxResults = 10, fetchDetails } = params; const results = await currentSearchers.iacr.search(query, { maxResults, fetchDetails }); return { content: [{ type: 'text', text: `Found ${results.length} IACR ePrint papers.\\\\n\\\\n${JSON.stringify(results.map((paper) => PaperFactory.toDict(paper)), null, 2)}` }] }; } case 'download_paper': { const params = args; const { paperId, platform, savePath = './downloads' } = params; const searcher = currentSearchers[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 }); return { content: [{ type: 'text', text: `PDF downloaded successfully to: ${filePath}` }] }; } case 'search_google_scholar': { const params = args; const { query, maxResults = 10, yearLow, yearHigh, author } = params; debugLog(`🔍 Google Scholar Search: query="${query}", maxResults=${maxResults}`); const results = await currentSearchers.googlescholar.search(query, { maxResults, yearLow, yearHigh, author }); debugLog(`📄 Google Scholar Results: Found ${results.length} papers`); return { content: [{ type: 'text', text: `Found ${results.length} Google Scholar papers.\\n\\n${JSON.stringify(results.map((paper) => PaperFactory.toDict(paper)), null, 2)}` }] }; } case 'get_paper_by_doi': { const params = args; const { doi, platform = 'all' } = params; const results = []; if (platform === 'all') { // 尝试所有平台 for (const [platformName, searcher] of Object.entries(currentSearchers)) { if (platformName === 'wos') continue; // 跳过别名 try { const paper = await searcher.getPaperByDoi(doi); if (paper) { results.push(PaperFactory.toDict(paper)); } } catch (error) { debugLog(`Error getting paper by DOI from ${platformName}:`, error); } } } else { // 指定平台 const searcher = currentSearchers[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 { content: [{ type: 'text', text: `No paper found with DOI: ${doi}` }] }; } return { content: [{ type: 'text', text: `Found ${results.length} paper(s) with DOI ${doi}:\\n\\n${JSON.stringify(results, null, 2)}` }] }; } case 'search_scihub': { const params = args; const { doiOrUrl, downloadPdf = false, savePath = './downloads' } = params; debugLog(`🔍 Sci-Hub Search: doiOrUrl="${doiOrUrl}", downloadPdf=${downloadPdf}`); // Search for the paper const results = await currentSearchers.scihub.search(doiOrUrl); if (results.length === 0) { return { content: [{ type: 'text', text: `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)}`; // Download PDF if requested if (downloadPdf && paper.pdfUrl) { try { const filePath = await currentSearchers.scihub.downloadPdf(doiOrUrl, { savePath }); responseText += `\n\nPDF downloaded successfully to: ${filePath}`; } catch (downloadError) { responseText += `\n\nFailed to download PDF: ${downloadError.message}`; } } return { content: [{ type: 'text', text: responseText }] }; } case 'check_scihub_mirrors': { const params = args; const { forceCheck = false } = params; if (forceCheck) { debugLog('🔄 Forcing Sci-Hub mirror health check...'); await currentSearchers.scihub.forceHealthCheck(); } const mirrorStatus = currentSearchers.scihub.getMirrorStatus(); return { content: [{ type: 'text', text: `Sci-Hub Mirror Status:\n\n${JSON.stringify(mirrorStatus, null, 2)}` }] }; } case 'search_sciencedirect': { const params = args; const { query, maxResults = 10, year, author, journal, openAccess } = params; if (!process.env.ELSEVIER_API_KEY) { throw new Error('Elsevier API key not configured. Please set ELSEVIER_API_KEY environment variable.'); } const results = await currentSearchers.sciencedirect.search(query, { maxResults, year, author, journal, openAccess }); return { content: [{ type: 'text', text: `Found ${results.length} ScienceDirect papers.\n\n${JSON.stringify(results.map((paper) => PaperFactory.toDict(paper)), null, 2)}` }] }; } case 'search_springer': { const params = args; const { query, maxResults = 10, year, author, journal, subject, openAccess, type } = params; if (!process.env.SPRINGER_API_KEY) { throw new Error('Springer API key not configured. Please set SPRINGER_API_KEY environment variable.'); } const results = await currentSearchers.springer.search(query, { maxResults, year, author, journal, subject, openAccess, type }); return { content: [{ type: 'text', text: `Found ${results.length} Springer papers.\n\n${JSON.stringify(results.map((paper) => PaperFactory.toDict(paper)), null, 2)}` }] }; } case 'search_wiley': { const params = args; const { query, maxResults = 10, year, author, journal, subject, openAccess } = params; if (!process.env.WILEY_TDM_TOKEN) { throw new Error('Wiley TDM token not configured. Please set WILEY_TDM_TOKEN environment variable.'); } const results = await currentSearchers.wiley.search(query, { maxResults, year, author, journal, subject, openAccess }); return { content: [{ type: 'text', text: `Found ${results.length} Wiley papers.\n\n${JSON.stringify(results.map((paper) => PaperFactory.toDict(paper)), null, 2)}` }] }; } case 'search_scopus': { const params = args; const { query, maxResults = 10, year, author, journal, affiliation, subject, openAccess, documentType } = params; if (!process.env.ELSEVIER_API_KEY) { throw new Error('Elsevier API key not configured. Please set ELSEVIER_API_KEY environment variable.'); } const results = await currentSearchers.scopus.search(query, { maxResults, year, author, journal, affiliation, subject, openAccess, documentType }); return { content: [{ type: 'text', text: `Found ${results.length} Scopus papers.\n\n${JSON.stringify(results.map((paper) => PaperFactory.toDict(paper)), null, 2)}` }] }; } case 'get_platform_status': { const statusInfo = []; for (const [platformName, searcher] of Object.entries(currentSearchers)) { if (platformName === 'wos' || platformName === 'scholar') continue; // 跳过别名 const capabilities = searcher.getCapabilities(); const hasApiKey = searcher.hasApiKey(); let apiKeyStatus = 'not_required'; if (capabilities.requiresApiKey) { if (hasApiKey) { // 验证API密钥 const isValid = await searcher.validateApiKey(); apiKeyStatus = isValid ? 'valid' : 'invalid'; } else { apiKeyStatus = 'missing'; } } // Add special status for Sci-Hub let additionalInfo = {}; if (platformName === 'scihub') { additionalInfo = { mirrorCount: currentSearchers.scihub.getMirrorStatus().length, workingMirrors: currentSearchers.scihub.getMirrorStatus().filter(m => m.status === 'Working').length }; } statusInfo.push({ platform: platformName, baseUrl: searcher.getBaseUrl(), capabilities: capabilities, apiKeyStatus: apiKeyStatus, ...additionalInfo }); } return { content: [{ type: 'text', text: `Platform Status:\n\n${JSON.stringify(statusInfo, null, 2)}` }] }; } default: debugLog(`Unknown tool requested: ${name}`); throw new Error(`Unknown tool: ${name}`); } } catch (error) { debugLog(`Error in tool ${name}:`, error); return { content: [{ type: 'text', text: `Error executing tool '${name}': ${error.message || 'Unknown error occurred'}` }], isError: true }; } }); /** * 启动服务器 */ async function main() { try { debugLog('🚀 Starting Paper Search MCP Server (Node.js)...'); debugLog(`📍 Working directory: ${process.cwd()}`); debugLog(`📦 Node.js version: ${process.version}`); debugLog(`🔧 Process arguments:`, process.argv); // 连接到标准输入输出传输 const transport = new StdioServerTransport(); debugLog('📡 Connecting to stdio transport...'); await server.connect(transport); debugLog('✅ Paper Search MCP Server is running!'); debugLog('🔌 Ready to receive MCP protocol messages via stdio'); // 注意:MCP服务器通过stdio通信,不监听网络端口 debugLog('ℹ️ Note: MCP servers communicate via stdio, not network ports'); } catch (error) { debugLog('❌ Failed to start server:', error); process.exit(1); } } // 处理未捕获的错误 - MCP模式下更温和 process.on('uncaughtException', (error) => { if (!isMCPMode) { debugLog('Uncaught Exception:', error); process.exit(1); } // MCP模式下不立即退出,避免干扰协议通信 }); process.on('unhandledRejection', (reason, promise) => { if (!isMCPMode) { debugLog('Unhandled Rejection at:', promise, 'reason:', reason); process.exit(1); } }); // 启动服务器 - 直接调用main()确保服务器总是启动 main().catch((error) => { debugLog('Failed to start MCP server:', error); process.exit(1); }); //# sourceMappingURL=server.js.map