Skip to main content
Glama
guardian-recommend-longreads.ts12.3 kB
import { GuardianClient } from '../api/guardian-client.js'; import { RecommendLongreadsParamsSchema } from '../types/guardian.js'; import { validateDate } from '../utils/formatters.js'; interface LongreadRecommendation { article: any; relevanceScore: number; reasons: string[]; readingTime: string; topics: string[]; } export async function guardianRecommendLongreads(client: GuardianClient, args: any): Promise<string> { const params = RecommendLongreadsParamsSchema.parse(args); const count = params.count || 3; const context = params.context || ''; const topicPreference = params.topic_preference || ''; // Determine date range for recommendations let fromDate: string; if (params.from_date) { fromDate = validateDate(params.from_date) || params.from_date; } else { // Default to last 3 months for fresh content const date = new Date(); date.setMonth(date.getMonth() - 3); fromDate = date.toISOString().substring(0, 10); } // Search for Long Read articles const searchParams: Record<string, any> = { 'tag': 'news/series/the-long-read', 'from-date': fromDate, 'page-size': 50, // Get a good selection for analysis 'show-fields': 'headline,standfirst,byline,wordcount,firstPublicationDate,body', 'show-tags': 'keyword,type,contributor', 'order-by': 'newest' }; const response = await client.search(searchParams); const longreads = response.response.results; if (longreads.length === 0) { return `No Long Read articles found since ${fromDate}. Try extending the date range.`; } // Analyze context to extract topics and preferences const contextAnalysis = analyzeContext(context, topicPreference); // Score and rank longreads based on relevance const recommendations = longreads .map(article => scoreLongread(article, contextAnalysis)) .sort((a, b) => b.relevanceScore - a.relevanceScore) .slice(0, count); // Format recommendations let result = `📚 **Curated Long Read Recommendations**\n`; result += `Based on: ${contextAnalysis.interests.length > 0 ? contextAnalysis.interests.join(', ') : 'diverse topics'}\n\n`; recommendations.forEach((rec, index) => { const article = rec.article; const rank = index + 1; result += `**${rank}. ${article.webTitle || 'Untitled'}**\n`; result += `${rec.readingTime} • Relevance: ${rec.relevanceScore.toFixed(1)}/100\n`; if (article.fields) { const { fields } = article; if (fields.byline) { result += `By: ${fields.byline}\n`; } if (fields.firstPublicationDate) { const pubDate = new Date(fields.firstPublicationDate).toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' }); result += `Published: ${pubDate}\n`; } if (fields.standfirst) { result += `Summary: ${fields.standfirst}\n`; } } result += `Topics: ${rec.topics.join(', ')}\n`; result += `Why recommended: ${rec.reasons.join(', ')}\n`; result += `URL: ${article.webUrl || 'N/A'}\n`; result += `Guardian ID: ${article.id || 'N/A'}\n\n`; }); // Add discovery suggestions result += `**Explore More**:\n`; result += `• Use guardian_longread with specific queries for targeted searches\n`; result += `• Try guardian_search_by_author with Long Read contributors\n`; const availableTopics = getPopularLongreadTopics(longreads); if (availableTopics.length > 0) { result += `• Popular Long Read topics: ${availableTopics.slice(0, 5).join(', ')}\n`; } return result; } interface ContextAnalysis { interests: string[]; preferredTypes: string[]; themes: string[]; } function analyzeContext(context: string, topicPreference: string): ContextAnalysis { const analysis: ContextAnalysis = { interests: [], preferredTypes: [], themes: [] }; // Combine context and topic preference for analysis const fullText = `${context} ${topicPreference}`.toLowerCase(); // Extract interests from common topics and keywords const topicKeywords = [ { keywords: ['climate', 'environment', 'global warming', 'sustainability'], topic: 'Environment' }, { keywords: ['politics', 'election', 'government', 'policy'], topic: 'Politics' }, { keywords: ['technology', 'ai', 'artificial intelligence', 'digital', 'tech'], topic: 'Technology' }, { keywords: ['economy', 'business', 'finance', 'market', 'money'], topic: 'Economics' }, { keywords: ['health', 'medicine', 'pandemic', 'covid', 'mental health'], topic: 'Health' }, { keywords: ['culture', 'art', 'music', 'film', 'literature', 'books'], topic: 'Culture' }, { keywords: ['science', 'research', 'discovery', 'study'], topic: 'Science' }, { keywords: ['society', 'social', 'community', 'inequality', 'justice'], topic: 'Society' }, { keywords: ['travel', 'places', 'cities', 'countries', 'explore'], topic: 'Travel' }, { keywords: ['sports', 'football', 'athletics', 'games'], topic: 'Sports' }, { keywords: ['food', 'cooking', 'restaurants', 'cuisine'], topic: 'Food' }, { keywords: ['war', 'conflict', 'international', 'world', 'global'], topic: 'World Affairs' }, { keywords: ['personal', 'memoir', 'life', 'experience', 'story'], topic: 'Personal Stories' }, { keywords: ['history', 'historical', 'past', 'archive'], topic: 'History' } ]; for (const topicGroup of topicKeywords) { if (topicGroup.keywords.some(keyword => fullText.includes(keyword))) { analysis.interests.push(topicGroup.topic); } } // Extract content type preferences const typeKeywords = [ { keywords: ['investigation', 'investigative', 'expose'], type: 'investigative' }, { keywords: ['profile', 'biography', 'portrait'], type: 'profile' }, { keywords: ['analysis', 'deep dive', 'explained'], type: 'analysis' }, { keywords: ['narrative', 'story', 'tale'], type: 'narrative' }, { keywords: ['review', 'opinion', 'commentary'], type: 'commentary' } ]; for (const typeGroup of typeKeywords) { if (typeGroup.keywords.some(keyword => fullText.includes(keyword))) { analysis.preferredTypes.push(typeGroup.type); } } // Extract themes const themeKeywords = [ { keywords: ['inspiring', 'hopeful', 'positive'], theme: 'uplifting' }, { keywords: ['serious', 'important', 'critical'], theme: 'serious' }, { keywords: ['fascinating', 'interesting', 'curious'], theme: 'intriguing' }, { keywords: ['new', 'recent', 'current'], theme: 'contemporary' }, { keywords: ['unusual', 'weird', 'strange', 'surprising'], theme: 'unusual' } ]; for (const themeGroup of themeKeywords) { if (themeGroup.keywords.some(keyword => fullText.includes(keyword))) { analysis.themes.push(themeGroup.theme); } } // If no interests detected, add some popular defaults if (analysis.interests.length === 0) { analysis.interests = ['Culture', 'Society', 'World Affairs']; } return analysis; } function scoreLongread(article: any, contextAnalysis: ContextAnalysis): LongreadRecommendation { let score = 0; const reasons: string[] = []; const topics: string[] = []; // Base quality score for Long Reads score += 40; // All Long Reads have baseline quality // Analyze article tags for topic matching if (article.tags) { const articleTopics = article.tags .filter((tag: any) => tag.type === 'keyword') .map((tag: any) => tag.webTitle); topics.push(...articleTopics.slice(0, 4)); // Limit displayed topics // Score based on interest matching for (const interest of contextAnalysis.interests) { const matchingTags = articleTopics.filter((topic: string) => topic.toLowerCase().includes(interest.toLowerCase()) || interest.toLowerCase().includes(topic.toLowerCase()) ); if (matchingTags.length > 0) { score += 20; reasons.push(`matches ${interest.toLowerCase()}`); } } } // Headline and standfirst analysis const headline = article.webTitle?.toLowerCase() || ''; const standfirst = article.fields?.standfirst?.toLowerCase() || ''; const fullText = `${headline} ${standfirst}`; // Theme matching for (const theme of contextAnalysis.themes) { const themeWords = getThemeWords(theme); if (themeWords.some(word => fullText.includes(word))) { score += 15; reasons.push(`${theme} content`); } } // Content type analysis for (const type of contextAnalysis.preferredTypes) { const typeWords = getTypeWords(type); if (typeWords.some(word => fullText.includes(word))) { score += 18; reasons.push(`${type} style`); } } // Word count analysis const wordCount = article.fields?.wordcount ? parseInt(article.fields.wordcount) : 0; const readingTime = calculateReadingTime(wordCount); if (wordCount > 3000) { score += 10; reasons.push('comprehensive coverage'); } else if (wordCount > 2000) { score += 8; reasons.push('detailed exploration'); } // Recency bonus (fresher content is generally preferred) if (article.fields?.firstPublicationDate) { const pubDate = new Date(article.fields.firstPublicationDate); const daysSince = (Date.now() - pubDate.getTime()) / (1000 * 60 * 60 * 24); if (daysSince < 30) { score += 12; reasons.push('recent publication'); } else if (daysSince < 60) { score += 8; reasons.push('fairly recent'); } } // Author recognition (some Long Read authors are particularly renowned) const byline = article.fields?.byline?.toLowerCase() || ''; const prominentAuthors = ['john', 'rachel', 'david', 'sarah', 'michael', 'emma']; // Simplified check if (prominentAuthors.some(name => byline.includes(name))) { score += 8; reasons.push('acclaimed author'); } // Ensure we have some reasons if (reasons.length === 0) { reasons.push('quality longform journalism'); } // Limit score to 100 score = Math.min(100, score); return { article, relevanceScore: score, reasons, readingTime, topics: topics.length > 0 ? topics : ['General Interest'] }; } function getThemeWords(theme: string): string[] { const themeWordMap: Record<string, string[]> = { 'uplifting': ['hope', 'inspiring', 'positive', 'success', 'triumph', 'joy'], 'serious': ['crisis', 'urgent', 'important', 'critical', 'severe', 'major'], 'intriguing': ['mystery', 'fascinating', 'remarkable', 'extraordinary', 'unusual', 'curious'], 'contemporary': ['new', 'modern', 'recent', 'current', 'today', 'now'], 'unusual': ['strange', 'bizarre', 'weird', 'unexpected', 'surprising', 'odd'] }; return themeWordMap[theme] || []; } function getTypeWords(type: string): string[] { const typeWordMap: Record<string, string[]> = { 'investigative': ['investigation', 'expose', 'reveals', 'uncovers', 'exclusive', 'probe'], 'profile': ['profile', 'portrait', 'life', 'biography', 'story of', 'meet'], 'analysis': ['analysis', 'explained', 'understanding', 'deep dive', 'breakdown', 'examine'], 'narrative': ['story', 'tale', 'journey', 'adventure', 'experience', 'narrative'], 'commentary': ['opinion', 'view', 'perspective', 'argues', 'believes', 'thinks'] }; return typeWordMap[type] || []; } function calculateReadingTime(wordCount: number): string { if (wordCount === 0) return 'Unknown length'; const wordsPerMinute = 250; // Average reading speed const minutes = Math.ceil(wordCount / wordsPerMinute); if (minutes < 60) { return `${minutes} min read`; } else { const hours = Math.floor(minutes / 60); const remainingMinutes = minutes % 60; return `${hours}h ${remainingMinutes}m read`; } } function getPopularLongreadTopics(longreads: any[]): string[] { const topicCounts: Record<string, number> = {}; longreads.forEach(article => { if (article.tags) { article.tags .filter((tag: any) => tag.type === 'keyword') .forEach((tag: any) => { const topic = tag.webTitle; topicCounts[topic] = (topicCounts[topic] || 0) + 1; }); } }); return Object.entries(topicCounts) .sort(([,a], [,b]) => b - a) .map(([topic]) => topic) .slice(0, 10); }

Implementation Reference

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/jbenton/guardian-mcp-server'

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