agentic-rss-parser 1.0.1

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.
@@ -0,0 +1,62 @@
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 { runAgenticParser } from '../parser.js';
6
+ import { createAnalyzer } from '../adapters/provider.js';
7
+ import { fetchFullArticle } from '../fetch-article.js';
8
+
9
+ const server = new McpServer({
10
+ name: 'agentic-rss-parser',
11
+ version: '1.0.0'
12
+ });
13
+
14
+ server.tool(
15
+ 'fetch_rss_feed',
16
+ {
17
+ url: z.string().url().describe('The RSS or Atom feed URL to fetch.'),
18
+ limit: z.number().int().min(1).max(100).default(10).describe('Maximum number of items to return.'),
19
+ provider: z.enum(['heuristic', 'openai', 'anthropic', 'local']).default('heuristic').describe('Analysis provider to use.')
20
+ },
21
+ async ({ url, limit, provider }) => {
22
+ const analyzer = await createAnalyzer({ provider });
23
+ const results = await runAgenticParser({
24
+ feedUrls: [url],
25
+ dbPath: './data/rss-agent.db',
26
+ analyzer,
27
+ model: { provider }
28
+ });
29
+
30
+ return {
31
+ content: [
32
+ {
33
+ type: 'text',
34
+ text: JSON.stringify(results.slice(0, limit), null, 2)
35
+ }
36
+ ]
37
+ };
38
+ }
39
+ );
40
+
41
+ server.tool(
42
+ 'fetch_full_article',
43
+ {
44
+ url: z.string().url().describe('The article URL to fetch.'),
45
+ provider: z.enum(['heuristic', 'openai', 'anthropic', 'local']).default('heuristic').describe('Analysis provider to use.')
46
+ },
47
+ async ({ url, provider }) => {
48
+ const text = await fetchFullArticle(url);
49
+
50
+ return {
51
+ content: [
52
+ {
53
+ type: 'text',
54
+ text: JSON.stringify({ url, text }, null, 2)
55
+ }
56
+ ]
57
+ };
58
+ }
59
+ );
60
+
61
+ const transport = new StdioServerTransport();
62
+ await server.connect(transport);
package/src/parser.js ADDED
@@ -0,0 +1,81 @@
1
+ import crypto from 'node:crypto';
2
+ import { analyzeFeedItem } from './agent.js';
3
+ import { createStorage } from './storage.js';
4
+ import { createAnalyzer } from './adapters/provider.js';
5
+ import { parseFeedXml } from './core/parser.js';
6
+ import { fetchTextWithRedirects } from './core/http.js';
7
+
8
+ function normalizeItem(feedUrl, item) {
9
+ const link = item.link || item.guid || '';
10
+ const id = crypto.createHash('sha256').update(`${feedUrl}:${link || item.title}`).digest('hex');
11
+ return {
12
+ id,
13
+ feedUrl,
14
+ title: item.title?.trim() || 'Untitled item',
15
+ link,
16
+ publishedAt: item.isoDate || item.pubDate || null,
17
+ contentSnippet: item.contentSnippet || item.content || ''
18
+ };
19
+ }
20
+
21
+ export async function runAgenticParser(config) {
22
+ const storage = createStorage(config.dbPath);
23
+ const results = [];
24
+ const analyzer = config.analyzer ?? await createAnalyzer(config.model);
25
+ const concurrency = normalizeConcurrency(config.concurrency);
26
+
27
+ try {
28
+ const feedRuns = await mapWithConcurrency(config.feedUrls, concurrency, async (feedUrl) => {
29
+ const xml = await fetchTextWithRedirects(feedUrl, config.parserOptions);
30
+ const feed = await parseFeedXml(xml, config.parserOptions);
31
+ const feedResults = [];
32
+
33
+ for (const item of feed.items) {
34
+ const normalized = normalizeItem(feedUrl, item);
35
+ if (storage.hasProcessed(normalized.id)) continue;
36
+
37
+ const analysis = await analyzeFeedItem(normalized, {
38
+ fetchFullArticle: config.fetchFullArticle,
39
+ analyzer
40
+ });
41
+
42
+ storage.markProcessed(normalized);
43
+ storage.saveAnalysis(normalized.id, {
44
+ id: crypto.randomUUID(),
45
+ ...analysis
46
+ });
47
+ feedResults.push({ item: normalized, analysis });
48
+ }
49
+
50
+ return feedResults;
51
+ });
52
+
53
+ results.push(...feedRuns.flat());
54
+ return results;
55
+ } finally {
56
+ storage.close();
57
+ }
58
+ }
59
+
60
+ function normalizeConcurrency(concurrency) {
61
+ const parsed = Number(concurrency);
62
+ if (!Number.isFinite(parsed) || parsed < 1) return 1;
63
+ return Math.min(16, Math.trunc(parsed));
64
+ }
65
+
66
+ async function mapWithConcurrency(items, limit, worker) {
67
+ const results = new Array(items.length);
68
+ let index = 0;
69
+
70
+ async function next() {
71
+ while (index < items.length) {
72
+ const currentIndex = index;
73
+ index += 1;
74
+ results[currentIndex] = await worker(items[currentIndex], currentIndex);
75
+ }
76
+ }
77
+
78
+ const workers = Array.from({ length: Math.min(limit, items.length) }, () => next());
79
+ await Promise.all(workers);
80
+ return results;
81
+ }
package/src/storage.js ADDED
@@ -0,0 +1,62 @@
1
+ import { DatabaseSync } from 'node:sqlite';
2
+ import { mkdirSync } from 'node:fs';
3
+ import { dirname } from 'node:path';
4
+
5
+ export function createStorage(dbPath) {
6
+ mkdirSync(dirname(dbPath), { recursive: true });
7
+ const db = new DatabaseSync(dbPath);
8
+
9
+ db.exec(`
10
+ CREATE TABLE IF NOT EXISTS processed_items (
11
+ id TEXT PRIMARY KEY,
12
+ feed_url TEXT NOT NULL,
13
+ title TEXT NOT NULL,
14
+ link TEXT NOT NULL,
15
+ published_at TEXT,
16
+ processed_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
17
+ ) STRICT;
18
+
19
+ CREATE TABLE IF NOT EXISTS analyses (
20
+ id TEXT PRIMARY KEY,
21
+ item_id TEXT NOT NULL,
22
+ decision TEXT NOT NULL,
23
+ confidence INTEGER NOT NULL,
24
+ summary TEXT NOT NULL,
25
+ impact TEXT NOT NULL,
26
+ action_items TEXT NOT NULL,
27
+ tags TEXT NOT NULL,
28
+ created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
29
+ ) STRICT;
30
+ `);
31
+
32
+ return {
33
+ hasProcessed(id) {
34
+ const row = db.prepare('SELECT 1 FROM processed_items WHERE id = ?').get(id);
35
+ return Boolean(row);
36
+ },
37
+ markProcessed(item) {
38
+ db.prepare(
39
+ 'INSERT OR IGNORE INTO processed_items (id, feed_url, title, link, published_at) VALUES (?, ?, ?, ?, ?)'
40
+ ).run(item.id, item.feedUrl, item.title, item.link, item.publishedAt ?? null);
41
+ },
42
+ saveAnalysis(itemId, analysis) {
43
+ db.prepare(
44
+ `INSERT OR REPLACE INTO analyses
45
+ (id, item_id, decision, confidence, summary, impact, action_items, tags)
46
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
47
+ ).run(
48
+ analysis.id,
49
+ itemId,
50
+ analysis.decision,
51
+ analysis.confidence,
52
+ analysis.summary,
53
+ analysis.impact,
54
+ JSON.stringify(analysis.actionItems),
55
+ JSON.stringify(analysis.tags)
56
+ );
57
+ },
58
+ close() {
59
+ db.close();
60
+ }
61
+ };
62
+ }
package/src/tools.js ADDED
@@ -0,0 +1,2 @@
1
+ export { analyzeFeedItem } from './agent.js';
2
+ export { fetchFullArticle } from './fetch-article.js';