Summary
The MCP server's search_memory_facts tool consistently returns:
Error searching facts: Unable to serialize unknown type: <class 'neo4j.time.DateTime'>
against a Neo4j-backed graph with non-trivial fact density. search_nodes works fine on the same graph; the failure is specific to fact (edge) retrieval.
Reproduction
Tested on:
graphiti HEAD = 9cdcc93 (Apr 22, 2026)
- Neo4j community 5.x via
neo4j:community Apple-container image
mcp_server/src/utils/formatting.py unchanged from 375023b
- Group has ~5,000 RELATES_TO edges populated by
add_episode_bulk runs over the past 2 months
mcp__graphiti-pe__search_memory_facts(
query="Payday Netchex add-on synergy",
group_ids=["gcc_deals"],
max_facts=3,
)
# → {"error": "Error searching facts: Unable to serialize unknown type: <class 'neo4j.time.DateTime'>"}
Failure is consistent across queries; it's not group-specific or query-specific.
Expected
search_memory_facts returns a FactSearchResponse with created_at / valid_at / invalid_at / expired_at rendered as ISO 8601 strings (matching the behavior of search_nodes and get_entity_edge).
Root cause hypothesis
mcp_server/src/utils/formatting.py:format_fact_result calls:
edge.model_dump(mode='json', exclude={'fact_embedding'})
Pydantic's mode='json' correctly serializes datetime.datetime, but not neo4j.time.DateTime. The fact that this fails for fact search but not node search suggests at least one search-path-internal EntityEdge construction is bypassing graphiti_core.edges.get_entity_edge_from_record (which calls parse_db_date to convert neo4j.time.DateTime → datetime).
The proper fix is upstream of the formatter — every EntityEdge constructor in the search path should funnel through get_entity_edge_from_record (or equivalently call parse_db_date on temporal fields). Defensive coercion in the formatter is a reasonable second line of defense at the API boundary.
Why this may have flown under the radar
Proposed fix
PR forthcoming with a defensive fix in format_fact_result that coerces neo4j.time.{DateTime,Date} → native datetime.datetime before model_dump. Description of that PR will note that the right long-term fix is upstream of the formatter.
Summary
The MCP server's
search_memory_factstool consistently returns:against a Neo4j-backed graph with non-trivial fact density.
search_nodesworks fine on the same graph; the failure is specific to fact (edge) retrieval.Reproduction
Tested on:
graphitiHEAD =9cdcc93(Apr 22, 2026)neo4j:communityApple-container imagemcp_server/src/utils/formatting.pyunchanged from375023badd_episode_bulkruns over the past 2 monthsFailure is consistent across queries; it's not group-specific or query-specific.
Expected
search_memory_factsreturns aFactSearchResponsewithcreated_at/valid_at/invalid_at/expired_atrendered as ISO 8601 strings (matching the behavior ofsearch_nodesandget_entity_edge).Root cause hypothesis
mcp_server/src/utils/formatting.py:format_fact_resultcalls:Pydantic's
mode='json'correctly serializesdatetime.datetime, but notneo4j.time.DateTime. The fact that this fails for fact search but not node search suggests at least one search-path-internalEntityEdgeconstruction is bypassinggraphiti_core.edges.get_entity_edge_from_record(which callsparse_db_dateto convertneo4j.time.DateTime → datetime).The proper fix is upstream of the formatter — every
EntityEdgeconstructor in the search path should funnel throughget_entity_edge_from_record(or equivalently callparse_db_dateon temporal fields). Defensive coercion in the formatter is a reasonable second line of defense at the API boundary.Why this may have flown under the radar
format_fact_resultwas introduced in the Oct 30, 2025 v1.0.0 MCP refactor (feat: MCP Server v1.0.0 - Modular architecture with multi-provider support #1024); the bug surface is ~6 months old.search_memory_factsis less commonly invoked thansearch_nodesin MCP workflows (topical questions vs. relation-shaped questions).Proposed fix
PR forthcoming with a defensive fix in
format_fact_resultthat coercesneo4j.time.{DateTime,Date}→ nativedatetime.datetimebeforemodel_dump. Description of that PR will note that the right long-term fix is upstream of the formatter.