Skip to main content
Glama
knowall-ai

Neo4j Agent Memory MCP Server

by knowall-ai

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
NameRequiredDescriptionDefault
queryNoSearch text to find in any property (searches for ANY word - e.g. "Ben Weeks" finds memories containing "Ben" OR "Weeks")
labelNoFilter by memory label
depthNoRelationship depth to include, defaults to 1
order_byNoSort order such as created_at DESC, name ASC
limitNoMaximum results to return, defaults to 10, max 200
since_dateNoISO date string to filter memories created after this date (e.g., "2024-01-01" or "2024-01-01T00:00:00Z")

Implementation Reference

  • 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),
          },
        ],
      };
    }
  • 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: [],
      },
    },
  • 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,
    }));
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the full burden of behavioral disclosure. It mentions 'search and retrieve' which implies a read-only operation, but doesn't specify permissions needed, rate limits, pagination behavior, error conditions, or what 'retrieve' entails (e.g., full memory details vs summaries). For a search tool with 6 parameters and no annotations, this leaves significant behavioral gaps.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, efficient sentence that front-loads the core purpose ('Search and retrieve memories from the knowledge graph') with zero wasted words. It's appropriately sized for a tool with a clear primary function, making it easy to parse quickly.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given 6 parameters, no annotations, and no output schema, the description is incomplete. It doesn't explain what 'retrieve' returns (e.g., memory objects, IDs, or summaries), how results are structured, or any behavioral aspects like error handling. For a search tool with moderate complexity, this leaves too much unspecified for reliable agent use.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, with each parameter well-documented in the schema (e.g., query searches for ANY word, depth defaults to 1, limit max 200). The description adds no additional parameter information beyond the schema, so it doesn't enhance understanding. Baseline 3 is appropriate when the schema does all the work.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the action ('Search and retrieve') and resource ('memories from the knowledge graph'), making the purpose immediately understandable. However, it doesn't distinguish this tool from potential sibling search operations (none listed in siblings, but 'list_memory_labels' is a related read operation). The description is specific but lacks sibling differentiation.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides no guidance on when to use this tool versus alternatives. With siblings like 'list_memory_labels' (which might list all labels without search) and other CRUD operations, there's no indication of when search is preferred over listing or when this tool should be avoided. No context or exclusions are mentioned.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

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/knowall-ai/mcp-neo4j-agent-memory'

If you have feedback or need assistance with the MCP directory API, please join our Discord server