UNPKG

periodicos-capes-mcp

Version:

MCP server para consulta de periódicos científicos do Portal de Periódicos CAPES

282 lines (281 loc) 14.3 kB
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js'; import { CAPESScraper } from './scraper.js'; import { DOCUMENT_TYPES, LANGUAGES } from './types.js'; import { RISExporter } from './ris-exporter.js'; class CAPESMCPServer { server; scraper; constructor() { this.server = new Server({ name: 'periodicos-capes-mcp', version: '1.0.0', }, { capabilities: { tools: {}, }, }); this.scraper = new CAPESScraper(); this.setupToolHandlers(); } setupToolHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: 'search_capes', description: 'Search for articles in the CAPES Periodicals Portal', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query string', }, max_pages: { type: 'number', description: 'Maximum number of pages to search (default: all)', minimum: 1, }, max_results: { type: 'number', description: 'Maximum number of results to return (default: all found)', minimum: 1, }, full_details: { type: 'boolean', description: 'Whether to fetch full article details (default: false)', default: false, }, max_workers: { type: 'number', description: 'Maximum number of concurrent workers (default: 5)', minimum: 1, maximum: 20, default: 5, }, timeout: { type: 'number', description: 'Request timeout in milliseconds (default: 30000)', minimum: 5000, default: 30000, }, advanced: { type: 'boolean', description: 'Use advanced search syntax (default: true)', default: true, }, include_metrics: { type: 'boolean', description: 'Include citation and journal quality metrics (OpenAlex + Qualis) (default: false)', default: false, }, document_types: { type: 'array', description: 'Filter by document types', items: { type: 'string', enum: DOCUMENT_TYPES } }, open_access_only: { type: 'boolean', description: 'Filter by open access (true = only open access, false = only non-open access, undefined = all)' }, peer_reviewed_only: { type: 'boolean', description: 'Filter by peer review status (true = only peer reviewed, false = only non-peer reviewed, undefined = all)' }, year_min: { type: 'number', description: 'Minimum publication year', minimum: 1800, maximum: 2030 }, year_max: { type: 'number', description: 'Maximum publication year', minimum: 1800, maximum: 2030 }, languages: { type: 'array', description: 'Filter by languages', items: { type: 'string', enum: LANGUAGES } }, export_ris: { type: 'boolean', description: 'Export results to RIS bibliographic format file (default: false)', default: false }, ris_output_dir: { type: 'string', description: 'Output directory for RIS file when export_ris is true (default: current working directory)' }, ris_return_content: { type: 'boolean', description: 'Include RIS content in response when export_ris is true (default: false)', default: false }, show_metadata_only: { type: 'boolean', description: 'Return only search metadata without articles content to save tokens (default: false)', default: false } }, required: ['query'], }, }, { name: 'get_article_details', description: 'Get detailed metadata for a specific article by ID', inputSchema: { type: 'object', properties: { article_id: { type: 'string', description: 'CAPES article ID', }, timeout: { type: 'number', description: 'Request timeout in milliseconds (default: 30000)', minimum: 5000, default: 30000, }, }, required: ['article_id'], }, }, ], }; }); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { if (name === 'search_capes') { if (!args) { throw new McpError(ErrorCode.InvalidParams, 'Arguments are required'); } const exportRis = args.export_ris || false; const risOutputDir = args.ris_output_dir; const risReturnContent = args.ris_return_content || false; const showMetadataOnly = args.show_metadata_only || false; const options = { query: args.query, max_pages: args.max_pages, max_results: args.max_results, // Force full_details if export_ris is true (needed for RIS export) // Force full_details if export_ris is true (needed for RIS export) full_details: args.full_details || exportRis, max_workers: args.max_workers || 5, timeout: args.timeout || 30000, advanced: args.advanced !== false, include_metrics: args.include_metrics || false, document_types: args.document_types, open_access_only: args.open_access_only, peer_reviewed_only: args.peer_reviewed_only, year_min: args.year_min, year_max: args.year_max, languages: args.languages, }; if (!options.query || typeof options.query !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'Query parameter is required and must be a string'); } const result = await this.scraper.search(options); // Handle RIS export if requested if (exportRis && 'articles' in result && result.articles && result.articles.length > 0) { const risResult = RISExporter.exportToRISFile(result.articles, risOutputDir); // Create response object with both search results and RIS export info const responseData = { ...result, ris_export: { file_path: risResult.file_path, article_count: risResult.article_count, file_size_bytes: risResult.file_size_bytes, created_at: risResult.created_at, ...(risReturnContent && { content: RISExporter.exportToRIS(result.articles) }) } }; // If show_metadata_only, remove articles from response to save tokens if (showMetadataOnly) { const { articles, ...metadataResponse } = responseData; return { content: [ { type: 'text', text: JSON.stringify(metadataResponse, null, 2), }, ], }; } return { content: [ { type: 'text', text: JSON.stringify(responseData, null, 2), }, ], }; } // If show_metadata_only without export, return only metadata if (showMetadataOnly) { const { articles, ...metadataResult } = result; return { content: [ { type: 'text', text: JSON.stringify(metadataResult, null, 2), }, ], }; } return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; } if (name === 'get_article_details') { if (!args) { throw new McpError(ErrorCode.InvalidParams, 'Arguments are required'); } const articleId = args.article_id; const timeout = args.timeout || 30000; if (!articleId || typeof articleId !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'article_id parameter is required and must be a string'); } const details = await this.scraper.scrapeArticleDetail(articleId, timeout); return { content: [ { type: 'text', text: JSON.stringify(details, null, 2), }, ], }; } throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`); } catch (error) { if (error instanceof McpError) { throw error; } throw new McpError(ErrorCode.InternalError, `Error executing ${name}: ${error instanceof Error ? error.message : String(error)}`); } }); } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); } } const server = new CAPESMCPServer(); server.run().catch(console.error);