Skip to main content
Glama
scholar-server.ts12.7 kB
import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import { z } from 'zod'; import { downloadPaper, fetchPaperDetail, getKimiAnalysis, searchPapers, } from './cool-papers.js'; import { DownloadOptions, KimiQA, PaperSummary, SearchOptions, SearchResult, } from './types.js'; const server = new Server( { name: 'scholar-mcp', version: '2.0.0', }, { capabilities: { tools: {}, }, } ); const SourceEnum = z.enum(['arxiv', 'venue']); type PublicPaperSummary = Omit< PaperSummary, 'detailUrl' | 'pdfStars' | 'kimiStars' >; interface PublicSearchResult extends Omit<SearchResult, 'papers'> { papers: PublicPaperSummary[]; } interface DownloadSuccess { paper: PublicPaperSummary; pdfUrl: string; filePath: string; fileSize: number; } interface DownloadFailure { paper: PublicPaperSummary; error: string; } interface BatchSearchReport extends PublicSearchResult { params: { maxResults?: number; skip?: number; sort?: number; }; } interface SingleDownloadPayload { mode: 'single'; download: DownloadSuccess; } interface BatchDownloadPayload { mode: 'batch'; search: BatchSearchReport; successes: DownloadSuccess[]; failures: DownloadFailure[]; } function sanitizePaperSummary(paper: PaperSummary): PublicPaperSummary { const { detailUrl: _detail, pdfStars: _pdf, kimiStars: _kimi, ...rest } = paper; return rest; } function sanitizeSearchResult(results: SearchResult): PublicSearchResult { return { source: results.source, query: results.query, total: results.total, papers: results.papers.map(sanitizePaperSummary), }; } const SearchPapersSchema = z.object({ source: SourceEnum.default('arxiv').describe('Data source: arxiv (preprints) or venue (published works)'), query: z.string().min(1).describe('Search keywords'), maxResults: z .number() .int() .min(1) .max(100) .optional() .describe('Limit result count (passed through to Cool Papers as the show parameter); use 3 for precise targeting, 10-20 for general use to control cost'), skip: z.number().int().min(0).optional().describe('Offset results using the skip parameter'), sort: z .number() .int() .min(0) .max(2) .optional() .describe('Sorting flag passed through to Cool Papers (0=date desc, 1=reading stars desc)'), }); const DownloadSinglePaperSchema = z.object({ source: SourceEnum.describe('Data source for the target paper'), downloadFolder: z .string() .min(1) .describe('Absolute or relative folder path where the PDF will be saved'), paperId: z .string() .trim() .min(1) .describe('Paper identifier (e.g. 2412.21139) to download'), filename: z .string() .optional() .describe('Optional filename override to use for the downloaded PDF'), }); const DownloadBatchPapersSchema = z.object({ source: SourceEnum.describe('Data source for the papers to download'), downloadFolder: z .string() .min(1) .describe('Absolute or relative folder path where PDFs will be saved'), query: z .string() .trim() .min(1) .describe('Keyword query to select papers for batch download'), maxResults: z .number() .int() .min(1) .max(100) .optional() .describe('Limit batch downloads to the first N results (passed through as the show parameter); set 3 for precise pulls, 10-20 for routine batches to limit cost'), skip: z .number() .int() .min(0) .optional() .describe('Skip the first N search results before downloading'), sort: z .number() .int() .min(0) .max(2) .optional() .describe('Sorting flag passed through to Cool Papers (0=newest, 1=top reading stars)'), }); const KimiAnalysisSchema = z.object({ source: SourceEnum.describe('Data source where the paper is listed'), paperId: z.string().min(1).describe('Identifier to request the Kimi FAQ'), }); server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'search_papers', description: 'Search Cool Papers (arXiv or venue) and return sanitized summaries with PDF links', inputSchema: { type: 'object', properties: { source: { type: 'string', enum: ['arxiv', 'venue'], default: 'arxiv', description: 'Choose between arxiv or venue listings', }, query: { type: 'string', description: 'Keywords to search for', }, maxResults: { type: 'number', description: 'Maximum number of items to return (passed as Cool Papers show); set 3 for precision, 10-20 for typical searches to manage cost', }, skip: { type: 'number', description: 'Number of items to skip before collecting results', }, sort: { type: 'number', description: 'Sorting directive (0=newest first, 1=highest reading stars)', }, }, required: ['query'], }, }, { name: 'download_single_paper', description: 'Download one Cool Papers PDF by explicit paper ID', inputSchema: { type: 'object', properties: { source: { type: 'string', enum: ['arxiv', 'venue'], description: 'Source where the paper is listed', }, paperId: { type: 'string', description: 'Paper identifier (e.g. 2412.21139) to download', }, downloadFolder: { type: 'string', description: 'Destination folder for the PDF', }, filename: { type: 'string', description: 'Optional target filename to use for the downloaded PDF', }, }, required: ['source', 'paperId', 'downloadFolder'], }, }, { name: 'download_batch_papers', description: 'Batch download Cool Papers PDFs for results that match a query', inputSchema: { type: 'object', properties: { source: { type: 'string', enum: ['arxiv', 'venue'], description: 'Source where matching papers will be fetched', }, query: { type: 'string', description: 'Keyword query used to select papers from Cool Papers', }, downloadFolder: { type: 'string', description: 'Destination folder for the downloaded PDFs', }, maxResults: { type: 'number', description: 'Limit number of results to download (passed as Cool Papers show); use 3 for precise batches, 10-20 for regular pulls to control cost', }, skip: { type: 'number', description: 'Skip the first N results before downloading', }, sort: { type: 'number', description: 'Sorting directive (0=newest, 1=top reading stars)', }, }, required: ['source', 'query', 'downloadFolder'], }, }, { name: 'kimi_analysis', description: 'Retrieve Kimi FAQ question/answer pairs for a Cool Papers paper', inputSchema: { type: 'object', properties: { source: { type: 'string', enum: ['arxiv', 'venue'], description: 'Source of the target paper', }, paperId: { type: 'string', description: 'Paper identifier to request the Kimi analysis', }, }, required: ['source', 'paperId'], }, }, ], })); server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case 'search_papers': { const validated = SearchPapersSchema.parse(args); const searchOptions: SearchOptions = { source: validated.source, query: validated.query, maxResults: validated.maxResults, skip: validated.skip, sort: validated.sort, }; const results: SearchResult = await searchPapers(searchOptions); const payload = sanitizeSearchResult(results); return { content: [ { type: 'text', text: JSON.stringify(payload, null, 2), }, ], }; } case 'download_single_paper': { const validated = DownloadSinglePaperSchema.parse(args); const downloadOptions: DownloadOptions = { source: validated.source, paperId: validated.paperId, downloadFolder: validated.downloadFolder, }; if (validated.filename) { downloadOptions.filename = validated.filename; } const [detail, result] = await Promise.all([ fetchPaperDetail(downloadOptions.source, downloadOptions.paperId), downloadPaper(downloadOptions), ]); const payload: SingleDownloadPayload = { mode: 'single', download: { paper: sanitizePaperSummary(detail), pdfUrl: result.pdfUrl, filePath: result.filePath, fileSize: result.fileSize, }, }; return { content: [ { type: 'text', text: JSON.stringify(payload, null, 2), }, ], }; } case 'download_batch_papers': { const validated = DownloadBatchPapersSchema.parse(args); const searchOptions: SearchOptions = { source: validated.source, query: validated.query, maxResults: validated.maxResults, skip: validated.skip, sort: validated.sort, }; const searchResult = await searchPapers(searchOptions); const searchReport: BatchSearchReport = { ...sanitizeSearchResult(searchResult), params: { maxResults: validated.maxResults, skip: validated.skip, sort: validated.sort, }, }; const successes: DownloadSuccess[] = []; const failures: DownloadFailure[] = []; for (const paper of searchResult.papers) { try { const downloadOutcome = await downloadPaper({ source: validated.source, paperId: paper.id, downloadFolder: validated.downloadFolder, }); successes.push({ paper: sanitizePaperSummary(paper), pdfUrl: downloadOutcome.pdfUrl, filePath: downloadOutcome.filePath, fileSize: downloadOutcome.fileSize, }); } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error during download'; failures.push({ paper: sanitizePaperSummary(paper), error: message, }); } } const payload: BatchDownloadPayload = { mode: 'batch', search: searchReport, successes, failures, }; return { content: [ { type: 'text', text: JSON.stringify(payload, null, 2), }, ], }; } case 'kimi_analysis': { const validated = KimiAnalysisSchema.parse(args); const qas: KimiQA[] = await getKimiAnalysis( validated.source, validated.paperId ); return { content: [ { type: 'text', text: JSON.stringify( { source: validated.source, paperId: validated.paperId, faqs: qas, }, null, 2 ), }, ], }; } default: throw new Error(`Unknown tool: ${name as string}`); } } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error during tool call'; return { content: [ { type: 'text', text: `Error: ${message}`, }, ], isError: true, }; } }); await server.connect(new StdioServerTransport());

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/han-517/scholar-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server