Skip to main content
Glama

MCP RSS

by ronnycoding
McpService.ts•21.7 kB
import { AppDataSource } from '../config/database'; import { Article, ArticleStatus } from '../entities/Article'; import { Feed } from '../entities/Feed'; import { ILike, Between, In } from 'typeorm'; import { EmbeddingService } from './EmbeddingService'; interface ArticleItem { id: number; title: string; content?: string; // Optional - only included when includeContent=true link: string; pubDate: string; fetchDate: string; status: string; feedTitle: string; feedCategory: string; excerpt?: string; similarity?: number; } interface ContentResponse { articles: ArticleItem[]; total?: number; success: boolean; message?: string; } interface TagResponse { success: boolean; message?: string; } interface SourcesResponse { sources: { id: number; title: string; category: string; url: string; isFavorite?: boolean; }[]; total?: number; success: boolean; message?: string; } export class McpService { private embeddingService: EmbeddingService | null = null; constructor() { // Initialize embedding service only if API key is available and not empty if (process.env.OPENAI_API_KEY && process.env.OPENAI_API_KEY.trim() !== '') { try { this.embeddingService = new EmbeddingService(); } catch (error) { console.error('Failed to initialize EmbeddingService:', error); // Service remains null, semantic search will be disabled } } } /** * Get all RSS feed sources with pagination * @param limit - Number of sources to return (default: 50) * @param offset - Offset for pagination (default: 0) * @param favoritesOnly - Only show favorite blogs (default: false) * @param category - Filter by category * @returns List of feed sources with pagination */ async get_sources( limit: number = 50, offset: number = 0, favoritesOnly?: boolean, category?: string ): Promise<SourcesResponse> { try { const feedRepository = AppDataSource.getRepository(Feed); const queryBuilder = feedRepository.createQueryBuilder('feed') .orderBy('feed.isFavorite', 'DESC') // Favorite blogs first .addOrderBy('feed.title', 'ASC') .skip(offset) .take(limit); // Filter by favorites if requested if (favoritesOnly) { queryBuilder.where('feed.isFavorite = :isFavorite', { isFavorite: true }); } // Filter by category if requested if (category) { const whereMethod = favoritesOnly ? 'andWhere' : 'where'; queryBuilder[whereMethod]('feed.category ILIKE :category', { category: `%${category}%` }); } const [feeds, total] = await queryBuilder.getManyAndCount(); const formattedSources = feeds.map(feed => ({ id: feed.id, title: feed.title, category: feed.category, url: feed.url, isFavorite: feed.isFavorite })); return { sources: formattedSources, total, success: true }; } catch (error) { return { sources: [], success: false, message: `Failed to get sources: ${error instanceof Error ? error.message : 'Unknown error'}` }; } } /** * Get article content with basic filtering * @param statuses - Article status filter (can be multiple) * @param limit - Number of articles to return * @param offset - Offset for pagination * @param source - Feed source filter * @param favoriteBlogsOnly - Only show articles from favorite blogs * @param prioritizeFavoriteBlogs - Show favorite blog articles first * @param includeContent - Include full content in response (default: false) * @param includeExcerpt - Include excerpt in response (default: false) * @param startDate - Start date for filtering (ISO string) * @param endDate - End date for filtering (ISO string) * @returns Article list with pagination */ async get_content( statuses?: ArticleStatus[], limit: number = 10, offset: number = 0, source?: string, favoriteBlogsOnly?: boolean, prioritizeFavoriteBlogs?: boolean, includeContent: boolean = false, includeExcerpt: boolean = false, startDate?: string, endDate?: string ): Promise<ContentResponse> { try { const articleRepository = AppDataSource.getRepository(Article); const queryBuilder = articleRepository.createQueryBuilder('article') .leftJoinAndSelect('article.feed', 'feed'); // Order by favorite blogs first if requested, then by pubDate if (prioritizeFavoriteBlogs) { queryBuilder .orderBy('feed.isFavorite', 'DESC') .addOrderBy('article.pubDate', 'DESC'); } else { queryBuilder.orderBy('article.pubDate', 'DESC'); } queryBuilder .skip(offset) .take(limit); if (statuses && statuses.length > 0) { queryBuilder.where('article.status IN (:...statuses)', { statuses }); } if (source) { const whereMethod = statuses && statuses.length > 0 ? 'andWhere' : 'where'; queryBuilder[whereMethod]('feed.title = :source', { source }); } if (favoriteBlogsOnly) { const whereMethod = (statuses && statuses.length > 0) || source ? 'andWhere' : 'where'; queryBuilder[whereMethod]('feed.isFavorite = :isFavorite', { isFavorite: true }); } // Date range filter if (startDate && endDate) { const whereMethod = (statuses && statuses.length > 0) || source || favoriteBlogsOnly ? 'andWhere' : 'where'; queryBuilder[whereMethod]('article.pubDate BETWEEN :startDate AND :endDate', { startDate, endDate }); } else if (startDate) { const whereMethod = (statuses && statuses.length > 0) || source || favoriteBlogsOnly ? 'andWhere' : 'where'; queryBuilder[whereMethod]('article.pubDate >= :startDate', { startDate }); } else if (endDate) { const whereMethod = (statuses && statuses.length > 0) || source || favoriteBlogsOnly ? 'andWhere' : 'where'; queryBuilder[whereMethod]('article.pubDate <= :endDate', { endDate }); } const [articles, total] = await queryBuilder.getManyAndCount(); const formattedArticles = articles.map(article => { const item: ArticleItem = { id: article.id, title: article.title, link: article.link, pubDate: article.pubDate.toISOString(), fetchDate: article.fetchDate.toISOString(), status: article.status, feedTitle: article.feed?.title || '', feedCategory: article.feed?.category || '' }; // Only include excerpt if requested if (includeExcerpt) { item.excerpt = this.createExcerpt(article.content); } // Only include full content if requested if (includeContent) { item.content = article.content; } return item; }); return { articles: formattedArticles, total, success: true }; } catch (error) { return { articles: [], success: false, message: `Failed to get articles: ${error instanceof Error ? error.message : 'Unknown error'}` }; } } /** * Advanced search with keyword, date range, category, and status filtering * @param keyword - Search term for title/content * @param category - Feed category filter * @param statuses - Article statuses to include * @param startDate - Start date for filtering (ISO string) * @param endDate - End date for filtering (ISO string) * @param limit - Number of results * @param offset - Offset for pagination * @param favoriteBlogsOnly - Only show articles from favorite blogs * @param prioritizeFavoriteBlogs - Show favorite blog articles first * @param includeContent - Include full content in response (default: false) * @returns Filtered articles */ async search_articles( keyword?: string, category?: string, statuses?: ArticleStatus[], startDate?: string, endDate?: string, limit: number = 20, offset: number = 0, favoriteBlogsOnly?: boolean, prioritizeFavoriteBlogs?: boolean, includeContent: boolean = false ): Promise<ContentResponse> { try { const articleRepository = AppDataSource.getRepository(Article); const queryBuilder = articleRepository.createQueryBuilder('article') .leftJoinAndSelect('article.feed', 'feed'); // Order by favorite blogs first if requested, then by pubDate if (prioritizeFavoriteBlogs) { queryBuilder .orderBy('feed.isFavorite', 'DESC') .addOrderBy('article.pubDate', 'DESC'); } else { queryBuilder.orderBy('article.pubDate', 'DESC'); } queryBuilder .skip(offset) .take(limit); const conditions: string[] = []; const parameters: any = {}; // Keyword search in title and content if (keyword) { conditions.push('(article.title ILIKE :keyword OR article.content ILIKE :keyword)'); parameters.keyword = `%${keyword}%`; } // Category filter if (category) { conditions.push('feed.category ILIKE :category'); parameters.category = `%${category}%`; } // Status filter if (statuses && statuses.length > 0) { conditions.push('article.status IN (:...statuses)'); parameters.statuses = statuses; } // Date range filter if (startDate && endDate) { conditions.push('article.pubDate BETWEEN :startDate AND :endDate'); parameters.startDate = startDate; parameters.endDate = endDate; } else if (startDate) { conditions.push('article.pubDate >= :startDate'); parameters.startDate = startDate; } else if (endDate) { conditions.push('article.pubDate <= :endDate'); parameters.endDate = endDate; } // Favorite blogs filter if (favoriteBlogsOnly) { conditions.push('feed.isFavorite = :isFavorite'); parameters.isFavorite = true; } if (conditions.length > 0) { queryBuilder.where(conditions.join(' AND '), parameters); } const [articles, total] = await queryBuilder.getManyAndCount(); const formattedArticles = articles.map(article => { const item: ArticleItem = { id: article.id, title: article.title, link: article.link, pubDate: article.pubDate.toISOString(), fetchDate: article.fetchDate.toISOString(), status: article.status, feedTitle: article.feed?.title || '', feedCategory: article.feed?.category || '', excerpt: this.createExcerpt(article.content) }; // Only include full content if requested if (includeContent) { item.content = article.content; } return item; }); return { articles: formattedArticles, total, success: true }; } catch (error) { return { articles: [], success: false, message: `Search failed: ${error instanceof Error ? error.message : 'Unknown error'}` }; } } /** * Semantic search using vector embeddings * @param query - Natural language search query * @param limit - Number of results * @param statuses - Filter by article statuses * @param category - Filter by category * @param includeContent - Include full content in response (default: false) * @returns Semantically similar articles */ async semantic_search( query: string, limit: number = 10, statuses?: ArticleStatus[], category?: string, includeContent: boolean = false ): Promise<ContentResponse> { if (!this.embeddingService) { return { articles: [], success: false, message: 'Semantic search is not available. Please set OPENAI_API_KEY environment variable.' }; } try { // Generate embedding for the query const queryEmbedding = await this.embeddingService.generateEmbedding(query); const embeddingString = `[${queryEmbedding.join(',')}]`; const articleRepository = AppDataSource.getRepository(Article); // Build query with vector similarity search let queryBuilder = articleRepository.createQueryBuilder('article') .leftJoinAndSelect('article.feed', 'feed') .where('article.embedding IS NOT NULL') .orderBy('article.embedding <=> :embedding', 'ASC') .setParameter('embedding', embeddingString) .take(limit); // Apply additional filters if (statuses && statuses.length > 0) { queryBuilder = queryBuilder.andWhere('article.status IN (:...statuses)', { statuses }); } if (category) { queryBuilder = queryBuilder.andWhere('feed.category ILIKE :category', { category: `%${category}%` }); } const articles = await queryBuilder.getMany(); const formattedArticles = articles.map(article => { const item: ArticleItem = { id: article.id, title: article.title, link: article.link, pubDate: article.pubDate.toISOString(), fetchDate: article.fetchDate.toISOString(), status: article.status, feedTitle: article.feed?.title || '', feedCategory: article.feed?.category || '', excerpt: this.createExcerpt(article.content) }; // Only include full content if requested if (includeContent) { item.content = article.content; } return item; }); return { articles: formattedArticles, success: true }; } catch (error) { return { articles: [], success: false, message: `Semantic search failed: ${error instanceof Error ? error.message : 'Unknown error'}` }; } } /** * Get daily digest of unread articles grouped by category * @param limit - Max articles per category * @param includeContent - Include full content in response (default: false) * @returns Unread articles grouped by category */ async get_daily_digest(limit: number = 5, includeContent: boolean = false): Promise<ContentResponse> { try { const articleRepository = AppDataSource.getRepository(Article); // Get all unread articles published today, grouped by category const today = new Date(); today.setHours(0, 0, 0, 0); const articles = await articleRepository.createQueryBuilder('article') .leftJoinAndSelect('article.feed', 'feed') .where('article.status = :status', { status: ArticleStatus.UNREAD }) .andWhere('article.pubDate >= :today', { today: today.toISOString() }) .orderBy('feed.category', 'ASC') .addOrderBy('article.pubDate', 'DESC') .getMany(); // Group by category and limit per category const grouped = new Map<string, typeof articles>(); articles.forEach(article => { const category = article.feed?.category || 'Uncategorized'; if (!grouped.has(category)) { grouped.set(category, []); } const categoryArticles = grouped.get(category)!; if (categoryArticles.length < limit) { categoryArticles.push(article); } }); // Flatten and format const formattedArticles: ArticleItem[] = []; grouped.forEach(categoryArticles => { categoryArticles.forEach(article => { const item: ArticleItem = { id: article.id, title: article.title, link: article.link, pubDate: article.pubDate.toISOString(), fetchDate: article.fetchDate.toISOString(), status: article.status, feedTitle: article.feed?.title || '', feedCategory: article.feed?.category || 'Uncategorized', excerpt: this.createExcerpt(article.content) }; // Only include full content if requested if (includeContent) { item.content = article.content; } formattedArticles.push(item); }); }); return { articles: formattedArticles, total: formattedArticles.length, success: true }; } catch (error) { return { articles: [], success: false, message: `Failed to get daily digest: ${error instanceof Error ? error.message : 'Unknown error'}` }; } } /** * Get favorite articles from the last week (titles only) * @returns Favorite articles from last 7 days with titles only */ async get_weekly_favorites(): Promise<ContentResponse> { try { const articleRepository = AppDataSource.getRepository(Article); // Calculate date 7 days ago const oneWeekAgo = new Date(); oneWeekAgo.setDate(oneWeekAgo.getDate() - 7); const articles = await articleRepository.createQueryBuilder('article') .leftJoinAndSelect('article.feed', 'feed') .where('article.status = :status', { status: ArticleStatus.FAVORITE }) .andWhere('article.pubDate >= :oneWeekAgo', { oneWeekAgo: oneWeekAgo.toISOString() }) .orderBy('article.pubDate', 'DESC') .getMany(); // Return only titles and minimal metadata const formattedArticles = articles.map(article => ({ id: article.id, title: article.title, link: article.link, pubDate: article.pubDate.toISOString(), fetchDate: article.fetchDate.toISOString(), status: article.status, feedTitle: article.feed?.title || '', feedCategory: article.feed?.category || '' })); return { articles: formattedArticles, total: formattedArticles.length, success: true }; } catch (error) { return { articles: [], success: false, message: `Failed to get weekly favorites: ${error instanceof Error ? error.message : 'Unknown error'}` }; } } /** * Get full article content by ID * @param articleId - Article ID * @returns Full article with content */ async get_article_full(articleId: number): Promise<ContentResponse> { try { const articleRepository = AppDataSource.getRepository(Article); const article = await articleRepository.findOne({ where: { id: articleId }, relations: ['feed'] }); if (!article) { return { articles: [], success: false, message: `Article ID ${articleId} not found` }; } const formattedArticle: ArticleItem = { id: article.id, title: article.title, content: article.content, link: article.link, pubDate: article.pubDate.toISOString(), fetchDate: article.fetchDate.toISOString(), status: article.status, feedTitle: article.feed?.title || '', feedCategory: article.feed?.category || '', excerpt: this.createExcerpt(article.content) }; return { articles: [formattedArticle], success: true }; } catch (error) { return { articles: [], success: false, message: `Failed to get article: ${error instanceof Error ? error.message : 'Unknown error'}` }; } } /** * Set article status/tag * @param articleId - Article ID * @param status - New status * @returns Operation result */ async set_tag(articleId: number, status: ArticleStatus): Promise<TagResponse> { try { const articleRepository = AppDataSource.getRepository(Article); const article = await articleRepository.findOne({ where: { id: articleId } }); if (!article) { return { success: false, message: `Article ID ${articleId} not found` }; } article.status = status; await articleRepository.save(article); return { success: true, message: `Article ${articleId} status updated to ${status}` }; } catch (error) { return { success: false, message: `Failed to update article status: ${error instanceof Error ? error.message : 'Unknown error'}` }; } } /** * Set blog/feed as favorite * @param feedId - Feed ID to update * @param isFavorite - Whether to mark as favorite * @returns Operation result */ async set_favorite_blog(feedId: number, isFavorite: boolean): Promise<TagResponse> { try { const feedRepository = AppDataSource.getRepository(Feed); const feed = await feedRepository.findOne({ where: { id: feedId } }); if (!feed) { return { success: false, message: `Blog/Feed ID ${feedId} not found` }; } feed.isFavorite = isFavorite; await feedRepository.save(feed); return { success: true, message: `Blog '${feed.title}' ${isFavorite ? 'marked as favorite' : 'removed from favorites'}` }; } catch (error) { return { success: false, message: `Failed to update blog favorite status: ${error instanceof Error ? error.message : 'Unknown error'}` }; } } /** * Create excerpt from content * @param content - Full content * @returns Excerpt (first 200 chars) */ private createExcerpt(content: string): string { const plainText = content.replace(/<[^>]*>/g, '').trim(); return plainText.length > 200 ? plainText.substring(0, 200) + '...' : plainText; } }

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/ronnycoding/my_mcp_rss'

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