guardian_recommend_longreads
Get personalized long-form article recommendations from The Guardian archives based on your interests, topics, and conversation context.
Instructions
Get personalized Long Read recommendations based on context and preferences
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| count | No | Number of recommendations (default: 3, max: 10) | |
| context | No | Context about interests, current conversation, or what you're looking for | |
| from_date | No | Earliest publication date to consider (default: 3 months ago) | |
| topic_preference | No | Specific topic or theme preference (e.g., "climate change", "technology", "culture") |
Implementation Reference
- The main asynchronous handler function that executes the tool: parses arguments, fetches Long Read articles from Guardian API, performs context analysis, scores relevance, ranks top recommendations, and returns formatted output.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; }
- src/types/guardian.ts:181-186 (schema)Zod schema defining the input parameters for the tool, used for validation in the handler.export const RecommendLongreadsParamsSchema = z.object({ count: z.number().min(1).max(10).optional(), context: z.string().optional(), from_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(), topic_preference: z.string().optional(), });
- src/tools/index.ts:38-38 (registration)Registers the tool handler function in the tools map returned by registerTools, mapping the snake_case name to the camelCase function.guardian_recommend_longreads: (args) => guardianRecommendLongreads(client, args),
- src/index.ts:515-540 (registration)Registers the tool in the MCP server by including it in the listTools response with name, description, and input schema (mirroring the Zod schema).name: 'guardian_recommend_longreads', description: 'Get personalized Long Read recommendations based on context and preferences', inputSchema: { type: 'object', properties: { count: { type: 'integer', description: 'Number of recommendations (default: 3, max: 10)', minimum: 1, maximum: 10, }, context: { type: 'string', description: 'Context about interests, current conversation, or what you\'re looking for', }, from_date: { type: 'string', description: 'Earliest publication date to consider (default: 3 months ago)', }, topic_preference: { type: 'string', description: 'Specific topic or theme preference (e.g., "climate change", "technology", "culture")', }, }, }, },
- Key helper function that scores individual Long Read articles for relevance to the user's context and preferences.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'] }; }