Skip to main content
Glama
server.ts6.06 kB
import { FastMCP } from "fastmcp"; import { z } from "zod"; import Parser from "rss-parser"; import fs from "fs/promises"; import path from "path"; import axios from "axios"; import os from "os"; // ------------------------------------------------------ // 🔐 Stable, cross-machine location for last_seen.json // ------------------------------------------------------ const CONFIG_DIR = path.join(os.homedir(), ".config", "goose", "content-fetcher-mcp"); const LAST_SEEN_FILE = path.join(CONFIG_DIR, "last_seen.json"); // Create the directory safely (no top-level await) async function ensureConfigDir() { try { await fs.mkdir(CONFIG_DIR, { recursive: true }); } catch {} } const YOUTUBE_CHANNEL_ID = "UCVLuT_AS687XAJ__-COCRFw"; const GOOSE_BLOG_RSS = "https://block.github.io/goose/blog/rss.xml"; const rssParser = new Parser(); /* -------------------------------------------------- Types -------------------------------------------------- */ interface ContentItem { id: string; title: string; url: string; published_at: string; type: "video" | "blog" | "release"; } /* -------------------------------------------------- Last Seen Helpers (reading & writing ONLY) -------------------------------------------------- */ async function loadLastSeen() { try { const raw = await fs.readFile(LAST_SEEN_FILE, "utf-8"); return JSON.parse(raw); } catch { return { seen: { youtube: [], blog: [], release: [] } }; } } async function saveLastSeen(data: any) { await fs.writeFile(LAST_SEEN_FILE, JSON.stringify(data, null, 2)); } async function hasSeen(type: string, id: string) { const data = await loadLastSeen(); return data.seen?.[type]?.includes(id); } async function markSeen(type: string, id: string) { const data = await loadLastSeen(); if (!data.seen[type]) data.seen[type] = []; if (!data.seen[type].includes(id)) data.seen[type].push(id); await saveLastSeen(data); } /* -------------------------------------------------- Fetch YouTube (NO writing to last_seen here!) -------------------------------------------------- */ async function fetchYoutube(): Promise<ContentItem[]> { const feed = await rssParser.parseURL( `https://www.youtube.com/feeds/videos.xml?channel_id=${YOUTUBE_CHANNEL_ID}` ); return feed.items.map((item) => ({ id: item.id || item.link || "", title: item.title || "", url: item.link || "", published_at: item.pubDate || "", type: "video" as const, })); } /* -------------------------------------------------- Generic RSS Fetcher (NO writing to last_seen) -------------------------------------------------- */ async function fetchRss(url: string): Promise<ContentItem[]> { const feed = await rssParser.parseURL(url); return feed.items.map((item) => ({ id: item.guid || item.link || "", title: item.title || "", url: item.link || "", published_at: item.pubDate || "", type: "blog" as const, })); } /* -------------------------------------------------- Hard-coded Goose Blog fetcher -------------------------------------------------- */ async function fetchGooseBlog(): Promise<ContentItem[]> { return await fetchRss(GOOSE_BLOG_RSS); } /* -------------------------------------------------- GitHub Releases (NO writing to last_seen) -------------------------------------------------- */ async function fetchGithubReleases(): Promise<ContentItem[]> { const response = await axios.get( "https://api.github.com/repos/block/goose/releases", { headers: { Accept: "application/vnd.github+json" } } ); return response.data.map((rel: any) => ({ id: rel.tag_name, title: rel.name || rel.tag_name, url: rel.html_url, published_at: rel.published_at || rel.created_at, type: "release" as const, })); } /* -------------------------------------------------- FastMCP Server -------------------------------------------------- */ const server = new FastMCP({ name: "content-fetcher-mcp", version: "3.0.0", }); /* ---------------- YouTube Tool ---------------- */ server.addTool({ name: "fetchYoutube", description: "Fetch ALL YouTube videos from the Goose channel.", parameters: z.object({}), execute: async () => JSON.stringify(await fetchYoutube()), }); /* ---------------- Generic RSS Tool ---------------- */ server.addTool({ name: "fetchRss", description: "Fetch ALL blog posts from any RSS feed.", parameters: z.object({ url: z.string().describe("RSS feed URL"), }), execute: async ({ url }) => JSON.stringify(await fetchRss(url)), }); /* ---------------- Goose Blog Tool ---------------- */ server.addTool({ name: "fetchGooseBlog", description: "Fetch ALL Goose blog posts.", parameters: z.object({}), execute: async () => JSON.stringify(await fetchGooseBlog()), }); /* ---------------- GitHub Releases Tool ---------------- */ server.addTool({ name: "fetchGithubReleases", description: "Fetch ALL GitHub releases from the Goose repo.", parameters: z.object({}), execute: async () => JSON.stringify(await fetchGithubReleases()), }); /* ---------------- isNewContent Tool ---------------- */ server.addTool({ name: "isNewContent", description: "Return true if this content item has NOT been seen before.", parameters: z.object({ id: z.string(), type: z.enum(["youtube", "blog", "release"]), }), execute: async ({ id, type }) => JSON.stringify({ is_new: !(await hasSeen(type, id)), }), }); /* ---------------- markContentSeen Tool ---------------- */ server.addTool({ name: "markContentSeen", description: "Mark a content item as seen AFTER posting.", parameters: z.object({ id: z.string(), type: z.enum(["youtube", "blog", "release"]), }), execute: async ({ id, type }) => { await markSeen(type, id); return JSON.stringify({ success: true }); }, }); /* -------------------------------------------------- Start the Server -------------------------------------------------- */ ensureConfigDir().then(() => { server.start({ transportType: "stdio" }); });

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/EbonyLouis/content-fetcher-mcp2'

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