Skip to main content
Glama

guardian_find_related

Identify related articles from The Guardian archives using shared tags, filter by section, publish date, or similarity threshold to refine search results.

Instructions

Find articles related to a given article using shared tags

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
article_idYesGuardian article ID or full URL to find related articles for
exclude_same_sectionNoExclude articles from the same section (default: false)
max_days_oldNoOnly find articles within this many days of the original (default: unlimited)
page_sizeNoResults per page, max 50 (default: 10)
similarity_thresholdNoMinimum number of shared tags required (default: 2)

Implementation Reference

  • The core handler function that executes the 'guardian_find_related' tool logic: parses input, fetches original article tags, searches by tags, computes similarity scores based on shared tags, applies filters, sorts, and returns formatted list of related articles.
    export async function guardianFindRelated(client: GuardianClient, args: any): Promise<string> { const params = FindRelatedParamsSchema.parse(args); // First, get the original article with all its tags const parsedId = parseGuardianUrl(params.article_id); const articleId = parsedId.startsWith('/') ? parsedId : `/${parsedId}`; const response = await client.getArticle(articleId, { 'show-tags': 'all', 'show-fields': 'headline,firstPublicationDate' }); const originalArticle = response.response.content; if (!originalArticle) { return 'Original article not found.'; } const originalTags = originalArticle.tags || []; const originalSection = originalArticle.sectionId; const originalDate = originalArticle.webPublicationDate; if (originalTags.length === 0) { return 'Original article has no tags for similarity matching.'; } // Extract useful tags (excluding very generic ones) const usefulTags = originalTags .filter(tag => { const tagType = tag.type; const tagId = tag.id; // Focus on more specific tags return ['keyword', 'contributor', 'series'].includes(tagType) && tagId.split('/').length >= 2; }) .map(tag => tag.id); if (usefulTags.length === 0) { return 'Original article has no specific tags for similarity matching.'; } // Search for articles with shared tags const similarityThreshold = params.similarity_threshold || 2; const excludeSameSection = params.exclude_same_section || false; const maxDaysOld = params.max_days_old; const relatedArticles: any[] = []; // Search for each tag and collect results (limit to first 5 tags to avoid too many API calls) for (const tag of usefulTags.slice(0, 5)) { const searchParams: Record<string, any> = { tag: tag, 'show-tags': 'all', 'show-fields': 'headline,standfirst,byline,publication,firstPublicationDate', 'page-size': 20 }; if (maxDaysOld && originalDate) { // Calculate date range const origDate = new Date(originalDate); const minDate = new Date(origDate.getTime() - maxDaysOld * 24 * 60 * 60 * 1000); const maxDate = new Date(origDate.getTime() + maxDaysOld * 24 * 60 * 60 * 1000); searchParams['from-date'] = minDate.toISOString().substring(0, 10); searchParams['to-date'] = maxDate.toISOString().substring(0, 10); } try { const tagResponse = await client.search(searchParams); const articles = tagResponse.response.results; for (const article of articles) { if (article.id !== originalArticle.id) { // Exclude original relatedArticles.push(article); } } } catch (error) { // Continue with other tags if one fails continue; } } // Count shared tags and rank by similarity const similarityScores: Record<string, { article: any; sharedTags: number }> = {}; for (const article of relatedArticles) { const articleId = article.id; if (!(articleId in similarityScores)) { const articleTags = (article.tags || []).map((tag: any) => tag.id); const sharedCount = usefulTags.filter(tag => articleTags.includes(tag)).length; // Apply filters if (excludeSameSection && article.sectionId === originalSection) { continue; } if (sharedCount >= similarityThreshold) { similarityScores[articleId] = { article: article, sharedTags: sharedCount }; } } } // Sort by similarity and limit results const pageSize = params.page_size || 10; const sortedSimilar = Object.values(similarityScores) .sort((a, b) => b.sharedTags - a.sharedTags) .slice(0, pageSize); if (sortedSimilar.length > 0) { let result = `Found ${sortedSimilar.length} related article(s) to '${originalArticle.webTitle || 'Unknown'}':\n\n`; sortedSimilar.forEach((item, index) => { const article = item.article; const sharedCount = item.sharedTags; result += `**${index + 1}. ${article.webTitle || 'Untitled'}** (Similarity: ${sharedCount} shared tags)\n`; // Show shared tags for transparency const articleTags = (article.tags || []).map((tag: any) => tag.id); const sharedTags = usefulTags.filter(tag => articleTags.includes(tag)); if (sharedTags.length > 0) { result += `Shared tags: ${sharedTags.slice(0, 3).join(', ')}${sharedTags.length > 3 ? ' (+' + (sharedTags.length - 3) + ' more)' : ''}\n`; } if (article.fields) { const { fields } = article; if (fields.byline) { result += `By: ${fields.byline}\n`; } if (fields.firstPublicationDate) { const pubDate = fields.firstPublicationDate.substring(0, 10); result += `Published: ${pubDate}\n`; } if (fields.standfirst) { result += `Summary: ${fields.standfirst}\n`; } } result += `Section: ${article.sectionName || 'Unknown'}\n`; result += `URL: ${article.webUrl || 'N/A'}\n\n`; }); return result; } else { return `No related articles found with at least ${similarityThreshold} shared tags.`; } }
  • Zod schema used for input validation in the handler function.
    export const FindRelatedParamsSchema = z.object({ article_id: z.string(), similarity_threshold: z.number().min(1).max(10).optional(), exclude_same_section: z.boolean().optional(), max_days_old: z.number().min(1).optional(), page_size: z.number().min(1).max(50).optional(), }); export type SearchParams = z.infer<typeof SearchParamsSchema>; export type GetArticleParams = z.infer<typeof GetArticleParamsSchema>; export type LongReadParams = z.infer<typeof LongReadParamsSchema>; export type LookbackParams = z.infer<typeof LookbackParamsSchema>; export type BrowseSectionParams = z.infer<typeof BrowseSectionParamsSchema>; export type SearchTagsParams = z.infer<typeof SearchTagsParamsSchema>; export type SearchByLengthParams = z.infer<typeof SearchByLengthParamsSchema>; export type SearchByAuthorParams = z.infer<typeof SearchByAuthorParamsSchema>; export type FindRelatedParams = z.infer<typeof FindRelatedParamsSchema>;
  • Tool registration in the registerTools function, mapping the tool name to its handler.
    guardian_find_related: (args) => guardianFindRelated(client, args),
  • Input schema exposed via ListTools MCP request for client-side validation.
    name: 'guardian_find_related', description: 'Find articles related to a given article using shared tags', inputSchema: { type: 'object', properties: { article_id: { type: 'string', description: 'Guardian article ID or full URL to find related articles for', }, similarity_threshold: { type: 'integer', description: 'Minimum number of shared tags required (default: 2)', minimum: 1, maximum: 10, }, exclude_same_section: { type: 'boolean', description: 'Exclude articles from the same section (default: false)', }, max_days_old: { type: 'integer', description: 'Only find articles within this many days of the original (default: unlimited)', minimum: 1, }, page_size: { type: 'integer', description: 'Results per page, max 50 (default: 10)', minimum: 1, maximum: 50, }, }, required: ['article_id'], },

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