Graph Unmerge
graph_unmergeSplit a falsely merged entity back into two separate entities by moving specified edges to a new entity. Use when entity resolution incorrectly merged distinct entities.
Instructions
Split a falsely merged entity back into two separate entities, redistributing specified edges. Use when entity resolution made a mistake (e.g. merged 'Anna' and 'Anne'). The original entity keeps every edge not listed in edges_to_move; the new entity gets the listed edges plus a fresh embedding stub (re-derive with graph_reembed). Logged to the audit trail with reason. Returns the IDs of both entities.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| entity_id | Yes | The merged entity ID to split | |
| new_entity_name | Yes | Name for the split-off entity | |
| new_entity_type | Yes | Type label for the split-off entity | |
| edges_to_move | Yes | Edges to move to the new entity | |
| reason | Yes | Why splitting (logged in audit) |
Implementation Reference
- src/mcp-server/index.ts:605-655 (registration)Registration of the graph_unmerge tool on the MCP server, with input schema and handler definition. Calls client.unmerge() to split a falsely merged entity.
// ─── Tool: graph_unmerge ─── server.registerTool("graph_unmerge", { title: "Graph Unmerge", description: "Split a falsely merged entity back into two separate entities, redistributing specified edges. Use when entity resolution made a mistake (e.g. merged 'Anna' and 'Anne'). The original entity keeps every edge not listed in `edges_to_move`; the new entity gets the listed edges plus a fresh embedding stub (re-derive with graph_reembed). Logged to the audit trail with `reason`. Returns the IDs of both entities.", inputSchema: { entity_id: z.string().describe("The merged entity ID to split"), new_entity_name: z.string().describe("Name for the split-off entity"), new_entity_type: z.string().describe("Type label for the split-off entity"), edges_to_move: z.array(z.object({ other_entity_id: z.string().describe("Entity on the other end of the edge"), relation_type: z.string().describe("Relationship type (e.g. WORKS_ON)"), direction: z.enum(["in", "out"]).describe("Direction relative to the entity being split"), })).describe("Edges to move to the new entity"), reason: z.string().describe("Why splitting (logged in audit)"), }, annotations: { destructiveHint: true }, }, async (args) => { try { const result = await client.unmerge( currentTenant(), args.entity_id, args.new_entity_name, args.new_entity_type as EntityType, args.edges_to_move.map((e) => ({ ...e, relation_type: e.relation_type as RelationshipType, })), args.reason, ); // Log to merge audit try { const auditDir = join(GRAPH_MEMORY_HOME, "logs"); mkdirSync(auditDir, { recursive: true }); const auditPath = join(auditDir, "merge-audit.jsonl"); const entry = JSON.stringify({ action: "unmerge", timestamp: new Date().toISOString(), ...result, reason: args.reason, }); writeFileSync(auditPath, entry + "\n", { flag: "a" }); } catch { /* audit logging is best-effort */ } return toolResult({ ...result, audit_logged: true }); } catch (err) { return toolError(`graph_unmerge failed: ${err instanceof Error ? err.message : String(err)}`); } }); - src/mcp-server/index.ts:611-621 (schema)Input schema for graph_unmerge: entity_id, new_entity_name, new_entity_type, edges_to_move (array of {other_entity_id, relation_type, direction}), and reason.
inputSchema: { entity_id: z.string().describe("The merged entity ID to split"), new_entity_name: z.string().describe("Name for the split-off entity"), new_entity_type: z.string().describe("Type label for the split-off entity"), edges_to_move: z.array(z.object({ other_entity_id: z.string().describe("Entity on the other end of the edge"), relation_type: z.string().describe("Relationship type (e.g. WORKS_ON)"), direction: z.enum(["in", "out"]).describe("Direction relative to the entity being split"), })).describe("Edges to move to the new entity"), reason: z.string().describe("Why splitting (logged in audit)"), }, - src/mcp-server/index.ts:623-655 (handler)Handler logic for graph_unmerge: calls client.unmerge() with tenant context, then logs to merge-audit.jsonl audit file. Returns both entity IDs.
}, async (args) => { try { const result = await client.unmerge( currentTenant(), args.entity_id, args.new_entity_name, args.new_entity_type as EntityType, args.edges_to_move.map((e) => ({ ...e, relation_type: e.relation_type as RelationshipType, })), args.reason, ); // Log to merge audit try { const auditDir = join(GRAPH_MEMORY_HOME, "logs"); mkdirSync(auditDir, { recursive: true }); const auditPath = join(auditDir, "merge-audit.jsonl"); const entry = JSON.stringify({ action: "unmerge", timestamp: new Date().toISOString(), ...result, reason: args.reason, }); writeFileSync(auditPath, entry + "\n", { flag: "a" }); } catch { /* audit logging is best-effort */ } return toolResult({ ...result, audit_logged: true }); } catch (err) { return toolError(`graph_unmerge failed: ${err instanceof Error ? err.message : String(err)}`); } });