Skip to content

fix(mcp): delete_episode cascade to extracted edges and orphan entities#1491

Open
brentkearney wants to merge 1 commit into
getzep:mainfrom
brentkearney:fix/mcp-delete-episode-cascade
Open

fix(mcp): delete_episode cascade to extracted edges and orphan entities#1491
brentkearney wants to merge 1 commit into
getzep:mainfrom
brentkearney:fix/mcp-delete-episode-cascade

Conversation

@brentkearney
Copy link
Copy Markdown

@brentkearney brentkearney commented May 15, 2026

Resolves part of #1489 (Gap 2).

Summary

The MCP delete_episode tool currently calls EpisodicNode.delete(client.driver), which drops only the Episodic node. The RELATES_TO edges extracted from the episode remain in the graph with stale episodes arrays pointing at the now-deleted UUID, and Entity nodes that were mentioned only by the deleted episode are left behind as orphans.

graphiti_core.Graphiti.remove_episode already exists and does the right thing — it deletes the episode, the edges where this episode is the first provenance entry, and any entity nodes mentioned only by the deleted episode. This PR wires the MCP tool through to it.

Why this matters

Without the cascade, delete_episode corrupts the graph silently:

  • Dangling edges accumulate, with episodes arrays referencing nonexistent UUIDs.
  • Orphan entity nodes accumulate.
  • Any "make a bad ingest, delete it, retry" workflow (common during backfill, migration, or correcting LLM errors) leaves residue behind that's hard to detect and harder to clean up after the fact.

Changes

Single-file, three-line behavioral change:

--- a/mcp_server/src/graphiti_mcp_server.py
+++ b/mcp_server/src/graphiti_mcp_server.py
@@ -14,7 +14,7 @@ from typing import Any, Optional
 from dotenv import load_dotenv
 from graphiti_core import Graphiti
 from graphiti_core.edges import EntityEdge
-from graphiti_core.nodes import EpisodeType, EpisodicNode
+from graphiti_core.nodes import EpisodeType
 from graphiti_core.search.search_filters import SearchFilters
 from graphiti_core.utils.maintenance.graph_data_operations import clear_data
 from mcp.server.fastmcp import FastMCP
@@ -579,10 +579,8 @@ async def delete_episode(uuid: str) -> SuccessResponse | ErrorResponse:
     try:
         client = await graphiti_service.get_client()

-        # Get the episodic node by UUID
-        episodic_node = await EpisodicNode.get_by_uuid(client.driver, uuid)
-        # Delete the node using its delete method
-        await episodic_node.delete(client.driver)
+        # Cascades to edges sourced from this episode and any orphaned entities.
+        await client.remove_episode(uuid)
         return SuccessResponse(message=f'Episode with UUID {uuid} deleted successfully')
     except Exception as e:
         error_msg = str(e)

The top-level EpisodicNode import becomes unused after the swap; the in-function from graphiti_core.nodes import EpisodicNode at the get_episodes site (around line 647) is preserved and remains the only EpisodicNode usage in the file.

Design notes

Behavior change vs. the previous tool: From a caller's perspective, the tool still takes a UUID and still returns SuccessResponse(message=...). The difference is observable only in the graph: edges and orphan entities are now removed alongside the episode, rather than being left dangling.

No new arguments, no new failure modes. Graphiti.remove_episode itself looks up the episode by UUID (same as before) and raises on a missing UUID just like EpisodicNode.get_by_uuid did — the surrounding try/except still catches that and returns ErrorResponse.

Why this isn't an opt-in flag. Leaving stale edges in the graph is unlikely to be the desired behavior for delete_episode. That said, this is a behavior change. If maintainers prefer to preserve existing behavior, an opt-out flag (cascade: bool = True) would preserve the option to fall back to the old behavior — happy to add it on request.

Verification

  • ruff check + ruff format --check clean.
  • pyright clean (0 errors, 0 warnings, 0 informations).
  • Manually verified on a self-hosted deployment: calling the tool on a known episode UUID removes the Episodic node, all RELATES_TO edges where the episode was the first provenance entry, and any entity nodes that were mentioned only by that episode. Edges that share provenance with other episodes are preserved.

Related PRs

This is PR 2 of 3 from #1489. The companion PRs:

…ntities

delete_episode was calling EpisodicNode.delete, which drops only the
episode node — leaving behind every entity edge sourced from it and
every entity node that the episode mentioned. The result: edges with
stale episode_uuid references in their .episodes list, and orphan
Entity nodes with no remaining provenance.

Graphiti exposes Graphiti.remove_episode for exactly this case: it
deletes the episode, the edges where this episode is the first
provenance entry, and any Entity nodes that were mentioned only by
the deleted episode.

Needed before re-ingesting historical episodes whose extracted edges
landed with hallucinated valid_at timestamps — without the cascade,
re-ingest would orphan the old edges instead of replacing them.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant