search_memories
Search and retrieve information from a Neo4j knowledge graph using text queries, filters, and relationship depth to find stored memories.
Instructions
Search and retrieve memories from the knowledge graph
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| query | No | Search text to find in any property (searches for ANY word - e.g. "Ben Weeks" finds memories containing "Ben" OR "Weeks") | |
| label | No | Filter by memory label | |
| depth | No | Relationship depth to include, defaults to 1 | |
| order_by | No | Sort order such as created_at DESC, name ASC | |
| limit | No | Maximum results to return, defaults to 10, max 200 | |
| since_date | No | ISO date string to filter memories created after this date (e.g., "2024-01-01" or "2024-01-01T00:00:00Z") |
Implementation Reference
- src/handlers/index.ts:22-188 (handler)Core handler logic for the 'search_memories' tool. Validates arguments, implements flexible full-text search across all memory properties using JavaScript (word-based matching in arrays/objects), builds Neo4j queries for filtering by label/date, collects matching memory IDs, then fetches detailed results with optional connected nodes up to specified depth. Handles fallbacks and limits results.case 'search_memories': { if (!isSearchMemoriesArgs(args)) { throw new McpError(ErrorCode.InvalidParams, 'Invalid search_memories arguments'); } const depth = args.depth ?? 1; const limit = Math.min(args.limit ?? 10, 200); let orderBy = 'memory.created_at DESC'; if (args.order_by) { const orderMatch = args.order_by.match(/^(memory\.|n\.)?([a-zA-Z_]+)\s+(ASC|DESC)$/i); if (orderMatch) { const property = orderMatch[2]; const direction = orderMatch[3].toUpperCase(); orderBy = `memory.${property} ${direction}`; } } const params: Record<string, any> = {}; if (args.query) params.query = args.query; if (args.label) params.label = args.label; if (args.since_date) params.since_date = args.since_date; // Try a different approach: execute simple queries and handle complexity in JavaScript const allMemoryIds = new Set<number>(); try { // First, get all memories that match the label and date filters (if any) let baseQuery = `MATCH (memory)`; const conditions = []; if (args.label) { conditions.push(`labels(memory)[0] = $label`); } if (args.since_date) { conditions.push(`memory.created_at >= $since_date`); } if (conditions.length > 0) { baseQuery += ` WHERE ` + conditions.join(' AND '); } baseQuery += ` RETURN id(memory) as id, properties(memory) as props`; const queryParams: Record<string, any> = {}; if (args.label) queryParams.label = args.label; if (args.since_date) queryParams.since_date = args.since_date; const allMemories = await neo4j.executeQuery(baseQuery, queryParams); // Filter in JavaScript for (const record of allMemories) { const props = record.props; // If query is empty, include all memories (they already match label/date filters) if (!args.query || args.query.trim() === '') { allMemoryIds.add(record.id); continue; } // Split query into words for more flexible matching const queryWords = args.query.toLowerCase().trim().split(/\s+/); let found = false; // Search through all properties for (const [key, value] of Object.entries(props)) { if (value === null || value === undefined) continue; const valueStr = Array.isArray(value) ? value.map(v => v?.toString() || '').join(' ').toLowerCase() : value.toString().toLowerCase(); // Check if ANY query word is found in this property for (const word of queryWords) { if (valueStr.includes(word)) { found = true; break; } } if (found) { allMemoryIds.add(record.id); break; } } } } catch (error) { console.error('Error in search:', error); // Fallback to a simpler query if the above fails const conditions = []; if (args.query && args.query.trim() !== '') { conditions.push('(memory.name CONTAINS $query OR memory.context CONTAINS $query OR memory.description CONTAINS $query)'); } if (args.label) { conditions.push('labels(memory)[0] = $label'); } if (args.since_date) { conditions.push('memory.created_at >= $since_date'); } let fallbackQuery = `MATCH (memory)`; if (conditions.length > 0) { fallbackQuery += ` WHERE ${conditions.join(' AND ')}`; } fallbackQuery += ` RETURN collect(DISTINCT id(memory)) as memoryIds`; const fallbackParams: Record<string, any> = {}; if (args.query) fallbackParams.query = args.query; if (args.label) fallbackParams.label = args.label; if (args.since_date) fallbackParams.since_date = args.since_date; const fallbackResults = await neo4j.executeQuery(fallbackQuery, fallbackParams); if (fallbackResults.length > 0 && fallbackResults[0].memoryIds) { fallbackResults[0].memoryIds.forEach((id: number) => allMemoryIds.add(id)); } } if (allMemoryIds.size === 0) { return { content: [ { type: 'text', text: JSON.stringify([], null, 2), }, ], }; } // Query 3: Fetch the actual memories with connections let finalQuery = ` MATCH (memory) WHERE id(memory) IN $memoryIds `; if (depth > 0) { finalQuery += ` OPTIONAL MATCH path = (memory)-[*1..${depth}]-(related) RETURN memory, collect(DISTINCT { memory: related, relationship: relationships(path)[0], distance: length(path) }) as connections ORDER BY ${orderBy} LIMIT ${limit} `; } else { finalQuery += ` RETURN memory, [] as connections ORDER BY ${orderBy} LIMIT ${limit}`; } const result = await neo4j.executeQuery(finalQuery, { memoryIds: Array.from(allMemoryIds) }); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; }
- src/tools/definitions.ts:11-44 (schema)MCP protocol tool schema definition for 'search_memories', exported in the tools array. Defines input parameters with descriptions used for tool discovery and validation by MCP clients.{ name: 'search_memories', description: 'Search and retrieve memories from the knowledge graph', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search text to find in any property (searches for ANY word - e.g. "Ben Weeks" finds memories containing "Ben" OR "Weeks")', }, label: { type: 'string', description: 'Filter by memory label', }, depth: { type: 'number', description: 'Relationship depth to include, defaults to 1', }, order_by: { type: 'string', description: 'Sort order such as created_at DESC, name ASC', }, limit: { type: 'number', description: 'Maximum results to return, defaults to 10, max 200', }, since_date: { type: 'string', description: 'ISO date string to filter memories created after this date (e.g., "2024-01-01" or "2024-01-01T00:00:00Z")', }, }, required: [], }, },
- src/types.ts:13-65 (schema)Internal TypeScript interface and type guard validator for SearchMemoriesArgs, imported and used in the handler for runtime argument validation.export interface SearchMemoriesArgs { query?: string; label?: string; depth?: number; order_by?: string; limit?: number; since_date?: string; } export interface CreateConnectionArgs { fromMemoryId: number; toMemoryId: number; type: string; properties?: Record<string, any>; } export interface UpdateMemoryArgs { nodeId: number; properties: Record<string, any>; } export interface UpdateConnectionArgs { fromMemoryId: number; toMemoryId: number; type: string; properties: Record<string, any>; } export interface DeleteMemoryArgs { nodeId: number; } export interface DeleteConnectionArgs { fromMemoryId: number; toMemoryId: number; type: string; } export interface ListMemoryLabelsArgs { // No arguments needed for this tool } export function isCreateMemoryArgs(args: unknown): args is CreateMemoryArgs { return typeof args === 'object' && args !== null && typeof (args as CreateMemoryArgs).label === 'string' && typeof (args as CreateMemoryArgs).properties === 'object'; } export function isSearchMemoriesArgs(args: unknown): args is SearchMemoriesArgs { if (typeof args !== 'object' || args === null) return false; const searchArgs = args as SearchMemoriesArgs; if (searchArgs.query !== undefined && typeof searchArgs.query !== 'string') return false; if (searchArgs.since_date !== undefined && typeof searchArgs.since_date !== 'string') return false; return true; }
- src/server.ts:39-41 (registration)Registration of all tools (including search_memories) via the imported tools array in the MCP server's listTools request handler.this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools, }));