Skip to main content
Glama
jbenton

guardian-mcp-server

by jbenton

guardian_find_related

Find Guardian articles related to a specific article by analyzing shared tags, with options to filter by recency, section, and similarity threshold.

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
similarity_thresholdNoMinimum number of shared tags required (default: 2)
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)

Implementation Reference

  • The main handler function that executes the guardian_find_related tool: parses args, fetches original article tags, searches for articles sharing tags, ranks by shared tag count, filters, 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 for validating input parameters to the guardian_find_related tool.
    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(),
    });
  • Registration of the tool handler in the tools registry: imports the handler and maps 'guardian_find_related' to it within registerTools function.
    import { guardianFindRelated } from './guardian-find-related.js';
    import { guardianGetArticleTags } from './guardian-get-article-tags.js';
    import { guardianContentTimeline } from './guardian-content-timeline.js';
    import { guardianAuthorProfile } from './guardian-author-profile.js';
    import { guardianTopicTrends } from './guardian-topic-trends.js';
    import { guardianTopStoriesByDate } from './guardian-top-stories-by-date.js';
    import { guardianRecommendLongreads } from './guardian-recommend-longreads.js';
    
    export type ToolHandler = (args: any) => Promise<string>;
    
    export function registerTools(client: GuardianClient): Record<string, ToolHandler> {
      return {
        guardian_search: (args) => guardianSearch(client, args),
        guardian_get_article: (args) => guardianGetArticle(client, args),
        guardian_longread: (args) => guardianLongread(client, args),
        guardian_lookback: (args) => guardianLookback(client, args),
        guardian_browse_section: (args) => guardianBrowseSection(client, args),
        guardian_get_sections: (args) => guardianGetSections(client, args),
        guardian_search_tags: (args) => guardianSearchTags(client, args),
        guardian_search_by_length: (args) => guardianSearchByLength(client, args),
        guardian_search_by_author: (args) => guardianSearchByAuthor(client, args),
        guardian_find_related: (args) => guardianFindRelated(client, args),
  • src/index.ts:353-386 (registration)
    MCP tool registration in ListToolsRequestSchema handler: defines the tool name, description, and input schema for the protocol.
      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