@mrfentmen/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,19 @@
1
+ # Rss MCP
2
+
3
+ RSS in one place: direct feed parsing and rss2json conversion for any feed URL. No key required.
4
+
5
+ This file is self contained. It reads public data only and never writes to the machine. All output is bounded and honest about what could not be fetched.
6
+
7
+ ## Tools
8
+
9
+ * `readFeed` Fetch and parse any RSS or Atom feed.
10
+ * `feedJson` Convert any feed to JSON via rss2json.
11
+
12
+ ## Usage
13
+
14
+ ```bash
15
+ npm install
16
+ npm run build
17
+ node dist/index.js
18
+ ```
19
+
package/dist/api.js ADDED
@@ -0,0 +1,73 @@
1
+ const RSS2JSON_BASE = 'https://api.rss2json.com/v1/api.json';
2
+ const UA = 'mrfentmen-rss-mcp/1.0 (https://github.com/mrfentmen)';
3
+ export class RssError extends Error {
4
+ }
5
+ function strip(s) {
6
+ return s
7
+ .replace(/<[^>]*>/g, ' ')
8
+ .replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/g, '&')
9
+ .replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&apos;/g, "'")
10
+ .replace(/\s+/g, ' ').trim();
11
+ }
12
+ function parseFeed(xml) {
13
+ const out = [];
14
+ const atomRe = /<entry>([\s\S]*?)<\/entry>/g;
15
+ let m;
16
+ while ((m = atomRe.exec(xml)) !== null) {
17
+ const e = m[1];
18
+ const title = /<title[^>]*>([\s\S]*?)<\/title>/.exec(e)?.[1] ?? '';
19
+ const link = /<link[^>]*href="([^"]+)"/.exec(e)?.[1] ?? '';
20
+ const pub = /<updated[^>]*>([\s\S]*?)<\/updated>/.exec(e)?.[1] ?? /<published[^>]*>([\s\S]*?)<\/published>/.exec(e)?.[1] ?? '';
21
+ if (title)
22
+ out.push({ title: strip(title), link, pub: strip(pub).slice(0, 10) });
23
+ }
24
+ const rssRe = /<item>([\s\S]*?)<\/item>/g;
25
+ while ((m = rssRe.exec(xml)) !== null) {
26
+ const e = m[1];
27
+ const title = /<title[^>]*>([\s\S]*?)<\/title>/.exec(e)?.[1] ?? '';
28
+ const link = /<link[^>]*>([\s\S]*?)<\/link>/.exec(e)?.[1] ?? '';
29
+ const pub = /<pubDate[^>]*>([\s\S]*?)<\/pubDate>/.exec(e)?.[1] ?? '';
30
+ if (title)
31
+ out.push({ title: strip(title), link: strip(link), pub: strip(pub).slice(0, 16) });
32
+ }
33
+ return out;
34
+ }
35
+ export async function readFeed(args) {
36
+ const url = (args.url ?? '').trim();
37
+ if (!/^https?:\/\//i.test(url))
38
+ throw new RssError('Provide a full http or https feed URL');
39
+ const limit = Math.min(args.limit ?? 10, 25);
40
+ const res = await fetch(url, {
41
+ headers: { 'User-Agent': UA, Accept: 'application/rss+xml, application/atom+xml, application/xml, text/xml' },
42
+ redirect: 'follow',
43
+ signal: AbortSignal.timeout(25000),
44
+ });
45
+ if (!res.ok)
46
+ throw new RssError(`Feed error ${res.status}`);
47
+ const text = await res.text();
48
+ const title = /<title[^>]*>([\s\S]*?)<\/title>/.exec(text)?.[1] ?? '';
49
+ const entries = parseFeed(text).slice(0, limit);
50
+ return `${strip(title) || 'Feed'}\n${entries.map((e, i) => `${i + 1}. ${e.title}\n ${e.pub ? `${e.pub} | ` : ''}${e.link}`).join('\n') || 'No entries found'}`;
51
+ }
52
+ export async function feedJson(args) {
53
+ const url = (args.url ?? '').trim();
54
+ if (!url)
55
+ return 'Provide an RSS feed URL.';
56
+ const limit = Math.max(1, Math.min(args.limit ?? 10, 30));
57
+ const res = await fetch(`${RSS2JSON_BASE}?rss_url=${encodeURIComponent(url)}`, {
58
+ headers: { 'User-Agent': UA, Accept: 'application/json' },
59
+ signal: AbortSignal.timeout(25000),
60
+ });
61
+ if (!res.ok)
62
+ throw new RssError(`RSS2JSON returned ${res.status}`);
63
+ const data = (await res.json());
64
+ if (data.status !== 'ok')
65
+ throw new RssError(`RSS2JSON error: ${data.message ?? data.status ?? 'unknown'}`);
66
+ const items = (data.items ?? []).slice(0, limit);
67
+ if (!items.length)
68
+ return 'The feed returned no items.';
69
+ return `Feed: ${data.feed?.title ?? url} (${items.length} items):\n` +
70
+ items
71
+ .map((item, i) => `${i + 1}. ${item.title ?? 'untitled'} | ${(item.pubDate ?? '').slice(0, 16)}${item.link ? `\n ${item.link}` : ''}`)
72
+ .join('\n');
73
+ }
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2
+ import { createServer } from "./server.js";
3
+ const main = async () => { const server = createServer(); await server.connect(new StdioServerTransport()); };
4
+ main().catch((error) => { console.error("Fatal error:", error); process.exit(1); });
package/dist/server.js ADDED
@@ -0,0 +1,37 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { z } from 'zod';
3
+ import { readFeed, feedJson } from './api.js';
4
+ const text = (value) => ({ content: [{ type: 'text', text: value }] });
5
+ const textError = (t) => ({ content: [{ type: "text", text: t }], isError: true });
6
+ const READ_ONLY = { readOnlyHint: true, openWorldHint: true };
7
+ const error = (e) => `Error: ${e instanceof Error ? e.message : String(e)}`;
8
+ export function createServer() {
9
+ const server = new McpServer({ name: 'rss-mcp', version: '1.0.0' });
10
+ server.registerTool('readFeed', {
11
+ title: "Read Feed",
12
+ description: 'Fetch and parse any RSS or Atom feed directly.',
13
+ inputSchema: z.object({ url: z.string().describe('Feed URL.').optional(), limit: z.number().describe('Max entries.').optional() }),
14
+ annotations: READ_ONLY,
15
+ }, async (args) => {
16
+ try {
17
+ return text(await readFeed(args));
18
+ }
19
+ catch (e) {
20
+ return textError(error(e));
21
+ }
22
+ });
23
+ server.registerTool('feedJson', {
24
+ title: "Feed Json",
25
+ description: 'Convert any RSS feed to JSON via rss2json.',
26
+ inputSchema: z.object({ url: z.string().describe('Feed URL.').optional(), limit: z.number().describe('Max entries.').optional() }),
27
+ annotations: READ_ONLY,
28
+ }, async (args) => {
29
+ try {
30
+ return text(await feedJson(args));
31
+ }
32
+ catch (e) {
33
+ return textError(error(e));
34
+ }
35
+ });
36
+ return server;
37
+ }
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "version": "1.0.0",
3
+ "type": "module",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "https://github.com/mrfentmen/rss-mcp.git"
7
+ },
8
+ "bin": {
9
+ "rss-mcp": "./dist/index.js"
10
+ },
11
+ "main": "./dist/index.js",
12
+ "files": [
13
+ "dist",
14
+ "server.json",
15
+ "README.md"
16
+ ],
17
+ "scripts": {
18
+ "build": "tsc -p tsconfig.json",
19
+ "start": "node dist/index.js",
20
+ "dev": "npm run build && node dist/index.js"
21
+ },
22
+ "license": "MIT",
23
+ "dependencies": {
24
+ "@modelcontextprotocol/sdk": "^1.0.4",
25
+ "zod": "^3.23.8"
26
+ },
27
+ "devDependencies": {
28
+ "@types/node": "^22.0.0",
29
+ "typescript": "^5.6.0"
30
+ },
31
+ "name": "@mrfentmen/rss-mcp",
32
+ "description": "RSS in one place: direct feed parsing and rss2json conversion for any feed URL. No key required.",
33
+ "mcpName": "io.github.mrfentmen/rss-mcp",
34
+ "keywords": [
35
+ "mcp",
36
+ "rss",
37
+ "atom",
38
+ "feeds",
39
+ "news"
40
+ ],
41
+ "engines": {
42
+ "node": ">=20"
43
+ }
44
+ }
package/server.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.mrfentmen/rss-mcp",
4
+ "description": "RSS in one place: direct feed parsing and rss2json conversion for any feed URL. No key required.",
5
+ "repository": {
6
+ "url": "https://github.com/mrfentmen/rss-mcp",
7
+ "source": "github"
8
+ },
9
+ "version": "1.0.0",
10
+ "packages": [
11
+ {
12
+ "registryType": "npm",
13
+ "identifier": "@mrfentmen/rss-mcp",
14
+ "version": "1.0.0",
15
+ "transport": {
16
+ "type": "stdio"
17
+ }
18
+ }
19
+ ]
20
+ }