Graph Entities
graph_entitiesSearch or browse the entity catalog to check existence before creating, or list entities by type. Returns sorted results up to a specified limit.
Instructions
Browse or search the entity catalog. Use to check if an entity exists before creating one with graph_relate, or to list entities of a given type. For relationship-aware lookups (entity + its neighbors) use graph_query instead. Returns up to limit entities ordered by sort_by; pagination is single-page (raise limit if you need more).
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| search | No | Full-text search query | |
| type | No | Filter by entity type (Person, Project, Concept, etc.) | |
| min_confidence | No | Min confidence threshold | |
| sort_by | No | Sort order (default: confidence) | confidence |
| limit | No | Max results (default: 20) |
Implementation Reference
- src/mcp-server/index.ts:402-427 (registration)Registration of the graph_entities tool on the MCP server with input schema and handler. Delegates to client.searchEntities().
server.registerTool("graph_entities", { title: "Graph Entities", description: "Browse or search the entity catalog. Use to check if an entity exists before creating one with graph_relate, or to list entities of a given type. For relationship-aware lookups (entity + its neighbors) use graph_query instead. Returns up to `limit` entities ordered by `sort_by`; pagination is single-page (raise `limit` if you need more).", inputSchema: { search: z.string().optional().describe("Full-text search query"), type: z.string().optional().describe("Filter by entity type (Person, Project, Concept, etc.)"), min_confidence: z.number().optional().describe("Min confidence threshold"), sort_by: z.enum(["confidence", "last_seen", "name"]).optional().default("confidence").describe("Sort order (default: confidence)"), limit: z.number().optional().default(20).describe("Max results (default: 20)"), }, annotations: { readOnlyHint: true }, }, async (args) => { try { const result = await client.searchEntities(currentTenant(), { search: args.search, type: args.type as EntityType | undefined, min_confidence: args.min_confidence, sort_by: args.sort_by, limit: args.limit, }); return toolResult(result); } catch (err) { return toolError(`graph_entities failed: ${err instanceof Error ? err.message : String(err)}`); } }); - src/mcp-server/index.ts:406-412 (schema)Input schema for graph_entities: search, type, min_confidence, sort_by, limit.
inputSchema: { search: z.string().optional().describe("Full-text search query"), type: z.string().optional().describe("Filter by entity type (Person, Project, Concept, etc.)"), min_confidence: z.number().optional().describe("Min confidence threshold"), sort_by: z.enum(["confidence", "last_seen", "name"]).optional().default("confidence").describe("Sort order (default: confidence)"), limit: z.number().optional().default(20).describe("Max results (default: 20)"), }, - src/mcp-server/index.ts:414-427 (handler)Handler function for graph_entities - delegates to client.searchEntities() with the provided filters.
}, async (args) => { try { const result = await client.searchEntities(currentTenant(), { search: args.search, type: args.type as EntityType | undefined, min_confidence: args.min_confidence, sort_by: args.sort_by, limit: args.limit, }); return toolResult(result); } catch (err) { return toolError(`graph_entities failed: ${err instanceof Error ? err.message : String(err)}`); } }); - src/shared/neo4j-client.ts:742-820 (helper)The core implementation in Neo4jClient.searchEntities() that executes Cypher queries to search/filter/sort entities by full-text search or direct filtering, with edge count and total.
async searchEntities( tenantId: string, options: { search?: string; type?: EntityType; min_confidence?: number; sort_by?: "confidence" | "last_seen" | "name"; limit?: number; } = {}, ): Promise<{ entities: EntityNode[]; total: number }> { const limit = options.limit ?? 20; let cypher: string; let params: Record<string, unknown>; if (options.search) { const typeFilter = options.type ? `AND ANY(l IN labels(node) WHERE l = $type)` : ""; const confFilter = options.min_confidence != null ? `AND node.confidence >= $minConf` : ""; cypher = ` CALL db.index.fulltext.queryNodes('entity_names', $search) YIELD node, score WHERE node:Entity AND node.tenant_id = $tenantId ${typeFilter} ${confFilter} WITH node, score OPTIONAL MATCH (node)-[r]-(other:Entity {tenant_id: $tenantId}) WITH node, labels(node) AS labels, count(r) AS edge_count, score RETURN node, labels, edge_count ORDER BY score DESC LIMIT $limit `; params = { tenantId, search: options.search, limit, ...(options.type ? { type: options.type } : {}), ...(options.min_confidence != null ? { minConf: options.min_confidence } : {}), }; } else { const typeMatch = options.type ? `(n:\`${options.type}\` {tenant_id: $tenantId})` : "(n:Entity {tenant_id: $tenantId})"; const confFilter = options.min_confidence != null ? `WHERE n.confidence >= $minConf` : ""; const orderBy = options.sort_by === "last_seen" ? "n.last_seen DESC" : options.sort_by === "name" ? "n.name ASC" : "n.confidence DESC"; cypher = ` MATCH ${typeMatch} ${confFilter} OPTIONAL MATCH (n)-[r]-(other:Entity {tenant_id: $tenantId}) WITH n, labels(n) AS labels, count(r) AS edge_count RETURN n AS node, labels, edge_count ORDER BY ${orderBy} LIMIT $limit `; params = { tenantId, limit, ...(options.min_confidence != null ? { minConf: options.min_confidence } : {}), }; } const rows = await this.run(cypher, params); const entities = rows.map((row) => { const nodeObj = row["node"] as { labels: string[]; properties: Record<string, unknown> }; const entity = recordToEntity(nodeObj.properties, nodeObj.labels); (entity as EntityNode & { edge_count?: number }).edge_count = Number(row["edge_count"] ?? 0); return entity; }); // Tenant-scoped total count const countCypher = options.type ? `MATCH (n:\`${options.type}\` {tenant_id: $tenantId}) RETURN count(n) AS total` : `MATCH (n:Entity {tenant_id: $tenantId}) RETURN count(n) AS total`; const countRows = await this.run(countCypher, { tenantId }); const totalNum = Number(countRows[0]?.["total"] ?? 0); return { entities, total: totalNum }; }