@0xquinto/rss-mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,99 @@
1
+ # RSS MCP Server
2
+
3
+ An MCP server that lets AI assistants subscribe to, fetch, search, and manage RSS feeds. Built with TypeScript and SQLite with full-text search.
4
+
5
+ ## Inspiration
6
+
7
+ Inspired by [Andrej Karpathy's post](https://x.com/karpathy/status/2018043254986703167) on reclaiming your information diet:
8
+
9
+ > "Finding myself going back to RSS/Atom feeds a lot more recently. There's a lot more higher quality longform and a lot less slop intended to provoke... We should bring back RSS - it's open, pervasive, hackable."
10
+
11
+ **Quick start with curated feeds**: Import the [Most Popular Blogs of Hacker News 2025](https://gist.github.com/emschwartz/e6d2bf860ccc367fe37ff953ba6de66b) OPML file to get 92 high-quality tech blogs.
12
+
13
+ ## Features
14
+
15
+ - Subscribe to RSS/Atom feeds and fetch new posts
16
+ - Full-text search across titles, summaries, and content (FTS5)
17
+ - Extract clean article content from web pages (Readability)
18
+ - Import feeds in bulk from OPML files
19
+ - Track read/unread state
20
+ - Daily digest for compact summaries
21
+ - HackerNews popularity ranking for posts
22
+ - Conditional HTTP requests (ETag/Last-Modified) and per-feed rate limiting
23
+
24
+ ## Installation
25
+
26
+ ### As a Claude Code Plugin (Recommended)
27
+
28
+ ```bash
29
+ /plugin marketplace add 0xQuinto/rss-mcp
30
+ /plugin install rss-mcp@0xquinto-rss-mcp
31
+ ```
32
+
33
+ ### Via bunx
34
+
35
+ ```bash
36
+ bunx @0xquinto/rss-mcp
37
+ ```
38
+
39
+ ### Via npx
40
+
41
+ ```bash
42
+ npx @0xquinto/rss-mcp
43
+ ```
44
+
45
+ ## Tools
46
+
47
+ | Tool | Description |
48
+ | ------------------- | ------------------------------------------------------------------ |
49
+ | `list_feeds` | List all subscribed feeds |
50
+ | `add_feed` | Subscribe to a feed by URL |
51
+ | `remove_feed` | Unsubscribe and delete all posts for a feed |
52
+ | `import_opml` | Bulk import feeds from an OPML file |
53
+ | `refresh_feeds` | Fetch latest posts (all feeds or a specific one) |
54
+ | `get_posts` | Query posts with filtering, pagination, and full-text search |
55
+ | `get_post_content` | Retrieve full article content (fetched and cached on first access) |
56
+ | `get_daily_digest` | Get compact digest of recent posts for synthesis |
57
+ | `get_popular_posts` | Rank recent posts by HackerNews engagement |
58
+ | `mark_read` | Mark posts as read |
59
+ | `mark_unread` | Mark posts as unread |
60
+
61
+ ## Data
62
+
63
+ Posts are stored in `~/.rss-mcp/rss.db` (SQLite). The database and FTS5 index are created automatically on first run.
64
+
65
+ ## MCP Configuration
66
+
67
+ ### Claude Code
68
+
69
+ Add to your project or user MCP settings:
70
+
71
+ ```json
72
+ {
73
+ "mcpServers": {
74
+ "rss-mcp": {
75
+ "command": "bunx",
76
+ "args": ["@0xquinto/rss-mcp"]
77
+ }
78
+ }
79
+ }
80
+ ```
81
+
82
+ ### Claude Desktop
83
+
84
+ Add to `claude_desktop_config.json`:
85
+
86
+ ```json
87
+ {
88
+ "mcpServers": {
89
+ "rss-mcp": {
90
+ "command": "bunx",
91
+ "args": ["@0xquinto/rss-mcp"]
92
+ }
93
+ }
94
+ }
95
+ ```
96
+
97
+ ## License
98
+
99
+ MIT
@@ -0,0 +1,3 @@
1
+ export declare function extractContent(html: string): string | null;
2
+ export declare function fetchAndExtract(url: string): Promise<string | null>;
3
+ //# sourceMappingURL=content.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content.d.ts","sourceRoot":"","sources":["../src/content.ts"],"names":[],"mappings":"AAGA,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAU1D;AAED,wBAAsB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAiBzE"}
@@ -0,0 +1,33 @@
1
+ import { Readability } from "@mozilla/readability";
2
+ import { parseHTML } from "linkedom";
3
+ export function extractContent(html) {
4
+ try {
5
+ const { document } = parseHTML(html);
6
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
7
+ const reader = new Readability(document);
8
+ const article = reader.parse();
9
+ return article?.textContent ?? null;
10
+ }
11
+ catch {
12
+ return null;
13
+ }
14
+ }
15
+ export async function fetchAndExtract(url) {
16
+ try {
17
+ const response = await fetch(url, {
18
+ redirect: "follow",
19
+ signal: AbortSignal.timeout(30000),
20
+ headers: {
21
+ "User-Agent": "Mozilla/5.0 (compatible; RSS-MCP/1.0)",
22
+ },
23
+ });
24
+ if (!response.ok)
25
+ return null;
26
+ const html = await response.text();
27
+ return extractContent(html);
28
+ }
29
+ catch {
30
+ return null;
31
+ }
32
+ }
33
+ //# sourceMappingURL=content.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content.js","sourceRoot":"","sources":["../src/content.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAErC,MAAM,UAAU,cAAc,CAAC,IAAY;IACzC,IAAI,CAAC;QACH,MAAM,EAAE,QAAQ,EAAE,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;QACrC,8DAA8D;QAC9D,MAAM,MAAM,GAAG,IAAI,WAAW,CAAC,QAAe,CAAC,CAAC;QAChD,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,EAAE,CAAC;QAC/B,OAAO,OAAO,EAAE,WAAW,IAAI,IAAI,CAAC;IACtC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,GAAW;IAC/C,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;YAChC,QAAQ,EAAE,QAAQ;YAClB,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,KAAK,CAAC;YAClC,OAAO,EAAE;gBACP,YAAY,EAAE,uCAAuC;aACtD;SACF,CAAC,CAAC;QAEH,IAAI,CAAC,QAAQ,CAAC,EAAE;YAAE,OAAO,IAAI,CAAC;QAE9B,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;QACnC,OAAO,cAAc,CAAC,IAAI,CAAC,CAAC;IAC9B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
package/dist/db.d.ts ADDED
@@ -0,0 +1,69 @@
1
+ import Database from "better-sqlite3";
2
+ export declare function getDb(): Database.Database;
3
+ export interface Feed {
4
+ id: number;
5
+ url: string;
6
+ title: string | null;
7
+ site_url: string | null;
8
+ last_fetched: string | null;
9
+ etag: string | null;
10
+ last_modified: string | null;
11
+ created_at: string;
12
+ }
13
+ export interface Post {
14
+ id: number;
15
+ feed_id: number;
16
+ guid: string;
17
+ title: string | null;
18
+ url: string | null;
19
+ summary: string | null;
20
+ content: string | null;
21
+ author: string | null;
22
+ published_at: string | null;
23
+ fetched_at: string;
24
+ is_read: number;
25
+ read_at: string | null;
26
+ starred: number;
27
+ feed_title?: string;
28
+ feed_url?: string;
29
+ }
30
+ export declare function addFeed(url: string, title?: string, siteUrl?: string): Feed;
31
+ export declare function listFeeds(): Feed[];
32
+ export declare function removeFeed(feedId: number): boolean;
33
+ export declare function getFeed(feedId: number): Feed | null;
34
+ export declare function updateFeedMeta(feedId: number, title: string | null, siteUrl: string | null, etag: string | null, lastModified: string | null): void;
35
+ export interface PostEntry {
36
+ guid: string;
37
+ title?: string;
38
+ url?: string;
39
+ summary?: string;
40
+ author?: string;
41
+ published_at?: string;
42
+ }
43
+ export declare function upsertPosts(feedId: number, entries: PostEntry[]): number;
44
+ export interface GetPostsOptions {
45
+ feedId?: number;
46
+ limit?: number;
47
+ offset?: number;
48
+ unreadOnly?: boolean;
49
+ search?: string;
50
+ since?: string;
51
+ }
52
+ export declare function getPosts(options?: GetPostsOptions): Post[];
53
+ export declare function markRead(postIds: number[]): number;
54
+ export declare function markUnread(postIds: number[]): number;
55
+ export interface DigestPost {
56
+ id: number;
57
+ feed: string;
58
+ title: string | null;
59
+ summary: string | null;
60
+ url: string | null;
61
+ published_at: string | null;
62
+ }
63
+ export declare function getDailyDigest(hours?: number, maxSummaryLength?: number): DigestPost[];
64
+ export declare function updatePostContent(postId: number, content: string): void;
65
+ export interface PostContent extends Post {
66
+ truncated: boolean;
67
+ }
68
+ export declare function getPostContent(postId: number, full?: boolean): PostContent | null;
69
+ //# sourceMappingURL=db.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"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"}
package/dist/db.js ADDED
@@ -0,0 +1,213 @@
1
+ import Database from "better-sqlite3";
2
+ import { homedir } from "os";
3
+ import { join } from "path";
4
+ import { mkdirSync, existsSync } from "fs";
5
+ const DB_DIR = join(homedir(), ".rss-mcp");
6
+ const DB_PATH = join(DB_DIR, "rss.db");
7
+ const SCHEMA = `
8
+ CREATE TABLE IF NOT EXISTS feeds (
9
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
10
+ url TEXT UNIQUE NOT NULL,
11
+ title TEXT,
12
+ site_url TEXT,
13
+ last_fetched TEXT,
14
+ etag TEXT,
15
+ last_modified TEXT,
16
+ created_at TEXT DEFAULT (datetime('now'))
17
+ );
18
+
19
+ CREATE TABLE IF NOT EXISTS posts (
20
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
21
+ feed_id INTEGER NOT NULL REFERENCES feeds(id) ON DELETE CASCADE,
22
+ guid TEXT NOT NULL,
23
+ title TEXT,
24
+ url TEXT,
25
+ summary TEXT,
26
+ content TEXT,
27
+ author TEXT,
28
+ published_at TEXT,
29
+ fetched_at TEXT DEFAULT (datetime('now')),
30
+ is_read INTEGER DEFAULT 0,
31
+ read_at TEXT,
32
+ starred INTEGER DEFAULT 0,
33
+ UNIQUE(feed_id, guid)
34
+ );
35
+
36
+ CREATE VIRTUAL TABLE IF NOT EXISTS posts_fts USING fts5(
37
+ title, summary, content,
38
+ content='posts',
39
+ content_rowid='id'
40
+ );
41
+
42
+ CREATE TRIGGER IF NOT EXISTS posts_ai AFTER INSERT ON posts BEGIN
43
+ INSERT INTO posts_fts(rowid, title, summary, content)
44
+ VALUES (new.id, new.title, new.summary, new.content);
45
+ END;
46
+
47
+ CREATE TRIGGER IF NOT EXISTS posts_ad AFTER DELETE ON posts BEGIN
48
+ INSERT INTO posts_fts(posts_fts, rowid, title, summary, content)
49
+ VALUES ('delete', old.id, old.title, old.summary, old.content);
50
+ END;
51
+
52
+ CREATE TRIGGER IF NOT EXISTS posts_au AFTER UPDATE ON posts BEGIN
53
+ INSERT INTO posts_fts(posts_fts, rowid, title, summary, content)
54
+ VALUES ('delete', old.id, old.title, old.summary, old.content);
55
+ INSERT INTO posts_fts(rowid, title, summary, content)
56
+ VALUES (new.id, new.title, new.summary, new.content);
57
+ END;
58
+ `;
59
+ let db = null;
60
+ export function getDb() {
61
+ if (db)
62
+ return db;
63
+ if (!existsSync(DB_DIR)) {
64
+ mkdirSync(DB_DIR, { recursive: true });
65
+ }
66
+ db = new Database(DB_PATH);
67
+ db.pragma("journal_mode = WAL");
68
+ db.pragma("foreign_keys = ON");
69
+ db.exec(SCHEMA);
70
+ return db;
71
+ }
72
+ export function addFeed(url, title, siteUrl) {
73
+ const db = getDb();
74
+ const stmt = db.prepare("INSERT INTO feeds (url, title, site_url) VALUES (?, ?, ?)");
75
+ const result = stmt.run(url, title ?? null, siteUrl ?? null);
76
+ const feed = db
77
+ .prepare("SELECT * FROM feeds WHERE id = ?")
78
+ .get(result.lastInsertRowid);
79
+ return feed;
80
+ }
81
+ export function listFeeds() {
82
+ const db = getDb();
83
+ return db
84
+ .prepare("SELECT * FROM feeds ORDER BY created_at DESC")
85
+ .all();
86
+ }
87
+ export function removeFeed(feedId) {
88
+ const db = getDb();
89
+ const result = db.prepare("DELETE FROM feeds WHERE id = ?").run(feedId);
90
+ return result.changes > 0;
91
+ }
92
+ export function getFeed(feedId) {
93
+ const db = getDb();
94
+ return (db.prepare("SELECT * FROM feeds WHERE id = ?").get(feedId) ?? null);
95
+ }
96
+ export function updateFeedMeta(feedId, title, siteUrl, etag, lastModified) {
97
+ const db = getDb();
98
+ db.prepare(`UPDATE feeds
99
+ SET title = COALESCE(?, title),
100
+ site_url = COALESCE(?, site_url),
101
+ last_fetched = datetime('now'),
102
+ etag = ?,
103
+ last_modified = ?
104
+ WHERE id = ?`).run(title, siteUrl, etag, lastModified, feedId);
105
+ }
106
+ export function upsertPosts(feedId, entries) {
107
+ const db = getDb();
108
+ const stmt = db.prepare(`INSERT OR IGNORE INTO posts (feed_id, guid, title, url, summary, author, published_at)
109
+ VALUES (?, ?, ?, ?, ?, ?, ?)`);
110
+ let count = 0;
111
+ const insertMany = db.transaction((entries) => {
112
+ for (const e of entries) {
113
+ const result = stmt.run(feedId, e.guid, e.title ?? null, e.url ?? null, e.summary ?? null, e.author ?? null, e.published_at ?? null);
114
+ if (result.changes > 0)
115
+ count++;
116
+ }
117
+ });
118
+ insertMany(entries);
119
+ return count;
120
+ }
121
+ export function getPosts(options = {}) {
122
+ const db = getDb();
123
+ const { feedId, limit = 50, offset = 0, unreadOnly = false, search, since, } = options;
124
+ const conditions = [];
125
+ const params = [];
126
+ if (feedId !== undefined) {
127
+ conditions.push("p.feed_id = ?");
128
+ params.push(feedId);
129
+ }
130
+ if (unreadOnly) {
131
+ conditions.push("p.is_read = 0");
132
+ }
133
+ if (since) {
134
+ conditions.push("p.published_at >= ?");
135
+ params.push(since);
136
+ }
137
+ if (search) {
138
+ conditions.push("p.id IN (SELECT rowid FROM posts_fts WHERE posts_fts MATCH ?)");
139
+ params.push(search);
140
+ }
141
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
142
+ const query = `
143
+ SELECT p.*, f.title as feed_title, f.url as feed_url
144
+ FROM posts p
145
+ JOIN feeds f ON p.feed_id = f.id
146
+ ${whereClause}
147
+ ORDER BY p.published_at DESC NULLS LAST
148
+ LIMIT ? OFFSET ?
149
+ `;
150
+ params.push(limit, offset);
151
+ return db.prepare(query).all(...params);
152
+ }
153
+ export function markRead(postIds) {
154
+ if (postIds.length === 0)
155
+ return 0;
156
+ const db = getDb();
157
+ const placeholders = postIds.map(() => "?").join(",");
158
+ const now = new Date().toISOString();
159
+ const result = db
160
+ .prepare(`UPDATE posts SET is_read = 1, read_at = ? WHERE id IN (${placeholders})`)
161
+ .run(now, ...postIds);
162
+ return result.changes;
163
+ }
164
+ export function markUnread(postIds) {
165
+ if (postIds.length === 0)
166
+ return 0;
167
+ const db = getDb();
168
+ const placeholders = postIds.map(() => "?").join(",");
169
+ const result = db
170
+ .prepare(`UPDATE posts SET is_read = 0, read_at = NULL WHERE id IN (${placeholders})`)
171
+ .run(...postIds);
172
+ return result.changes;
173
+ }
174
+ export function getDailyDigest(hours = 24, maxSummaryLength = 300) {
175
+ const db = getDb();
176
+ const cutoff = new Date(Date.now() - hours * 60 * 60 * 1000).toISOString();
177
+ const rows = db
178
+ .prepare(`SELECT p.id, f.title AS feed, p.title, p.summary, p.url, p.published_at
179
+ FROM posts p JOIN feeds f ON p.feed_id = f.id
180
+ WHERE p.published_at >= ?
181
+ ORDER BY p.published_at DESC`)
182
+ .all(cutoff);
183
+ return rows.map((row) => ({
184
+ ...row,
185
+ summary: row.summary && row.summary.length > maxSummaryLength
186
+ ? row.summary.slice(0, maxSummaryLength) + "..."
187
+ : row.summary,
188
+ }));
189
+ }
190
+ const MAX_CONTENT_LENGTH = 5000;
191
+ const TRUNCATION_MARKER = "\n\n[Content truncated. Use full=true for complete article.]";
192
+ export function updatePostContent(postId, content) {
193
+ const db = getDb();
194
+ db.prepare("UPDATE posts SET content = ? WHERE id = ?").run(content, postId);
195
+ }
196
+ export function getPostContent(postId, full = false) {
197
+ const db = getDb();
198
+ const row = db
199
+ .prepare(`SELECT p.*, f.title as feed_title
200
+ FROM posts p JOIN feeds f ON p.feed_id = f.id
201
+ WHERE p.id = ?`)
202
+ .get(postId);
203
+ if (!row)
204
+ return null;
205
+ let content = row.content ?? "";
206
+ let truncated = false;
207
+ if (!full && content.length > MAX_CONTENT_LENGTH) {
208
+ content = content.slice(0, MAX_CONTENT_LENGTH) + TRUNCATION_MARKER;
209
+ truncated = true;
210
+ }
211
+ return { ...row, content, truncated };
212
+ }
213
+ //# sourceMappingURL=db.js.map
package/dist/db.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"db.js","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import type { PostEntry } from "./db.js";
2
+ export interface FetchResult {
3
+ xml: string | null;
4
+ etag: string | null;
5
+ lastModified: string | null;
6
+ }
7
+ export declare function fetchFeed(url: string, etag?: string | null, lastModified?: string | null): Promise<FetchResult>;
8
+ export interface ParsedFeed {
9
+ title: string | null;
10
+ siteUrl: string | null;
11
+ entries: PostEntry[];
12
+ }
13
+ export declare function parseFeed(xml: string): ParsedFeed;
14
+ //# sourceMappingURL=feeds.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"feeds.d.ts","sourceRoot":"","sources":["../src/feeds.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEzC,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED,wBAAsB,SAAS,CAC7B,GAAG,EAAE,MAAM,EACX,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,EACpB,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,GAC3B,OAAO,CAAC,WAAW,CAAC,CAyBtB;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,OAAO,EAAE,SAAS,EAAE,CAAC;CACtB;AAkBD,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CA6BjD"}
package/dist/feeds.js ADDED
@@ -0,0 +1,54 @@
1
+ import { parseFeed as parseRssFeed } from "feedsmith";
2
+ export async function fetchFeed(url, etag, lastModified) {
3
+ const headers = {};
4
+ if (etag)
5
+ headers["If-None-Match"] = etag;
6
+ if (lastModified)
7
+ headers["If-Modified-Since"] = lastModified;
8
+ const response = await fetch(url, {
9
+ headers,
10
+ redirect: "follow",
11
+ signal: AbortSignal.timeout(30000),
12
+ });
13
+ if (response.status === 304) {
14
+ return { xml: null, etag: null, lastModified: null };
15
+ }
16
+ if (!response.ok) {
17
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
18
+ }
19
+ const xml = await response.text();
20
+ return {
21
+ xml,
22
+ etag: response.headers.get("etag"),
23
+ lastModified: response.headers.get("last-modified"),
24
+ };
25
+ }
26
+ export function parseFeed(xml) {
27
+ const result = parseRssFeed(xml);
28
+ const feed = result.feed;
29
+ const entries = (feed.items ?? []).map((item) => {
30
+ let authorStr;
31
+ if (typeof item.author === "string") {
32
+ authorStr = item.author;
33
+ }
34
+ else if (item.author) {
35
+ authorStr = item.author.name ?? item.author.email;
36
+ }
37
+ return {
38
+ guid: item.id ?? item.link ?? "",
39
+ title: item.title,
40
+ url: item.link,
41
+ summary: item.description ?? item.content,
42
+ author: authorStr,
43
+ published_at: item.published
44
+ ? new Date(item.published).toISOString()
45
+ : undefined,
46
+ };
47
+ });
48
+ return {
49
+ title: feed.title ?? null,
50
+ siteUrl: feed.link ?? null,
51
+ entries,
52
+ };
53
+ }
54
+ //# sourceMappingURL=feeds.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"feeds.js","sourceRoot":"","sources":["../src/feeds.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,IAAI,YAAY,EAAE,MAAM,WAAW,CAAC;AAStD,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,GAAW,EACX,IAAoB,EACpB,YAA4B;IAE5B,MAAM,OAAO,GAA2B,EAAE,CAAC;IAC3C,IAAI,IAAI;QAAE,OAAO,CAAC,eAAe,CAAC,GAAG,IAAI,CAAC;IAC1C,IAAI,YAAY;QAAE,OAAO,CAAC,mBAAmB,CAAC,GAAG,YAAY,CAAC;IAE9D,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;QAChC,OAAO;QACP,QAAQ,EAAE,QAAQ;QAClB,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,KAAK,CAAC;KACnC,CAAC,CAAC;IAEH,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QAC5B,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC;IACvD,CAAC;IAED,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CAAC,QAAQ,QAAQ,CAAC,MAAM,KAAK,QAAQ,CAAC,UAAU,EAAE,CAAC,CAAC;IACrE,CAAC;IAED,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;IAClC,OAAO;QACL,GAAG;QACH,IAAI,EAAE,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC;QAClC,YAAY,EAAE,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC;KACpD,CAAC;AACJ,CAAC;AAwBD,MAAM,UAAU,SAAS,CAAC,GAAW;IACnC,MAAM,MAAM,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;IACjC,MAAM,IAAI,GAAG,MAAM,CAAC,IAAgB,CAAC;IAErC,MAAM,OAAO,GAAgB,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,IAAc,EAAE,EAAE;QACrE,IAAI,SAA6B,CAAC;QAClC,IAAI,OAAO,IAAI,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;YACpC,SAAS,GAAG,IAAI,CAAC,MAAM,CAAC;QAC1B,CAAC;aAAM,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YACvB,SAAS,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,IAAI,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC;QACpD,CAAC;QAED,OAAO;YACL,IAAI,EAAE,IAAI,CAAC,EAAE,IAAI,IAAI,CAAC,IAAI,IAAI,EAAE;YAChC,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,GAAG,EAAE,IAAI,CAAC,IAAI;YACd,OAAO,EAAE,IAAI,CAAC,WAAW,IAAI,IAAI,CAAC,OAAO;YACzC,MAAM,EAAE,SAAS;YACjB,YAAY,EAAE,IAAI,CAAC,SAAS;gBAC1B,CAAC,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,WAAW,EAAE;gBACxC,CAAC,CAAC,SAAS;SACd,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,OAAO;QACL,KAAK,EAAE,IAAI,CAAC,KAAK,IAAI,IAAI;QACzB,OAAO,EAAE,IAAI,CAAC,IAAI,IAAI,IAAI;QAC1B,OAAO;KACR,CAAC;AACJ,CAAC"}
package/dist/hn.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ export interface HNScore {
2
+ score: number;
3
+ comments: number;
4
+ hn_url: string;
5
+ }
6
+ export declare function fetchHNScore(url: string): Promise<HNScore | null>;
7
+ //# sourceMappingURL=hn.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hn.d.ts","sourceRoot":"","sources":["../src/hn.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB;AAUD,wBAAsB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CA2BvE"}
package/dist/hn.js ADDED
@@ -0,0 +1,28 @@
1
+ const HN_SEARCH_URL = "https://hn.algolia.com/api/v1/search";
2
+ export async function fetchHNScore(url) {
3
+ try {
4
+ const searchUrl = new URL(HN_SEARCH_URL);
5
+ searchUrl.searchParams.set("query", url);
6
+ searchUrl.searchParams.set("restrictSearchableAttributes", "url");
7
+ searchUrl.searchParams.set("hitsPerPage", "1");
8
+ const response = await fetch(searchUrl.toString(), {
9
+ signal: AbortSignal.timeout(10000),
10
+ });
11
+ if (!response.ok)
12
+ return null;
13
+ const data = (await response.json());
14
+ const hits = data.hits ?? [];
15
+ if (hits.length === 0)
16
+ return null;
17
+ const hit = hits[0];
18
+ return {
19
+ score: hit.points ?? 0,
20
+ comments: hit.num_comments ?? 0,
21
+ hn_url: `https://news.ycombinator.com/item?id=${hit.objectID}`,
22
+ };
23
+ }
24
+ catch {
25
+ return null;
26
+ }
27
+ }
28
+ //# sourceMappingURL=hn.js.map
package/dist/hn.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hn.js","sourceRoot":"","sources":["../src/hn.ts"],"names":[],"mappings":"AAAA,MAAM,aAAa,GAAG,sCAAsC,CAAC;AAgB7D,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,GAAW;IAC5C,IAAI,CAAC;QACH,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,aAAa,CAAC,CAAC;QACzC,SAAS,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;QACzC,SAAS,CAAC,YAAY,CAAC,GAAG,CAAC,8BAA8B,EAAE,KAAK,CAAC,CAAC;QAClE,SAAS,CAAC,YAAY,CAAC,GAAG,CAAC,aAAa,EAAE,GAAG,CAAC,CAAC;QAE/C,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,SAAS,CAAC,QAAQ,EAAE,EAAE;YACjD,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,KAAK,CAAC;SACnC,CAAC,CAAC;QAEH,IAAI,CAAC,QAAQ,CAAC,EAAE;YAAE,OAAO,IAAI,CAAC;QAE9B,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAqB,CAAC;QACzD,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,IAAI,EAAE,CAAC;QAE7B,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;QAEnC,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;QACpB,OAAO;YACL,KAAK,EAAE,GAAG,CAAC,MAAM,IAAI,CAAC;YACtB,QAAQ,EAAE,GAAG,CAAC,YAAY,IAAI,CAAC;YAC/B,MAAM,EAAE,wCAAwC,GAAG,CAAC,QAAQ,EAAE;SAC/D,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
package/dist/index.js ADDED
@@ -0,0 +1,298 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { z } from "zod";
5
+ import { readFileSync } from "fs";
6
+ import * as db from "./db.js";
7
+ import { fetchFeed, parseFeed } from "./feeds.js";
8
+ import { fetchHNScore } from "./hn.js";
9
+ import { parseOpml } from "./opml.js";
10
+ import { fetchAndExtract } from "./content.js";
11
+ const server = new McpServer({
12
+ name: "rss-mcp",
13
+ version: "1.0.0",
14
+ });
15
+ // Tool: list_feeds
16
+ server.tool("list_feeds", {}, async () => {
17
+ const feeds = db.listFeeds();
18
+ return {
19
+ content: [{ type: "text", text: JSON.stringify(feeds, null, 2) }],
20
+ };
21
+ });
22
+ // Tool: add_feed
23
+ server.tool("add_feed", { url: z.string().describe("RSS/Atom feed URL to subscribe to") }, async ({ url }) => {
24
+ try {
25
+ const feed = db.addFeed(url);
26
+ return {
27
+ content: [{ type: "text", text: JSON.stringify(feed, null, 2) }],
28
+ };
29
+ }
30
+ catch (error) {
31
+ return {
32
+ content: [{ type: "text", text: `Error: ${error}` }],
33
+ isError: true,
34
+ };
35
+ }
36
+ });
37
+ // Tool: remove_feed
38
+ server.tool("remove_feed", { feed_id: z.number().describe("ID of the feed to remove") }, async ({ feed_id }) => {
39
+ const removed = db.removeFeed(feed_id);
40
+ return {
41
+ content: [{ type: "text", text: JSON.stringify({ removed }) }],
42
+ };
43
+ });
44
+ // Tool: import_opml
45
+ server.tool("import_opml", { file_path: z.string().describe("Path to OPML file") }, async ({ file_path }) => {
46
+ try {
47
+ const text = readFileSync(file_path, "utf-8");
48
+ const feeds = parseOpml(text);
49
+ let imported = 0;
50
+ for (const f of feeds) {
51
+ try {
52
+ db.addFeed(f.url, f.title, f.siteUrl);
53
+ imported++;
54
+ }
55
+ catch {
56
+ // Skip duplicates
57
+ }
58
+ }
59
+ return {
60
+ content: [
61
+ {
62
+ type: "text",
63
+ text: JSON.stringify({ imported, total_in_file: feeds.length }),
64
+ },
65
+ ],
66
+ };
67
+ }
68
+ catch (error) {
69
+ return {
70
+ content: [{ type: "text", text: `Error: ${error}` }],
71
+ isError: true,
72
+ };
73
+ }
74
+ });
75
+ // Tool: refresh_feeds
76
+ server.tool("refresh_feeds", {
77
+ feed_id: z
78
+ .number()
79
+ .optional()
80
+ .describe("Optional specific feed ID to refresh"),
81
+ }, async ({ feed_id }) => {
82
+ const feeds = feed_id
83
+ ? [db.getFeed(feed_id)].filter(Boolean)
84
+ : db.listFeeds();
85
+ const minInterval = 15 * 60 * 1000; // 15 minutes
86
+ const now = Date.now();
87
+ const results = {
88
+ refreshed: 0,
89
+ new_posts: 0,
90
+ skipped: 0,
91
+ errors: [],
92
+ };
93
+ for (const feed of feeds) {
94
+ if (!feed)
95
+ continue;
96
+ // Check rate limit
97
+ if (feed.last_fetched) {
98
+ const lastFetched = new Date(feed.last_fetched).getTime();
99
+ if (now - lastFetched < minInterval) {
100
+ results.skipped++;
101
+ continue;
102
+ }
103
+ }
104
+ try {
105
+ const { xml, etag, lastModified } = await fetchFeed(feed.url, feed.etag, feed.last_modified);
106
+ if (xml === null) {
107
+ results.skipped++;
108
+ continue;
109
+ }
110
+ const { title, siteUrl, entries } = parseFeed(xml);
111
+ const count = db.upsertPosts(feed.id, entries);
112
+ db.updateFeedMeta(feed.id, title, siteUrl, etag, lastModified);
113
+ results.refreshed++;
114
+ results.new_posts += count;
115
+ }
116
+ catch (error) {
117
+ results.errors.push({
118
+ feed_id: feed.id,
119
+ url: feed.url,
120
+ error: String(error),
121
+ });
122
+ }
123
+ }
124
+ return {
125
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
126
+ };
127
+ });
128
+ // Tool: get_posts
129
+ server.tool("get_posts", {
130
+ feed_id: z.number().optional().describe("Filter by feed ID"),
131
+ limit: z
132
+ .number()
133
+ .optional()
134
+ .default(50)
135
+ .describe("Maximum posts to return"),
136
+ offset: z.number().optional().default(0).describe("Pagination offset"),
137
+ unread_only: z
138
+ .boolean()
139
+ .optional()
140
+ .default(false)
141
+ .describe("Only return unread posts"),
142
+ search: z.string().optional().describe("FTS5 full-text search query"),
143
+ since: z
144
+ .string()
145
+ .optional()
146
+ .describe("ISO 8601 date to filter posts after"),
147
+ }, async ({ feed_id, limit, offset, unread_only, search, since }) => {
148
+ const posts = db.getPosts({
149
+ feedId: feed_id,
150
+ limit,
151
+ offset,
152
+ unreadOnly: unread_only,
153
+ search,
154
+ since,
155
+ });
156
+ return {
157
+ content: [{ type: "text", text: JSON.stringify(posts, null, 2) }],
158
+ };
159
+ });
160
+ // Tool: get_post_content
161
+ server.tool("get_post_content", {
162
+ post_id: z.number().describe("ID of the post"),
163
+ full: z
164
+ .boolean()
165
+ .optional()
166
+ .default(false)
167
+ .describe("Return full content without truncation"),
168
+ }, async ({ post_id, full }) => {
169
+ let result = db.getPostContent(post_id, full);
170
+ if (!result) {
171
+ return {
172
+ content: [
173
+ {
174
+ type: "text",
175
+ text: JSON.stringify({ error: `Post ${post_id} not found` }),
176
+ },
177
+ ],
178
+ isError: true,
179
+ };
180
+ }
181
+ // If no content yet, fetch it
182
+ if (!result.content && result.url) {
183
+ const extractedContent = await fetchAndExtract(result.url);
184
+ if (extractedContent) {
185
+ db.updatePostContent(post_id, extractedContent);
186
+ result = db.getPostContent(post_id, full);
187
+ }
188
+ }
189
+ return {
190
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
191
+ };
192
+ });
193
+ // Tool: get_daily_digest
194
+ server.tool("get_daily_digest", {
195
+ hours: z.number().optional().default(24).describe("Hours to look back"),
196
+ max_summary_length: z
197
+ .number()
198
+ .optional()
199
+ .default(300)
200
+ .describe("Max chars per summary"),
201
+ }, async ({ hours, max_summary_length }) => {
202
+ const posts = db.getDailyDigest(hours, max_summary_length);
203
+ const feeds = new Set(posts.map((p) => p.feed));
204
+ return {
205
+ content: [
206
+ {
207
+ type: "text",
208
+ text: JSON.stringify({
209
+ period: `last ${hours}h`,
210
+ total_posts: posts.length,
211
+ feeds: feeds.size,
212
+ posts,
213
+ }, null, 2),
214
+ },
215
+ ],
216
+ };
217
+ });
218
+ // Tool: mark_read
219
+ server.tool("mark_read", {
220
+ post_ids: z.array(z.number()).describe("Array of post IDs to mark as read"),
221
+ }, async ({ post_ids }) => {
222
+ const marked = db.markRead(post_ids);
223
+ return {
224
+ content: [{ type: "text", text: JSON.stringify({ marked }) }],
225
+ };
226
+ });
227
+ // Tool: mark_unread
228
+ server.tool("mark_unread", {
229
+ post_ids: z
230
+ .array(z.number())
231
+ .describe("Array of post IDs to mark as unread"),
232
+ }, async ({ post_ids }) => {
233
+ const marked = db.markUnread(post_ids);
234
+ return {
235
+ content: [{ type: "text", text: JSON.stringify({ marked }) }],
236
+ };
237
+ });
238
+ // Tool: get_popular_posts
239
+ server.tool("get_popular_posts", {
240
+ days: z.number().optional().default(7).describe("Days to look back"),
241
+ limit: z.number().optional().default(10).describe("Max posts to return"),
242
+ }, async ({ days, limit }) => {
243
+ const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
244
+ const posts = db.getPosts({ since, limit: 500 });
245
+ const results = [];
246
+ // Fetch HN scores concurrently (max 5 at a time)
247
+ const chunks = [];
248
+ for (let i = 0; i < posts.length; i += 5) {
249
+ chunks.push(posts.slice(i, i + 5));
250
+ }
251
+ for (const chunk of chunks) {
252
+ const chunkResults = await Promise.all(chunk.map(async (post) => {
253
+ if (!post.url)
254
+ return null;
255
+ const hn = await fetchHNScore(post.url);
256
+ if (!hn)
257
+ return null;
258
+ return {
259
+ id: post.id,
260
+ feed: post.feed_title ?? "",
261
+ title: post.title,
262
+ url: post.url,
263
+ published_at: post.published_at,
264
+ hn_score: hn.score,
265
+ hn_comments: hn.comments,
266
+ hn_url: hn.hn_url,
267
+ };
268
+ }));
269
+ results.push(...chunkResults.filter((r) => r !== null));
270
+ }
271
+ // Sort by HN score and take top N
272
+ const ranked = results
273
+ .sort((a, b) => b.hn_score - a.hn_score)
274
+ .slice(0, limit);
275
+ return {
276
+ content: [
277
+ {
278
+ type: "text",
279
+ text: JSON.stringify({
280
+ period: `last ${days} days`,
281
+ total_checked: posts.length,
282
+ posts: ranked,
283
+ }, null, 2),
284
+ },
285
+ ],
286
+ };
287
+ });
288
+ // Start server
289
+ async function main() {
290
+ const transport = new StdioServerTransport();
291
+ await server.connect(transport);
292
+ console.error("RSS MCP Server running on stdio");
293
+ }
294
+ main().catch((error) => {
295
+ console.error("Fatal error:", error);
296
+ process.exit(1);
297
+ });
298
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/opml.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ export interface OPMLFeed {
2
+ url: string;
3
+ title?: string;
4
+ siteUrl?: string;
5
+ }
6
+ export declare function parseOpml(opmlText: string): OPMLFeed[];
7
+ //# sourceMappingURL=opml.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"opml.d.ts","sourceRoot":"","sources":["../src/opml.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,QAAQ;IACvB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAUD,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,QAAQ,EAAE,CAsBtD"}
package/dist/opml.js ADDED
@@ -0,0 +1,24 @@
1
+ import { parseOpml as parseOpmlFeed } from "feedsmith";
2
+ export function parseOpml(opmlText) {
3
+ const result = parseOpmlFeed(opmlText);
4
+ const feeds = [];
5
+ function extractFeeds(outlines) {
6
+ if (!outlines)
7
+ return;
8
+ for (const outline of outlines) {
9
+ if (outline.xmlUrl) {
10
+ feeds.push({
11
+ url: outline.xmlUrl,
12
+ title: outline.text ?? outline.title,
13
+ siteUrl: outline.htmlUrl,
14
+ });
15
+ }
16
+ if (outline.outlines) {
17
+ extractFeeds(outline.outlines);
18
+ }
19
+ }
20
+ }
21
+ extractFeeds(result.body?.outlines);
22
+ return feeds;
23
+ }
24
+ //# sourceMappingURL=opml.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"opml.js","sourceRoot":"","sources":["../src/opml.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,IAAI,aAAa,EAAE,MAAM,WAAW,CAAC;AAgBvD,MAAM,UAAU,SAAS,CAAC,QAAgB;IACxC,MAAM,MAAM,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAC;IACvC,MAAM,KAAK,GAAe,EAAE,CAAC;IAE7B,SAAS,YAAY,CAAC,QAAmC;QACvD,IAAI,CAAC,QAAQ;YAAE,OAAO;QACtB,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;YAC/B,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;gBACnB,KAAK,CAAC,IAAI,CAAC;oBACT,GAAG,EAAE,OAAO,CAAC,MAAM;oBACnB,KAAK,EAAE,OAAO,CAAC,IAAI,IAAI,OAAO,CAAC,KAAK;oBACpC,OAAO,EAAE,OAAO,CAAC,OAAO;iBACzB,CAAC,CAAC;YACL,CAAC;YACD,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC;gBACrB,YAAY,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;YACjC,CAAC;QACH,CAAC;IACH,CAAC;IAED,YAAY,CAAC,MAAM,CAAC,IAAI,EAAE,QAAqC,CAAC,CAAC;IACjE,OAAO,KAAK,CAAC;AACf,CAAC"}
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@0xquinto/rss-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for RSS feed management with full-text search and HackerNews ranking",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "rss-mcp": "dist/index.js"
8
+ },
9
+ "type": "module",
10
+ "scripts": {
11
+ "build": "bun run tsc",
12
+ "start": "bun dist/index.js",
13
+ "dev": "bun --watch src/index.ts",
14
+ "prepublishOnly": "bun run build"
15
+ },
16
+ "keywords": [
17
+ "mcp",
18
+ "rss",
19
+ "feeds",
20
+ "hackernews",
21
+ "claude",
22
+ "ai",
23
+ "model-context-protocol"
24
+ ],
25
+ "author": "Diego Gomez <diego.gomez210@gmail.com>",
26
+ "license": "MIT",
27
+ "files": [
28
+ "dist"
29
+ ],
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "https://github.com/0xquinto/rss-mcp-ts"
33
+ },
34
+ "engines": {
35
+ "node": ">=18"
36
+ },
37
+ "dependencies": {
38
+ "@modelcontextprotocol/sdk": "^1.0.0",
39
+ "@mozilla/readability": "^0.6.0",
40
+ "better-sqlite3": "^12.0.0",
41
+ "feedsmith": "^1.0.0",
42
+ "linkedom": "^0.18.0",
43
+ "zod": "^3.25.76"
44
+ },
45
+ "devDependencies": {
46
+ "@types/better-sqlite3": "^7.6.0",
47
+ "@types/node": "^22.0.0",
48
+ "tsx": "^4.0.0",
49
+ "typescript": "^5.7.0"
50
+ }
51
+ }