@rhofkens/mcp-quotes-server-claude-code
Version:
Model Context Protocol (MCP) server for managing and serving quotes
226 lines • 9.01 kB
JavaScript
/**
* Get Quotes Tool
*
* MCP tool for retrieving quotes from a specific person
*/
import { serperClient } from '../services/serperClient.js';
import { APIError, ValidationError, wrapError, createStructuredError, ErrorContextBuilder, generateRequestId, logError, ErrorAggregator, withTimeout, } from '../utils/errorHandling.js';
import { logger } from '../utils/logger.js';
import { QuoteSchemas, validate } from '../utils/validation.js';
/**
* Input schema for the getQuotes tool
*/
const getQuotesSchema = QuoteSchemas.getQuotesParams;
/**
* Handler function for the getQuotes tool with enhanced error handling
*/
async function getQuotesHandler(params) {
const requestId = generateRequestId();
const errorAggregator = new ErrorAggregator();
try {
// Validate input parameters
const validatedParams = validate(getQuotesSchema, params);
const { person, numberOfQuotes, topic } = validatedParams;
// Build error context
const context = new ErrorContextBuilder()
.setOperation('getQuotes')
.setInput({ person, numberOfQuotes, topic })
.setEnvironment()
.build();
logger.info('Getting quotes', {
requestId,
person,
numberOfQuotes,
topic,
});
// Check circuit breaker status
const cbStatus = serperClient.getCircuitBreakerStatus();
if (!cbStatus.canExecute) {
logger.warn('Circuit breaker is open, API temporarily unavailable', {
requestId,
state: cbStatus.state,
});
}
// Build search query
const searchQuery = serperClient.buildQuoteSearchQuery(person, topic);
// Search for quotes using Serper API with timeout
const searchResults = await withTimeout(serperClient.searchQuotes({
query: searchQuery,
num: numberOfQuotes * 2, // Request more to account for filtering
}), 30000, // 30 second overall timeout
'Quote search operation timed out');
// Process search results into quotes
const quotes = [];
const seenQuotes = new Set(); // To avoid duplicates
for (const result of searchResults) {
if (quotes.length >= numberOfQuotes) {
break;
}
try {
// Extract quote from snippet
const quoteText = serperClient.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);
}
}
catch (parseError) {
// Aggregate parsing errors but don't fail the entire operation
errorAggregator.add(parseError instanceof Error ? parseError : new Error(String(parseError)), { snippet: result.snippet });
}
}
// If we don't have enough quotes, search for more generic results
if (quotes.length < numberOfQuotes) {
logger.warn('Not enough quotes found, trying broader search', {
requestId,
found: quotes.length,
requested: numberOfQuotes,
});
try {
// Try a broader search without topic
const broaderQuery = topic
? serperClient.buildQuoteSearchQuery(person)
: `famous quotes by ${person}`;
const additionalResults = await withTimeout(serperClient.searchQuotes({
query: broaderQuery,
num: (numberOfQuotes - quotes.length) * 3,
}), 20000, // 20 second timeout for additional search
'Additional quote search timed out');
for (const result of additionalResults) {
if (quotes.length >= numberOfQuotes) {
break;
}
try {
const quoteText = serperClient.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);
}
}
catch (parseError) {
// Aggregate parsing errors
errorAggregator.add(parseError instanceof Error ? parseError : new Error(String(parseError)), { snippet: result.snippet });
}
}
}
catch (additionalSearchError) {
// Log but don't fail if additional search fails
logger.warn('Additional search failed', {
requestId,
error: additionalSearchError,
});
// Add to context for debugging
if (context.relatedErrors) {
new ErrorContextBuilder().addRelatedError('ADDITIONAL_SEARCH_FAILED', additionalSearchError instanceof Error
? additionalSearchError.message
: String(additionalSearchError));
}
}
}
// Log parsing errors if any
if (errorAggregator.hasErrors()) {
logger.warn('Some quotes could not be parsed', {
requestId,
errors: errorAggregator.getErrors().length,
});
}
// Log final results
logger.info('Quotes retrieved successfully', {
requestId,
person,
requested: numberOfQuotes,
found: quotes.length,
topic,
parsingErrors: errorAggregator.getErrors().length,
});
return { quotes };
}
catch (error) {
// Build comprehensive error context
const context = new ErrorContextBuilder()
.setOperation('getQuotes')
.setInput(params)
.setEnvironment()
.build();
// Handle specific error types
if (error instanceof ValidationError) {
logError(error, context);
// Return structured error for validation failures
const structured = createStructuredError(error, context, requestId);
throw new ValidationError(structured.error.userMessage, error.field, structured.error.details);
}
if (error instanceof APIError) {
logError(error, context);
// Add circuit breaker status to error details
const cbStatus = serperClient.getCircuitBreakerStatus();
error.details = {
...error.details,
circuitBreakerState: cbStatus.state,
requestId,
};
throw error;
}
// Wrap unknown errors with context
logError(error, context, 'error');
const wrappedError = wrapError(error, 'Failed to retrieve quotes');
wrappedError.details = {
...wrappedError.details,
requestId,
};
throw wrappedError;
}
}
/**
* Tool definition for getQuotes
*/
export const getQuotesTool = {
name: 'getQuotes',
description: 'Retrieve quotes from a specific person. Searches for authentic quotes and returns them with attribution and source information.',
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: getQuotesHandler,
};
/**
* Alternative handler that can be used directly (not through MCP)
*/
export async function getQuotes(params) {
const result = await getQuotesHandler(params);
return result.quotes;
}
/**
* Handler export for tool registry
*/
export const handleGetQuotes = getQuotesHandler;
//# sourceMappingURL=getQuotes.js.map