UNPKG

@rhofkens/mcp-quotes-server-claude-code

Version:

Model Context Protocol (MCP) server for managing and serving quotes

259 lines 9.8 kB
/** * Resilient Get Quotes Tool * * Enhanced version of getQuotes with full resilience patterns */ import { resilientSerperClient } from '../services/resilientSerperClient.js'; import { QuoteCache } from '../utils/cache.js'; import { ValidationError, wrapError } from '../utils/errors.js'; import { logger } from '../utils/logger.js'; import { QuoteSchemas, validate } from '../utils/validation.js'; /** * Input schema for the getResilientQuotes tool */ const getResilientQuotesSchema = QuoteSchemas.getQuotesParams; /** * Handler function for the getResilientQuotes tool */ async function getResilientQuotesHandler(params) { try { // Validate input parameters const validatedParams = validate(getResilientQuotesSchema, params); const { person, numberOfQuotes, topic } = validatedParams; logger.info('Getting quotes with resilience', { person, numberOfQuotes, topic }); // Generate cache key for direct cache check const cacheKey = QuoteCache.generateKey(person, topic, numberOfQuotes); const cacheCheck = resilientSerperClient['cache'].getWithFallback(cacheKey); // If we have fresh cache, return immediately if (cacheCheck.data && !cacheCheck.stale) { logger.info('Returning fresh cached quotes', { person, count: cacheCheck.data.length, }); return { quotes: cacheCheck.data.slice(0, numberOfQuotes), metadata: { cached: true, stale: false, }, }; } // Build search query const searchQuery = resilientSerperClient.buildQuoteSearchQuery(person, topic); // Track retry attempts const retryCount = 0; try { // Search for quotes using resilient client const searchResults = await resilientSerperClient.searchQuotes({ query: searchQuery, num: numberOfQuotes * 2, // Request more to account for filtering }); // Process search results into quotes const quotes = []; const seenQuotes = new Set(); for (const result of searchResults) { if (quotes.length >= numberOfQuotes) { break; } const quoteText = resilientSerperClient.extractQuoteFromSnippet(result.snippet); if (quoteText && !seenQuotes.has(quoteText)) { seenQuotes.add(quoteText); const quote = { text: quoteText, author: person, }; if (result.link) { quote.source = result.link; } quotes.push(quote); } } // If we don't have enough quotes, try broader search if (quotes.length < numberOfQuotes) { logger.warn('Not enough quotes found, trying broader search', { found: quotes.length, requested: numberOfQuotes, }); const broaderQuery = topic ? resilientSerperClient.buildQuoteSearchQuery(person) : `famous quotes by ${person}`; const additionalResults = await resilientSerperClient.searchQuotes({ query: broaderQuery, num: (numberOfQuotes - quotes.length) * 3, }); for (const result of additionalResults) { if (quotes.length >= numberOfQuotes) { break; } const quoteText = resilientSerperClient.extractQuoteFromSnippet(result.snippet); if (quoteText && !seenQuotes.has(quoteText)) { seenQuotes.add(quoteText); const quote = { text: quoteText, author: person, }; if (result.link) { quote.source = result.link; } quotes.push(quote); } } } // Cache the results if (quotes.length > 0) { resilientSerperClient['cache'].set(cacheKey, quotes); } logger.info('Quotes retrieved successfully with resilience', { person, requested: numberOfQuotes, found: quotes.length, topic, retries: retryCount, cached: false, }); return { quotes, metadata: { cached: false, ...(retryCount > 0 && { retries: retryCount }), }, }; } catch (apiError) { // If API fails, check for stale cache if (cacheCheck.data) { logger.warn('API failed, returning stale cached quotes', { person, error: apiError instanceof Error ? apiError.message : String(apiError), }); return { quotes: cacheCheck.data.slice(0, numberOfQuotes), metadata: { cached: true, stale: true, fallback: true, }, }; } // Try any cached quotes for this person const fallbackKey = QuoteCache.generateKey(person); const fallbackCache = resilientSerperClient['cache'].getWithFallback(fallbackKey); if (fallbackCache.data) { logger.warn('Using fallback cache for different query', { person, originalTopic: topic, }); return { quotes: fallbackCache.data.slice(0, numberOfQuotes), metadata: { cached: true, stale: fallbackCache.stale, fallback: true, }, }; } throw apiError; } } catch (error) { // Handle validation errors if (error instanceof ValidationError) { logger.error('Validation error in getResilientQuotes', error); throw error; } // Log and wrap other errors logger.error('Failed to retrieve quotes with resilience', error); throw wrapError(error, 'Failed to retrieve quotes'); } } /** * Tool definition for getResilientQuotes */ export const getResilientQuotesTool = { name: 'getResilientQuotes', description: 'Retrieve quotes with resilience patterns including caching, circuit breaker, and automatic retries. Provides fallback to cached data when external services are unavailable.', inputSchema: { type: 'object', properties: { person: { type: 'string', description: 'The name of the person whose quotes to retrieve', }, numberOfQuotes: { type: 'number', description: 'Number of quotes to return (between 1 and 10)', minimum: 1, maximum: 10, }, topic: { type: 'string', description: 'Optional topic to filter quotes by (e.g., "success", "love", "life")', }, }, required: ['person', 'numberOfQuotes'], }, handler: getResilientQuotesHandler, }; /** * Get health status of the resilient quotes system */ export async function getQuotesHealthStatus() { const health = await resilientSerperClient.getHealthStatus(); return { ...health, recommendation: determineHealthRecommendation(health), }; } /** * Determine health recommendation based on status */ function determineHealthRecommendation(health) { const { circuitBreaker, cache } = health; if (circuitBreaker.state === 'OPEN') { return 'Service is down. Using cached responses only.'; } if (circuitBreaker.state === 'HALF_OPEN') { return 'Service is recovering. Some requests may fail.'; } if (circuitBreaker.failures > 3) { return 'Service experiencing intermittent failures. Monitor closely.'; } const hitRate = cache.hits / (cache.hits + cache.misses); if (hitRate < 0.3 && cache.hits + cache.misses > 50) { return 'Low cache hit rate. Consider pre-warming common queries.'; } return 'System operating normally.'; } /** * Pre-warm cache with common quotes */ export async function prewarmQuotesCache(commonPeople = [ 'Albert Einstein', 'Maya Angelou', 'Mark Twain', 'Oscar Wilde', 'Gandhi', ]) { logger.info('Pre-warming quotes cache', { people: commonPeople }); for (const person of commonPeople) { try { await getResilientQuotesHandler({ person, numberOfQuotes: 5, }); // Small delay to avoid overwhelming the API await new Promise((resolve) => setTimeout(resolve, 1000)); } catch (error) { logger.warn('Failed to pre-warm cache for person', { person, error: error instanceof Error ? error.message : String(error), }); } } } /** * Handler export for tool registry */ export const handleGetResilientQuotes = getResilientQuotesHandler; //# sourceMappingURL=getResilientQuotes.js.map