Skip to main content
Glama
index.ts20.4 kB
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, InitializeRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import { GuardianClient, GuardianApiError } from './api/guardian-client.js'; import { registerTools } from './tools/index.js'; const SERVER_VERSION = '1.0.0'; class GuardianMcpServer { private server: Server; private guardianClient: GuardianClient; constructor() { this.server = new Server( { name: 'guardian', version: SERVER_VERSION, }, { capabilities: { tools: {}, }, } ); try { this.guardianClient = new GuardianClient(); } catch (error) { console.error('Failed to initialize Guardian client:', error instanceof Error ? error.message : error); process.exit(1); } this.setupHandlers(); } private setupHandlers(): void { this.server.setRequestHandler(InitializeRequestSchema, async (request) => { return { protocolVersion: '2024-11-05', capabilities: { tools: {}, }, serverInfo: { name: 'guardian', version: SERVER_VERSION, }, }; }); this.server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: 'guardian_search', description: 'Search Guardian articles with flexible filtering options', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search terms (can be empty to browse all content)', }, section: { type: 'string', description: 'Filter by section ID (get available sections via guardian_get_sections)', }, tag: { type: 'string', description: 'Filter by tag (over 50,000 available tags)', }, from_date: { type: 'string', description: 'Start date (YYYY-MM-DD format)', }, to_date: { type: 'string', description: 'End date (YYYY-MM-DD format)', }, order_by: { type: 'string', description: "Sort order: 'newest', 'oldest', 'relevance' (default: 'relevance')", enum: ['newest', 'oldest', 'relevance'], }, page_size: { type: 'integer', description: 'Results per page, max 200 (default: 20)', minimum: 1, maximum: 200, }, page: { type: 'integer', description: 'Page number (default: 1)', minimum: 1, }, show_fields: { type: 'string', description: 'Comma-separated fields to include (headline,standfirst,body,byline,thumbnail,publication)', }, production_office: { type: 'string', description: "Filter by office: 'uk', 'us', 'au'", enum: ['uk', 'us', 'au'], }, detail_level: { type: 'string', description: "Response detail level: 'minimal' (fast), 'standard' (default), 'full' (complete)", enum: ['minimal', 'standard', 'full'], }, }, }, }, { name: 'guardian_get_article', description: 'Retrieve full content of a specific Guardian article', inputSchema: { type: 'object', properties: { article_id: { type: 'string', description: 'The Guardian article ID or full URL (e.g., "politics/2024/dec/01/example" or "https://www.theguardian.com/politics/2024/dec/01/example")', }, show_fields: { type: 'string', description: 'Fields to include (default: headline,standfirst,body,byline,publication,firstPublicationDate)', }, truncate: { type: 'boolean', description: 'Whether to truncate content to preview length (default: false for full content)', }, }, required: ['article_id'], }, }, { name: 'guardian_longread', description: 'Search specifically for articles from The Long Read series', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search terms within Long Read articles', }, from_date: { type: 'string', description: 'Start date (YYYY-MM-DD format)', }, to_date: { type: 'string', description: 'End date (YYYY-MM-DD format)', }, page_size: { type: 'integer', description: 'Results per page, max 200 (default: 10)', minimum: 1, maximum: 200, }, page: { type: 'integer', description: 'Page number (default: 1)', minimum: 1, }, }, }, }, { name: 'guardian_lookback', description: 'Find top stories from a specific date or date range', inputSchema: { type: 'object', properties: { date: { type: 'string', description: 'Specific date (YYYY-MM-DD) or start of range', }, end_date: { type: 'string', description: 'End date for range (YYYY-MM-DD)', }, section: { type: 'string', description: 'Filter by section', }, page_size: { type: 'integer', description: 'Number of results (default: 20)', minimum: 1, maximum: 200, }, }, required: ['date'], }, }, { name: 'guardian_browse_section', description: 'Browse recent articles from a specific Guardian section', inputSchema: { type: 'object', properties: { section: { type: 'string', description: 'Section ID (use guardian_get_sections to find valid IDs)', }, days_back: { type: 'integer', description: 'How many days back to search (default: 7)', minimum: 1, maximum: 365, }, page_size: { type: 'integer', description: 'Number of results, max 200 (default: 20)', minimum: 1, maximum: 200, }, }, required: ['section'], }, }, { name: 'guardian_get_sections', description: 'Get all available Guardian sections', inputSchema: { type: 'object', properties: {}, }, }, { name: 'guardian_search_tags', description: "Search through Guardian's 50,000+ tags to find relevant ones", inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search term for tag names', }, page_size: { type: 'integer', description: 'Results per page, max 200 (default: 20)', minimum: 1, maximum: 200, }, page: { type: 'integer', description: 'Page number (default: 1)', minimum: 1, }, }, required: ['query'], }, }, { name: 'guardian_search_by_length', description: 'Search Guardian articles filtered by word count range', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search terms (optional)', }, min_words: { type: 'integer', description: 'Minimum word count (default: 0)', minimum: 0, }, max_words: { type: 'integer', description: 'Maximum word count (default: unlimited)', minimum: 1, }, section: { type: 'string', description: 'Filter by section ID', }, from_date: { type: 'string', description: 'Start date (YYYY-MM-DD format)', }, to_date: { type: 'string', description: 'End date (YYYY-MM-DD format)', }, order_by: { type: 'string', description: "Sort order: 'newest', 'oldest', 'relevance' (default: 'newest')", enum: ['newest', 'oldest', 'relevance'], }, page_size: { type: 'integer', description: 'Results per page, max 200 (default: 20)', minimum: 1, maximum: 200, }, }, }, }, { name: 'guardian_search_by_author', description: 'Search Guardian articles by specific author/journalist', inputSchema: { type: 'object', properties: { author: { type: 'string', description: 'Author name to search for', }, query: { type: 'string', description: "Additional search terms within author's articles", }, section: { type: 'string', description: 'Filter by section ID', }, from_date: { type: 'string', description: 'Start date (YYYY-MM-DD format)', }, to_date: { type: 'string', description: 'End date (YYYY-MM-DD format)', }, order_by: { type: 'string', description: "Sort order: 'newest', 'oldest', 'relevance' (default: 'newest')", enum: ['newest', 'oldest', 'relevance'], }, page_size: { type: 'integer', description: 'Results per page, max 200 (default: 20)', minimum: 1, maximum: 200, }, page: { type: 'integer', description: 'Page number (default: 1)', minimum: 1, }, }, required: ['author'], }, }, { 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'], }, }, { name: 'guardian_get_article_tags', description: 'Get detailed tag information for a specific Guardian article', inputSchema: { type: 'object', properties: { article_id: { type: 'string', description: 'Guardian article ID or full URL to inspect tags for', }, }, required: ['article_id'], }, }, { name: 'guardian_content_timeline', description: 'Analyze content timeline for a topic over time showing trends and peaks', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Topic or search terms to analyze over time', }, from_date: { type: 'string', description: 'Start date (YYYY-MM-DD)', }, to_date: { type: 'string', description: 'End date (YYYY-MM-DD)', }, interval: { type: 'string', description: 'Time interval for analysis', enum: ['day', 'week', 'month', 'quarter'], }, section: { type: 'string', description: 'Filter by section (optional)', }, }, required: ['query', 'from_date', 'to_date'], }, }, { 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'], }, }, { name: 'guardian_topic_trends', description: 'Compare trends of multiple topics over time with correlation analysis', inputSchema: { type: 'object', properties: { topics: { type: 'array', items: { type: 'string', }, description: 'List of topics/keywords to compare (max 5)', minItems: 1, maxItems: 5, }, from_date: { type: 'string', description: 'Start date (YYYY-MM-DD)', }, to_date: { type: 'string', description: 'End date (YYYY-MM-DD)', }, interval: { type: 'string', description: 'Time interval for comparison', enum: ['month', 'quarter', 'year'], }, }, required: ['topics', 'from_date', 'to_date'], }, }, { name: 'guardian_top_stories_by_date', description: 'Get intelligently ranked top stories for a specific date using editorial prioritization', inputSchema: { type: 'object', properties: { date: { type: 'string', description: 'Date to analyze (YYYY-MM-DD)', }, story_count: { type: 'integer', description: 'Number of top stories to return (default: 10, max: 20)', minimum: 1, maximum: 20, }, section: { type: 'string', description: 'Filter by section (optional)', }, }, required: ['date'], }, }, { 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")', }, }, }, }, ], }; }); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { const result = await this.handleToolCall(name, args); return { content: [ { type: 'text', text: result, }, ], }; } catch (error) { if (error instanceof GuardianApiError) { return { content: [ { type: 'text', text: `Error: ${error.message}`, }, ], }; } throw error; } }); } private async handleToolCall(toolName: string, args: any): Promise<string> { const tools = registerTools(this.guardianClient); const tool = tools[toolName]; if (!tool) { throw new Error(`Unknown tool: ${toolName}`); } return await tool(args); } async run(): Promise<void> { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('Guardian MCP server running on stdio'); } } const server = new GuardianMcpServer(); server.run().catch((error) => { console.error('Server error:', error); process.exit(1); });

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