Skip to main content
Glama

MCP-RSS-Crawler

by mshk
index.ts7.92 kB
import Database from 'better-sqlite3'; import path from 'path'; import os from 'os'; import { DbFeed, DbFeedItem } from 'shared-types'; // Database configuration const DEFAULT_DB_DIR = path.join(os.homedir(), '.mcp-rss-crawler'); const DEFAULT_DB_FILE = path.join(DEFAULT_DB_DIR, 'feeds.db'); // Database connection options export interface DbOptions { dbPath?: string; readonly?: boolean; } // Create singleton database connections let readOnlyDb: Database.Database | null = null; let writeDb: Database.Database | null = null; /** * Get a read-only database connection */ export function getDb(options: DbOptions = {}): Database.Database { const dbPath = options.dbPath || process.env.DB_FILE || DEFAULT_DB_FILE; if (!readOnlyDb) { try { readOnlyDb = new Database(dbPath, { readonly: true }); console.log(`Connected to database at ${dbPath}`); } catch (error) { console.error('Error connecting to database:', error); throw new Error('Failed to connect to the database'); } } return readOnlyDb; } /** * Get a writable database connection */ export function getWriteDb(options: DbOptions = {}): Database.Database { const dbPath = options.dbPath || process.env.DB_FILE || DEFAULT_DB_FILE; if (!writeDb) { try { writeDb = new Database(dbPath); console.log(`Connected to database for writing at ${dbPath}`); } catch (error) { console.error('Error connecting to database for writing:', error); throw new Error('Failed to connect to the database for writing'); } } return writeDb; } /** * Close all database connections */ export function closeDb(): void { if (readOnlyDb) { readOnlyDb.close(); readOnlyDb = null; } if (writeDb) { writeDb.close(); writeDb = null; } } // Feed operations export const feedOperations = { /** * Get all feeds */ getAllFeeds(options: DbOptions = {}): DbFeed[] { const db = getDb(options); return db.prepare('SELECT * FROM feeds ORDER BY category, name').all() as DbFeed[]; }, /** * Get feeds by category */ getFeedsByCategory(category: string, options: DbOptions = {}): DbFeed[] { const db = getDb(options); return db.prepare('SELECT * FROM feeds WHERE category = ? ORDER BY name').all(category) as DbFeed[]; }, /** * Get a feed by ID */ getFeedById(id: string, options: DbOptions = {}): DbFeed | null { const db = getDb(options); return db.prepare('SELECT * FROM feeds WHERE id = ?').get(id) as DbFeed | null; }, /** * Add a new feed */ addFeed(feed: Omit<DbFeed, 'id'>, options: DbOptions = {}): string { const db = getWriteDb(options); const id = crypto.randomUUID(); db.prepare(` INSERT INTO feeds (id, url, name, category, last_updated) VALUES (?, ?, ?, ?, ?) `).run(id, feed.url, feed.name, feed.category || null, feed.last_updated || Date.now()); return id; }, /** * Update a feed */ updateFeed(feed: DbFeed, options: DbOptions = {}): void { const db = getWriteDb(options); db.prepare(` UPDATE feeds SET url = ?, name = ?, category = ?, last_updated = ? WHERE id = ? `).run(feed.url, feed.name, feed.category || null, feed.last_updated, feed.id); }, /** * Delete a feed */ deleteFeed(id: string, options: DbOptions = {}): void { const db = getWriteDb(options); // Start a transaction to delete feed and its items const transaction = db.transaction(() => { // Delete items first (foreign key constraint) db.prepare('DELETE FROM items WHERE feed_id = ?').run(id); // Delete the feed db.prepare('DELETE FROM feeds WHERE id = ?').run(id); }); transaction(); } }; // Category operations export const categoryOperations = { /** * Get all categories */ getAllCategories(options: DbOptions = {}): string[] { const db = getDb(options); const categories = db.prepare('SELECT DISTINCT category FROM feeds WHERE category IS NOT NULL').all() as { category: string }[]; return categories.map(c => c.category); } }; // Item operations export const itemOperations = { /** * Get items from a specific feed */ getItemsByFeed(feedId: string, limit: number = 50, options: DbOptions = {}): DbFeedItem[] { const db = getDb(options); return db.prepare(` SELECT i.*, f.name as feed_title, f.url as feed_url FROM items i JOIN feeds f ON i.feed_id = f.id WHERE i.feed_id = ? ORDER BY i.published DESC LIMIT ? `).all(feedId, limit) as DbFeedItem[]; }, /** * Get recent items */ getRecentItems(limit: number = 50, options: DbOptions = {}): DbFeedItem[] { const db = getDb(options); return db.prepare(` SELECT i.*, f.name as feed_title, f.url as feed_url FROM items i JOIN feeds f ON i.feed_id = f.id ORDER BY i.published DESC LIMIT ? `).all(limit) as DbFeedItem[]; }, /** * Get items by category */ getItemsByCategory(category: string, limit: number = 50, options: DbOptions = {}): DbFeedItem[] { const db = getDb(options); return db.prepare(` SELECT i.*, f.name as feed_title, f.url as feed_url FROM items i JOIN feeds f ON i.feed_id = f.id WHERE f.category = ? ORDER BY i.published DESC LIMIT ? `).all(category, limit) as DbFeedItem[]; }, /** * Search items */ searchItems(query: string, limit: number = 50, options: DbOptions = {}): DbFeedItem[] { const db = getDb(options); const searchPattern = `%${query}%`; return db.prepare(` SELECT i.*, f.name as feed_title, f.url as feed_url FROM items i JOIN feeds f ON i.feed_id = f.id WHERE i.title LIKE ? OR i.summary LIKE ? OR i.content LIKE ? ORDER BY i.published DESC LIMIT ? `).all(searchPattern, searchPattern, searchPattern, limit) as DbFeedItem[]; }, /** * Get categories for an item */ getItemCategories(itemId: string, options: DbOptions = {}): string[] { const db = getDb(options); const rows = db.prepare('SELECT category FROM categories WHERE item_id = ?').all(itemId) as { category: string }[]; return rows.map(row => row.category); }, /** * Get a single item by ID */ getItemById(itemId: string, options: DbOptions = {}): DbFeedItem | null { const db = getDb(options); const item = db.prepare(` SELECT i.*, f.name as feed_title, f.url as feed_url FROM items i JOIN feeds f ON i.feed_id = f.id WHERE i.id = ? `).get(itemId) as DbFeedItem | null; if (item) { item.categories = this.getItemCategories(itemId, options); } return item; }, /** * Add a new item */ addItem(item: Omit<DbFeedItem, 'id'>, options: DbOptions = {}): string { const db = getWriteDb(options); const id = item.id || crypto.randomUUID(); // Start a transaction const transaction = db.transaction(() => { // Insert the item db.prepare(` INSERT INTO items (id, feed_id, title, link, summary, content, published, author) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `).run( id, item.feed_id, item.title, item.link || '', item.summary || null, item.content || null, item.published, item.author || null ); // Insert categories if any if (item.categories && item.categories.length > 0) { const insertCategory = db.prepare('INSERT INTO categories (item_id, category) VALUES (?, ?)'); for (const category of item.categories) { insertCategory.run(id, category); } } }); transaction(); return id; } }; // Export all operations export default { getDb, getWriteDb, closeDb, feeds: feedOperations, categories: categoryOperations, items: itemOperations };

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/mshk/mcp-rss-crawler'

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