UNPKG

@mseep/atlas-mcp-server

Version:

A Model Context Protocol (MCP) server for ATLAS, a Neo4j-powered task management system for LLM Agents - implementing a three-tier architecture (Projects, Tasks, Knowledge) to manage complex workflows.

131 lines (112 loc) 5.31 kB
import { SearchResultItem } from "../../../services/neo4j/searchService.js"; import { ResponseFormatter, createFormattedResponse } from "../../../utils/responseFormatter.js"; import { UnifiedSearchResponse } from "./types.js"; /** * Formatter for unified search responses */ export class UnifiedSearchFormatter implements ResponseFormatter<UnifiedSearchResponse> { // The input 'responseData' should match the UnifiedSearchResponse type structure. format(responseData: UnifiedSearchResponse): string { // Destructure the 'results' property as defined in UnifiedSearchResponse const { results, total, page, limit, totalPages } = responseData; // Create a summary section with pagination info const summary = `Search Results\n\n` + `Found ${total ?? 0} result(s)\n` + // Use nullish coalescing for safety `Page ${page ?? 1} of ${totalPages ?? 1} (${limit ?? 0} per page)\n`; // Use nullish coalescing // Add a robust check for results being a valid array before accessing length if (!Array.isArray(results) || results.length === 0) { return `${summary}\nNo matches found for the specified search criteria.`; } // Group results by entity type for better organization const groupedResults: Record<string, SearchResultItem[]> = {}; results.forEach((result: SearchResultItem) => { // Add explicit type here if (!groupedResults[result.type]) { groupedResults[result.type] = []; } groupedResults[result.type].push(result); }); // Build formatted output for each entity type group let resultsOutput = ''; Object.entries(groupedResults).forEach(([type, items]) => { // Add section heading for this entity type resultsOutput += `\n${this.capitalizeFirstLetter(type)} Results (${items.length})\n\n`; // Format each result item items.forEach((item, index) => { const score = Math.round(item.score * 10) / 10; // Round to 1 decimal place const relevanceIndicator = this.getRelevanceIndicator(score); resultsOutput += `${index + 1}. ${relevanceIndicator} ${item.title}\n`; // Add relevant metadata based on entity type if (item.type === 'project') { resultsOutput += ` ID: ${item.id}\n`; resultsOutput += ` Type: ${item.entityType}\n`; resultsOutput += ` Match: Found in ${item.matchedProperty}\n`; } else if (item.type === 'task') { resultsOutput += ` ID: ${item.id}\n`; resultsOutput += ` Project: ${item.projectName || 'Unknown'}\n`; resultsOutput += ` Type: ${item.entityType}\n`; resultsOutput += ` Match: Found in ${item.matchedProperty}\n`; } else if (item.type === 'knowledge') { resultsOutput += ` ID: ${item.id}\n`; resultsOutput += ` Project: ${item.projectName || 'Unknown'}\n`; resultsOutput += ` Domain: ${item.entityType}\n`; resultsOutput += ` Match: Found in ${item.matchedProperty}\n`; } // Add a snippet of the matched content if (item.matchedValue) { const matchSnippet = this.truncateText(item.matchedValue, 100); resultsOutput += ` Content: "${matchSnippet}"\n`; } // Conditionally add created date if available if (item.createdAt) { resultsOutput += ` Created: ${new Date(item.createdAt).toLocaleString()}\n`; } // Add a blank line after each item resultsOutput += `\n`; }); }); // Add help text for pagination let paginationHelp = ""; if (totalPages > 1) { paginationHelp = `\nTo view more results, use 'page' parameter (current: ${page}, total pages: ${totalPages}).`; } return `${summary}${resultsOutput}${paginationHelp}`; } /** * Capitalize the first letter of a string */ private capitalizeFirstLetter(str: string): string { return str.charAt(0).toUpperCase() + str.slice(1); } /** * Get a visual indicator for the relevance score */ private getRelevanceIndicator(score: number): string { if (score >= 8) return '🔍 [Highly Relevant]'; if (score >= 6) return '🔍 [Relevant]'; if (score >= 4) return '🔍 [Moderately Relevant]'; return '🔍 [Potentially Relevant]'; } /** * Truncate text to a specified length with ellipsis */ private truncateText(text: string | null | undefined, maxLength: number): string { // Add check to ensure text is a string and handle null/undefined if (typeof text !== 'string' || text.length === 0) { return ''; // Return empty string if text is not valid } if (text.length <= maxLength) { return text; } return text.substring(0, maxLength - 3) + '...'; } } /** * Create a formatted, human-readable response for the atlas_unified_search tool * * @param data The search response data * @param isError Whether this response represents an error condition * @returns Formatted MCP tool response with appropriate structure */ export function formatUnifiedSearchResponse(data: UnifiedSearchResponse, isError = false): any { return createFormattedResponse(data, new UnifiedSearchFormatter(), isError); }