Skip to main content
Glama
praveenkumarkunchala2005

Founder Intelligence Engine

fetch_personalized_news

Retrieve strategic news updates tailored to a founder's profile by analyzing relevance, summarizing content, and managing cached data for timely intelligence.

Instructions

Fetch personalized, strategically-relevant news for a founder. Checks cache freshness first — returns stored articles if <24h old. Otherwise scrapes Google News via Apify, ranks by cosine similarity, summarizes with Groq, and stores results.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
profile_idYesUUID of the profile to fetch news for (must have run analyze_profile first)

Implementation Reference

  • The core logic for fetching, scraping, ranking, and summarizing personalized news based on a founder's profile.
    export async function fetchPersonalizedNews({ profile_id }) {
      if (!profile_id) {
        throw new Error("profile_id is required.");
      }
    
      // 1. Check staleness
      const { stale, lastNewsFetch } = await isNewsStale(profile_id);
    
      if (!stale) {
        const cached = await getCachedArticles(profile_id);
        return {
          source: "cache",
          last_fetched: lastNewsFetch?.toISOString(),
          article_count: cached.length,
          articles: cached,
        };
      }
    
      // 2. Fetch profile + analysis
      const profile = await getProfile(profile_id);
      if (!profile) {
        throw new Error("Profile not found.");
      }
    
      let analysisData = await getProfileAnalysis(profile_id);
      if (!analysisData) {
        await analyzeProfile({ profile_id });
        analysisData = await getProfileAnalysis(profile_id);
      }
    
      if (!analysisData) {
        throw new Error("Profile analysis could not be generated.");
      }
    
      // 3. Build search queries
      const queries = buildSearchQueries(analysisData, profile.business_idea);
    
      // 4. Scrape Google News via Apify
      const rawArticles = await scrapeGoogleNews(queries, 30);
    
      if (rawArticles.length === 0) {
        const cached = await getCachedArticles(profile_id);
        if (cached.length > 0) {
          return {
            source: "cache_fallback",
            queries_used: queries,
            article_count: cached.length,
            articles: cached,
            message: "Live news fetch returned no items; returning cached articles.",
          };
        }
    
        return {
          source: "live",
          queries_used: queries,
          article_count: 0,
          articles: [],
          message: "No news articles found for the generated search queries. Check APIFY connectivity/actor config.",
        };
      }
    
      // 5. Deduplicate against existing stored URLs
      const existingUrls = new Set(await getArticleUrls(profile_id));
      const newArticles = rawArticles.filter((a) => !existingUrls.has(a.url));
    
      if (newArticles.length === 0) {
        await touchNewsFetch(profile_id);
        const cached = await getCachedArticles(profile_id);
        return {
          source: "cache_refreshed",
          article_count: cached.length,
          articles: cached,
          message: "All scraped articles already exist in database.",
        };
      }
    
      // 6. Generate embeddings for new articles
      const articleTexts = newArticles.map(
        (a) => `${a.title}\n${a.description}\n${a.content}`.trim()
      );
      const articleEmbeddings = await generateEmbeddings(articleTexts);
    
      // 7. Parse profile embedding
      let profileEmbedding = profile.embedding;
      if (typeof profileEmbedding === "string") {
        try {
          profileEmbedding = JSON.parse(profileEmbedding);
        } catch {
          profileEmbedding = null;
        }
      }
    
      if (!Array.isArray(profileEmbedding) || profileEmbedding.length === 0) {
        const seedText = profile.combined_text || profile.business_idea || "";
        if (!seedText.trim()) {
          throw new Error("Profile has no embedding and no text to regenerate embedding.");
        }
        profileEmbedding = await generateEmbedding(seedText);
      }
    
      // 8. Compute cosine similarity and rank
      const scored = newArticles.map((article, idx) => ({
        ...article,
        embedding: articleEmbeddings[idx],
        similarity_score: cosineSimilarity(profileEmbedding, articleEmbeddings[idx]),
      }));
    
      const relevant = scored
        .filter((a) => a.similarity_score >= SIMILARITY_THRESHOLD)
        .sort((a, b) => b.similarity_score - a.similarity_score)
        .slice(0, TOP_ARTICLES_COUNT);
    
      if (relevant.length === 0) {
        await touchNewsFetch(profile_id);
        return {
          source: "live",
          article_count: 0,
          articles: [],
          message: `No articles exceeded similarity threshold (${SIMILARITY_THRESHOLD}).`,
        };
      }
    
      // 9. Summarize top articles with Groq
      const founderContext = `Founder's business: ${profile.business_idea || "N/A"}\nIndustry: ${(analysisData.industry_tags || []).join(", ")}\nInterests: ${(analysisData.interests || []).join(", ")}`;
    
      const summarizedArticles = [];
    
      for (const article of relevant) {
        try {
          const userPrompt = `FOUNDER CONTEXT:\n${founderContext}\n\nARTICLE:\nTitle: ${article.title}\nDescription: ${article.description}\nContent: ${article.content}`;
    
          const summary = await callGroq(NEWS_INTELLIGENCE_PROMPT, userPrompt);
    
          summarizedArticles.push({
            ...article,
            summarized_output: summary,
          });
        } catch (err) {
          console.error(`Failed to summarize article "${article.title}":`, err.message);
          summarizedArticles.push({
            ...article,
            summarized_output: {
              headline_summary: article.description || article.title,
              strategic_relevance: "Summary unavailable due to processing error.",
              category: "MARKET_SIGNAL",
              action_items: [],
              relevance_confidence: article.similarity_score,
              risk_signals: [],
              opportunity_signals: [],
            },
          });
        }
      }
    
      // 10. Store articles
      const insertRows = summarizedArticles.map((a) => ({
        profile_id,
        title: a.title,
        description: a.description,
        content: a.content,
        url: a.url,
        embedding: JSON.stringify(a.embedding),
        similarity_score: a.similarity_score,
        summarized_output: a.summarized_output,
      }));
    
      await upsertArticles(insertRows);
    
      // 11. Update fetch_history
      await touchNewsFetch(profile_id);
    
      // 12. Return intelligence feed
      const feed = summarizedArticles.map((a) => ({
        title: a.title,
        url: a.url,
        similarity_score: Math.round(a.similarity_score * 1000) / 1000,
        summarized_output: a.summarized_output,
      }));
    
      return {
        source: "live",
        article_count: feed.length,
        queries_used: queries,
        articles: feed,
      };
    }
  • src/index.js:115-128 (registration)
    Registration of the 'fetch_personalized_news' tool in the main MCP server setup.
    // Tool 3: fetch_personalized_news
    // ─────────────────────────────────────────────────────────────
    server.tool(
      "fetch_personalized_news",
      "Fetch personalized, strategically-relevant news for a founder. Checks cache freshness first — returns stored articles if <24h old. Otherwise scrapes Google News via Apify, ranks by cosine similarity, summarizes with Groq, and stores results.",
      {
        profile_id: z
          .string()
          .uuid()
          .describe("UUID of the profile to fetch news for (must have run analyze_profile first)"),
      },
      async (args) => {
        try {
          const result = await fetchPersonalizedNews(args);

Latest Blog Posts

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/praveenkumarkunchala2005/mcp'

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