Skip to main content
Glama

Graph Query

graph_query
Read-only

Retrieve memory graph nodes and edges for given entity names. Returns connected subgraph with edge weights and source provenance.

Instructions

Query the memory graph by canonical entity name. Use when you know the entity name or close-to-canonical form (e.g. "Steve", "graph-memory"); for natural-language phrasing or synonyms (e.g. "the knowledge graph project") prefer graph_search. Returns up to limit matching nodes plus the edges that connect them within max_hops, with per-edge weight and source provenance.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
entitiesYesEntity names to search for
entity_typesNoFilter results to these entity types
max_hopsNoMax traversal depth (default: 2)
min_weightNoMin edge weight to traverse (default: 0.3)
limitNoMax results (default: 20)
project_contextNoProject directory or name for affinity scoring
context_levelNoResponse detail level (default: full)full
current_onlyNoOnly current facts, exclude superseded (default: true)

Implementation Reference

  • Registration of the graph_query tool with the MCP server, including inputSchema (Zod-based) definition and the async handler that delegates to client.query().
    server.registerTool("graph_query", {
      title: "Graph Query",
      description:
        "Query the memory graph by canonical entity name. Use when you know the entity name or close-to-canonical form (e.g. \"Steve\", \"graph-memory\"); for natural-language phrasing or synonyms (e.g. \"the knowledge graph project\") prefer graph_search. Returns up to `limit` matching nodes plus the edges that connect them within `max_hops`, with per-edge weight and source provenance.",
      inputSchema: {
        entities: z.array(z.string()).describe("Entity names to search for"),
        entity_types: z.array(z.string()).optional().describe("Filter results to these entity types"),
        max_hops: z.number().optional().default(2).describe("Max traversal depth (default: 2)"),
        min_weight: z.number().optional().default(0.3).describe("Min edge weight to traverse (default: 0.3)"),
        limit: z.number().optional().default(20).describe("Max results (default: 20)"),
        project_context: z.string().optional().describe("Project directory or name for affinity scoring"),
        context_level: z.enum(["minimal", "full", "relations-only"]).optional().default("full").describe("Response detail level (default: full)"),
        current_only: z.boolean().optional().default(true).describe("Only current facts, exclude superseded (default: true)"),
      },
      annotations: { readOnlyHint: true },
    }, async (args) => {
      try {
        const result = await client.query(currentTenant(), args.entities, {
          entity_types: args.entity_types as EntityType[] | undefined,
          max_hops: args.max_hops,
          min_weight: args.min_weight,
          limit: args.limit,
          project_context: args.project_context,
          current_only: args.current_only,
        });
    
        if (args.context_level === "minimal") {
          return toolResult({
            nodes: result.nodes.map((n) => ({
              id: n.id, type: n.type, name: n.name, confidence: n.confidence,
            })),
            edge_count: result.edges.length,
          });
        }
    
        if (args.context_level === "relations-only") {
          return toolResult({
            nodes: result.nodes.map((n) => ({
              id: n.id, type: n.type, name: n.name,
            })),
            edges: result.edges,
          });
        }
    
        return toolResult(result);
      } catch (err) {
        return toolError(`graph_query failed: ${err instanceof Error ? err.message : String(err)}`);
      }
    });
  • Input schema for graph_query tool: entities (required array), entity_types, max_hops, min_weight, limit, project_context, context_level, current_only — all with Zod validation and defaults.
    inputSchema: {
      entities: z.array(z.string()).describe("Entity names to search for"),
      entity_types: z.array(z.string()).optional().describe("Filter results to these entity types"),
      max_hops: z.number().optional().default(2).describe("Max traversal depth (default: 2)"),
      min_weight: z.number().optional().default(0.3).describe("Min edge weight to traverse (default: 0.3)"),
      limit: z.number().optional().default(20).describe("Max results (default: 20)"),
      project_context: z.string().optional().describe("Project directory or name for affinity scoring"),
      context_level: z.enum(["minimal", "full", "relations-only"]).optional().default("full").describe("Response detail level (default: full)"),
      current_only: z.boolean().optional().default(true).describe("Only current facts, exclude superseded (default: true)"),
    },
    annotations: { readOnlyHint: true },
  • Handler function for graph_query — calls client.query() with tenant ID and args, then formats response based on context_level (minimal, relations-only, or full).
    }, async (args) => {
      try {
        const result = await client.query(currentTenant(), args.entities, {
          entity_types: args.entity_types as EntityType[] | undefined,
          max_hops: args.max_hops,
          min_weight: args.min_weight,
          limit: args.limit,
          project_context: args.project_context,
          current_only: args.current_only,
        });
    
        if (args.context_level === "minimal") {
          return toolResult({
            nodes: result.nodes.map((n) => ({
              id: n.id, type: n.type, name: n.name, confidence: n.confidence,
            })),
            edge_count: result.edges.length,
          });
        }
    
        if (args.context_level === "relations-only") {
          return toolResult({
            nodes: result.nodes.map((n) => ({
              id: n.id, type: n.type, name: n.name,
            })),
            edges: result.edges,
          });
        }
    
        return toolResult(result);
      } catch (err) {
        return toolError(`graph_query failed: ${err instanceof Error ? err.message : String(err)}`);
      }
    });
  • Neo4jClient.query() — the core traversal logic: resolves entity names to IDs via fulltext search, then traverses the graph up to max_hops, applying tenant isolation, weight/type filters, current_only validity filter, and optional project-context affinity scoring.
    async query(
      tenantId: string,
      entities: string[],
      options: {
        entity_types?: EntityType[];
        max_hops?: number;
        min_weight?: number;
        limit?: number;
        project_context?: string;
        current_only?: boolean;
      } = {},
    ): Promise<QueryResult> {
      const config = getConfig();
      const maxHops = options.max_hops ?? config.query.default_max_hops;
      const minWeight = options.min_weight ?? config.query.default_min_weight;
      const limit = options.limit ?? config.query.default_limit;
      const currentOnly = options.current_only ?? true;
    
      // First resolve entity names to IDs via full-text search, scoped to tenant.
      // The fulltext index is global, so we filter results to the tenant's nodes.
      const entityIds: string[] = [];
      for (const name of entities) {
        const searchRows = await this.run(
          `
          CALL db.index.fulltext.queryNodes('entity_names', $name)
          YIELD node, score
          WHERE node.tenant_id = $tenantId
          RETURN node.id AS id, score
          ORDER BY score DESC LIMIT 1
          `,
          { tenantId, name },
        );
        if (searchRows.length > 0) {
          entityIds.push(String(searchRows[0]["id"]));
        }
      }
    
      if (entityIds.length === 0) {
        return { nodes: [], edges: [], source_files: [] };
      }
    
      // Traverse from resolved entities — restrict the entire path to nodes in
      // this tenant so we cannot cross into another tenant's subgraph even via
      // a shared edge id collision.
      const validityFilter = currentOnly ? "AND rel.invalid_at IS NULL" : "";
      const typeFilter =
        options.entity_types && options.entity_types.length > 0
          ? `AND ANY(label IN labels(m) WHERE label IN $entityTypes)`
          : "";
    
      const rows = await this.run(
        `
        UNWIND $entityIds AS startId
        MATCH (start:Entity {tenant_id: $tenantId, id: startId})
        // *0..N includes the seed itself (zero-length path). Without this the
        // entity you actually asked about never appears in its own query result.
        MATCH path = (start)-[*0..${maxHops}]-(m:Entity)
        WHERE m.tenant_id = $tenantId
          AND ALL(node IN nodes(path) WHERE node.tenant_id = $tenantId)
          AND ALL(rel IN relationships(path) WHERE rel.weight >= $minWeight ${validityFilter})
          ${typeFilter}
        WITH DISTINCT m, relationships(path) AS rels, start
        RETURN m, labels(m) AS labels,
               [rel IN rels | {
                 props: properties(rel),
                 type: type(rel),
                 fromId: startNode(rel).id,
                 toId: endNode(rel).id
               }] AS edgeData
        LIMIT $limit
        `,
        {
          tenantId,
          entityIds,
          minWeight,
          limit,
          ...(options.entity_types ? { entityTypes: options.entity_types } : {}),
        },
      );
    
      const nodeMap = new Map<string, EntityNode>();
      const edgeMap = new Map<string, RelationshipEdge>();
      const sourceFiles = new Set<string>();
    
      for (const row of rows) {
        const nodeObj = row["m"] as { labels: string[]; properties: Record<string, unknown> };
        const node = recordToEntity(nodeObj.properties, nodeObj.labels);
        nodeMap.set(node.id, node);
        if (node.source_file) sourceFiles.add(node.source_file);
    
        const edgeData = row["edgeData"] as Array<{
          props: Record<string, unknown>;
          type: string;
          fromId: string;
          toId: string;
        }>;
        for (const ed of edgeData) {
          const key = `${ed.fromId}-${ed.type}-${ed.toId}`;
          if (!edgeMap.has(key)) {
            const edge = recordToEdge(ed.props, ed.type, ed.fromId, ed.toId);
            if (options.project_context) {
              edge.effective_weight = edge.weight;
            }
            edgeMap.set(key, edge);
          }
        }
      }
    
      // Apply project-context affinity (scoped to tenant)
      if (options.project_context) {
        const affinityRows = await this.run(
          `
          MATCH (proj:Project {tenant_id: $tenantId})
          WHERE proj.directory CONTAINS $projectContext OR proj.name = $projectContext
          MATCH (proj)-[*1..2]-(related:Entity)
          WHERE related.tenant_id = $tenantId
          RETURN DISTINCT related.id AS id
          `,
          { tenantId, projectContext: options.project_context },
        );
        const projectRelatedIds = new Set(affinityRows.map((r) => String(r["id"])));
    
        for (const edge of edgeMap.values()) {
          const fromRelated = projectRelatedIds.has(edge.from);
          const toRelated = projectRelatedIds.has(edge.to);
          if (fromRelated || toRelated) {
            edge.effective_weight = edge.weight * config.affinity.hop_1_multiplier;
          } else {
            edge.effective_weight = edge.weight;
          }
        }
      }
    
      return {
        nodes: Array.from(nodeMap.values()),
        edges: Array.from(edgeMap.values()).sort(
          (a, b) => (b.effective_weight ?? b.weight) - (a.effective_weight ?? a.weight),
        ),
        source_files: Array.from(sourceFiles),
      };
    }
Behavior4/5

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

Adds detail on return format (nodes and edges with weight and provenance) beyond the readOnlyHint annotation. However, does not discuss pagination or other potential behaviors.

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?

Two concise sentences with no wasted words; first sentence covers purpose and usage, second covers output behavior.

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

Completeness4/5

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

For a query tool with 8 parameters and no output schema, the description adequately covers core behavior. Some parameters lack elaboration but schema descriptions suffice. Marginal improvement possible.

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 coverage is 100%, so baseline is 3. Description adds context for limit and max_hops parameters but does not significantly enhance understanding of other parameters beyond their schema descriptions.

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

Purpose5/5

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

Clearly states 'Query the memory graph by canonical entity name', specifying the verb and resource, and distinguishes from sibling graph_search.

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

Usage Guidelines5/5

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

Explicitly advises when to use this tool (known entity name) and when to prefer graph_search (natural-language phrasing or synonyms), providing clear guidance.

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/stevepridemore/graph-memory'

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