Skip to main content
Glama

Research MCP

by gyash1512
utils.ts•5.93 kB
/** * Utility functions for the Research MCP */ import type { ArxivPaper, SemanticScholarPaper, PubMedPaper, BibTeXEntry, RateLimiter } from './types.js'; /** * Simple rate limiter implementation */ export class SimpleRateLimiter implements RateLimiter { private requests: number[] = []; private readonly maxRequests: number; private readonly windowMs: number; constructor(maxRequests: number = 3, windowSeconds: number = 1) { this.maxRequests = maxRequests; this.windowMs = windowSeconds * 1000; } async checkLimit(): Promise<void> { const now = Date.now(); // Remove requests older than the window this.requests = this.requests.filter(time => now - time < this.windowMs); if (this.requests.length >= this.maxRequests) { const oldestRequest = this.requests[0]; const waitTime = this.windowMs - (now - oldestRequest); if (waitTime > 0) { await new Promise<void>(resolve => global.setTimeout(resolve, waitTime)); // Recursively check again after waiting return this.checkLimit(); } } } incrementCount(): void { this.requests.push(Date.now()); } } /** * Remove duplicate papers based on title similarity */ export function deduplicatePapers<T extends { title: string }>(papers: T[]): T[] { const seen = new Set<string>(); const unique: T[] = []; for (const paper of papers) { const normalizedTitle = normalizeTitle(paper.title); if (!seen.has(normalizedTitle)) { seen.add(normalizedTitle); unique.push(paper); } } return unique; } /** * Normalize title for comparison */ function normalizeTitle(title: string): string { return title .toLowerCase() .replace(/[^\w\s]/g, '') .replace(/\s+/g, ' ') .trim(); } /** * Generate BibTeX entry for arXiv paper */ export function arxivToBibTeX(paper: ArxivPaper): string { const year = new Date(paper.published).getFullYear(); const key = generateBibTeXKey(paper.authors[0] || 'Unknown', year, paper.title); const authors = paper.authors.join(' and '); const entry: BibTeXEntry = { type: 'article', key, fields: { title: `{${paper.title}}`, author: authors, year: year.toString(), eprint: paper.id, archivePrefix: 'arXiv', primaryClass: paper.primaryCategory, url: paper.url, abstract: `{${paper.abstract}}` } }; return formatBibTeX(entry); } /** * Generate BibTeX entry for Semantic Scholar paper */ export function semanticScholarToBibTeX(paper: SemanticScholarPaper): string { const year = paper.year || new Date().getFullYear(); const firstAuthor = paper.authors[0]?.name || 'Unknown'; const key = generateBibTeXKey(firstAuthor, year, paper.title); const authors = paper.authors.map(a => a.name).join(' and '); const entry: BibTeXEntry = { type: 'article', key, fields: { title: `{${paper.title}}`, author: authors, year: year.toString(), url: paper.url } }; if (paper.abstract) { entry.fields.abstract = `{${paper.abstract}}`; } if (paper.venue) { entry.fields.journal = paper.venue; } return formatBibTeX(entry); } /** * Generate BibTeX entry for PubMed paper */ export function pubmedToBibTeX(paper: PubMedPaper): string { const key = generateBibTeXKey(paper.authors[0] || 'Unknown', parseInt(paper.year), paper.title); const authors = paper.authors.join(' and '); const entry: BibTeXEntry = { type: 'article', key, fields: { title: `{${paper.title}}`, author: authors, year: paper.year, journal: paper.journal, url: paper.url, note: `PMID: ${paper.pmid}` } }; if (paper.abstract) { entry.fields.abstract = `{${paper.abstract}}`; } if (paper.doi) { entry.fields.doi = paper.doi; } return formatBibTeX(entry); } /** * Generate BibTeX citation key */ function generateBibTeXKey(author: string, year: number, title: string): string { const authorPart = author.split(' ').pop()?.toLowerCase() || 'unknown'; const titleWords = title.toLowerCase().split(' ').filter(w => w.length > 3); const titlePart = titleWords[0] || 'paper'; return `${authorPart}${year}${titlePart}`; } /** * Format BibTeX entry to string */ function formatBibTeX(entry: BibTeXEntry): string { let result = `@${entry.type}{${entry.key},\n`; for (const [key, value] of Object.entries(entry.fields)) { result += ` ${key} = ${value},\n`; } result += '}\n'; return result; } /** * Sanitize user input for API queries */ export function sanitizeQuery(query: string): string { return query.trim().slice(0, 500); } /** * Format error message for MCP response */ export function formatError(error: unknown): string { if (error instanceof Error) { return error.message; } return String(error); } /** * Parse year range from query string */ export function parseYearRange(query: string): { startYear?: number; endYear?: number } { const yearMatch = query.match(/(?:after|since|from)\s+(\d{4})|(?:before|until|to)\s+(\d{4})|(\d{4})-(\d{4})/i); if (!yearMatch) { return {}; } const result: { startYear?: number; endYear?: number } = {}; if (yearMatch[1]) { result.startYear = parseInt(yearMatch[1]); } if (yearMatch[2]) { result.endYear = parseInt(yearMatch[2]); } if (yearMatch[3] && yearMatch[4]) { result.startYear = parseInt(yearMatch[3]); result.endYear = parseInt(yearMatch[4]); } return result; } /** * Truncate text to specified length */ export function truncateText(text: string, maxLength: number = 500): string { if (text.length <= maxLength) { return text; } return text.slice(0, maxLength - 3) + '...'; } /** * Sleep utility for rate limiting */ export async function sleep(ms: number): Promise<void> { await new Promise<void>(resolve => global.setTimeout(resolve, ms)); }

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/gyash1512/ResearchMCP'

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