Skip to main content
Glama
jbenton

guardian-mcp-server

by jbenton

guardian_author_profile

Analyze Guardian journalist profiles by examining their published articles to identify writing patterns, topics covered, and publication frequency within specified time periods.

Instructions

Generate comprehensive profile analysis for a Guardian journalist

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
authorYesAuthor/journalist name to analyze
analysis_periodNoYear to analyze (e.g., "2024") or use from_date/to_date
from_dateNoStart date (YYYY-MM-DD) - alternative to analysis_period
to_dateNoEnd date (YYYY-MM-DD) - alternative to analysis_period

Implementation Reference

  • The core handler function that implements the guardian_author_profile tool. It validates input, determines date range, searches for author articles using GuardianClient, computes statistics (articles, sections, monthly output, word count, tags, headlines), analyzes patterns, and returns a formatted profile report.
    export async function guardianAuthorProfile(client: GuardianClient, args: any): Promise<string> {
      const params = AuthorProfileParamsSchema.parse(args);
    
      // Handle analysis period - can be year or date range
      let fromDate: string;
      let toDate: string;
      
      if (params.analysis_period && !params.from_date && !params.to_date) {
        // Handle year format like "2024"
        const year = params.analysis_period;
        if (/^\d{4}$/.test(year)) {
          fromDate = `${year}-01-01`;
          toDate = `${year}-12-31`;
        } else {
          throw new Error('analysis_period must be a 4-digit year (e.g., "2024")');
        }
      } else if (params.from_date && params.to_date) {
        fromDate = validateDate(params.from_date) || params.from_date;
        toDate = validateDate(params.to_date) || params.to_date;
      } else {
        // Default to last year
        const now = new Date();
        fromDate = `${now.getFullYear() - 1}-01-01`;
        toDate = `${now.getFullYear() - 1}-12-31`;
      }
    
      if (!validateDate(fromDate) || !validateDate(toDate)) {
        throw new Error('Invalid date format. Use YYYY-MM-DD format.');
      }
    
      let result = `Author Profile: ${params.author} (${fromDate} to ${toDate})\n\n`;
    
      // Search for all articles by the author in the period
      const searchParams: Record<string, any> = {
        q: `"${params.author}"`,
        'from-date': fromDate,
        'to-date': toDate,
        'page-size': 200, // Get as many as possible for analysis
        'show-fields': 'headline,byline,firstPublicationDate,wordcount,standfirst',
        'show-tags': 'keyword,type',
        'order-by': 'newest'
      };
    
      const response = await client.search(searchParams);
      const allArticles = response.response.results;
    
      // Filter to only articles where the author name appears in the byline
      const authorArticles = allArticles.filter(article => {
        const byline = article.fields?.byline || '';
        return byline.toLowerCase().includes(params.author.toLowerCase());
      });
    
      if (authorArticles.length === 0) {
        return `No articles found for author "${params.author}" in the specified period.`;
      }
    
      const stats: AuthorStats = {
        totalArticles: authorArticles.length,
        sectionCoverage: {},
        monthlyOutput: {},
        averageWordCount: 0,
        topTags: {},
        recentHeadlines: []
      };
    
      // Analyze the articles
      let totalWords = 0;
      let wordCountArticles = 0;
    
      authorArticles.forEach(article => {
        // Section coverage
        const section = article.sectionName || 'Unknown';
        stats.sectionCoverage[section] = (stats.sectionCoverage[section] || 0) + 1;
    
        // Monthly output
        if (article.fields?.firstPublicationDate) {
          const date = new Date(article.fields.firstPublicationDate);
          const monthKey = date.toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
          stats.monthlyOutput[monthKey] = (stats.monthlyOutput[monthKey] || 0) + 1;
        }
    
        // Word count analysis
        if (article.fields?.wordcount && !isNaN(Number(article.fields.wordcount))) {
          totalWords += Number(article.fields.wordcount);
          wordCountArticles++;
        }
    
        // Tag analysis
        if (article.tags) {
          article.tags.forEach(tag => {
            if (tag.type === 'keyword') {
              stats.topTags[tag.webTitle] = (stats.topTags[tag.webTitle] || 0) + 1;
            }
          });
        }
    
        // Recent headlines (first 5)
        if (stats.recentHeadlines.length < 5) {
          stats.recentHeadlines.push(article.webTitle || 'Untitled');
        }
      });
    
      stats.averageWordCount = wordCountArticles > 0 ? Math.round(totalWords / wordCountArticles) : 0;
    
      // Format the results
      result += `**Publishing Statistics**\n`;
      result += `• Total Articles: ${stats.totalArticles}\n`;
      result += `• Average Word Count: ${stats.averageWordCount} words\n`;
      
      const avgPerMonth = (stats.totalArticles / 12).toFixed(1);
      result += `• Average Output: ${avgPerMonth} articles per month\n\n`;
    
      // Section coverage
      result += `**Section Coverage**\n`;
      const sortedSections = Object.entries(stats.sectionCoverage)
        .sort(([,a], [,b]) => b - a)
        .slice(0, 8);
      
      sortedSections.forEach(([section, count]) => {
        const percentage = ((count / stats.totalArticles) * 100).toFixed(1);
        result += `• ${section}: ${count} articles (${percentage}%)\n`;
      });
      result += '\n';
    
      // Monthly activity (show top 6 months)
      result += `**Most Active Months**\n`;
      const sortedMonths = Object.entries(stats.monthlyOutput)
        .sort(([,a], [,b]) => b - a)
        .slice(0, 6);
      
      sortedMonths.forEach(([month, count]) => {
        result += `• ${month}: ${count} articles\n`;
      });
      result += '\n';
    
      // Top topics/tags
      result += `**Top Topics**\n`;
      const sortedTags = Object.entries(stats.topTags)
        .sort(([,a], [,b]) => b - a)
        .slice(0, 10);
      
      sortedTags.forEach(([tag, count]) => {
        result += `• ${tag}: ${count} articles\n`;
      });
      result += '\n';
    
      // Recent work
      result += `**Recent Headlines**\n`;
      stats.recentHeadlines.forEach((headline, index) => {
        result += `${index + 1}. ${headline}\n`;
      });
    
      // Writing patterns analysis
      result += `\n**Writing Patterns**\n`;
      
      // Determine writing style based on word count
      if (stats.averageWordCount < 400) {
        result += `• Style: Brief news reporting (avg. ${stats.averageWordCount} words)\n`;
      } else if (stats.averageWordCount < 800) {
        result += `• Style: Standard journalism (avg. ${stats.averageWordCount} words)\n`;
      } else {
        result += `• Style: Long-form analysis (avg. ${stats.averageWordCount} words)\n`;
      }
    
      // Specialization analysis
      const topSection = sortedSections[0];
      if (topSection && (topSection[1] / stats.totalArticles) > 0.5) {
        result += `• Specialization: ${topSection[0]} specialist (${((topSection[1] / stats.totalArticles) * 100).toFixed(1)}% coverage)\n`;
      } else {
        result += `• Specialization: Multi-section correspondent\n`;
      }
    
      return result;
    }
  • Zod schema used for input validation in the handler. Defines required 'author' and optional date parameters.
    export const AuthorProfileParamsSchema = z.object({
      author: z.string(),
      analysis_period: z.string().optional(), // Can be year like "2024" or date range
      from_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
      to_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
    });
  • Tool registration in the registerTools function that maps 'guardian_author_profile' to the handler.
    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),
        guardian_get_article_tags: (args) => guardianGetArticleTags(client, args),
        guardian_content_timeline: (args) => guardianContentTimeline(client, args),
        guardian_author_profile: (args) => guardianAuthorProfile(client, args),
        guardian_topic_trends: (args) => guardianTopicTrends(client, args),
        guardian_top_stories_by_date: (args) => guardianTopStoriesByDate(client, args),
        guardian_recommend_longreads: (args) => guardianRecommendLongreads(client, args),
      };
    }
  • src/index.ts:432-457 (registration)
    MCP protocol tool registration including name, description, and input schema definition in the ListTools response.
    {
      name: 'guardian_author_profile',
      description: 'Generate comprehensive profile analysis for a Guardian journalist',
      inputSchema: {
        type: 'object',
        properties: {
          author: {
            type: 'string',
            description: 'Author/journalist name to analyze',
          },
          analysis_period: {
            type: 'string',
            description: 'Year to analyze (e.g., "2024") or use from_date/to_date',
          },
          from_date: {
            type: 'string',
            description: 'Start date (YYYY-MM-DD) - alternative to analysis_period',
          },
          to_date: {
            type: 'string',
            description: 'End date (YYYY-MM-DD) - alternative to analysis_period',
          },
        },
        required: ['author'],
      },
    },

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