We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/basicmachines-co/basic-memory'
If you have feedback or need assistance with the MCP directory API, please join our Discord server
"""Tests for datetime serialization in memory schema models."""
import json
from datetime import datetime
from basic_memory.schemas.memory import (
EntitySummary,
RelationSummary,
ObservationSummary,
MemoryMetadata,
GraphContext,
ContextResult,
)
class TestDateTimeSerialization:
"""Test datetime serialization for MCP schema compliance."""
def test_entity_summary_datetime_serialization(self):
"""Test EntitySummary serializes datetime as ISO format string."""
test_datetime = datetime(2023, 12, 8, 10, 30, 0)
entity = EntitySummary(
external_id="550e8400-e29b-41d4-a716-446655440000",
entity_id=1,
permalink="test/entity",
title="Test Entity",
file_path="test/entity.md",
created_at=test_datetime,
)
# Test model_dump_json() produces ISO format
json_str = entity.model_dump_json()
data = json.loads(json_str)
assert data["created_at"] == "2023-12-08T10:30:00"
assert data["type"] == "entity"
assert data["title"] == "Test Entity"
assert data["external_id"] == "550e8400-e29b-41d4-a716-446655440000"
# entity_id is excluded from serialization
assert "entity_id" not in data
def test_relation_summary_datetime_serialization(self):
"""Test RelationSummary serializes datetime as ISO format string."""
test_datetime = datetime(2023, 12, 8, 15, 45, 30)
relation = RelationSummary(
relation_id=1,
entity_id=1,
title="Test Relation",
file_path="test/relation.md",
permalink="test/relation",
relation_type="relates_to",
from_entity="entity1",
to_entity="entity2",
created_at=test_datetime,
)
# Test model_dump_json() produces ISO format
json_str = relation.model_dump_json()
data = json.loads(json_str)
assert data["created_at"] == "2023-12-08T15:45:30"
assert data["type"] == "relation"
assert data["relation_type"] == "relates_to"
# Internal IDs excluded from serialization
assert "relation_id" not in data
assert "entity_id" not in data
assert "from_entity_id" not in data
assert "from_entity_external_id" not in data
assert "to_entity_id" not in data
assert "to_entity_external_id" not in data
def test_observation_summary_serialization(self):
"""Test ObservationSummary keeps file_path/created_at, excludes internal IDs."""
test_datetime = datetime(2023, 12, 8, 20, 15, 45)
observation = ObservationSummary(
observation_id=1,
entity_id=1,
title="Test Observation",
file_path="test/observation.md",
permalink="test/observation",
category="note",
content="Test content",
created_at=test_datetime,
)
json_str = observation.model_dump_json()
data = json.loads(json_str)
# Internal ID fields excluded
assert "observation_id" not in data
assert "entity_id" not in data
assert "entity_external_id" not in data
assert "title" not in data
# file_path and created_at kept (needed when observation is primary_result)
assert data["file_path"] == "test/observation.md"
assert data["created_at"] == "2023-12-08T20:15:45"
# Other kept fields
assert data["type"] == "observation"
assert data["category"] == "note"
assert data["content"] == "Test content"
assert data["permalink"] == "test/observation"
def test_memory_metadata_datetime_serialization(self):
"""Test MemoryMetadata excludes generated_at and total_results."""
test_datetime = datetime(2023, 12, 8, 12, 0, 0)
metadata = MemoryMetadata(
depth=2, generated_at=test_datetime, primary_count=5, related_count=3
)
# Test model_dump_json() excludes internal fields
json_str = metadata.model_dump_json()
data = json.loads(json_str)
# Excluded fields
assert "generated_at" not in data
assert "total_results" not in data
# Kept fields
assert data["depth"] == 2
assert data["primary_count"] == 5
assert data["related_count"] == 3
def test_context_result_with_datetime_serialization(self):
"""Test ContextResult with nested models serializes correctly."""
test_datetime = datetime(2023, 12, 8, 9, 30, 15)
entity = EntitySummary(
external_id="550e8400-e29b-41d4-a716-446655440000",
entity_id=1,
permalink="test/entity",
title="Test Entity",
file_path="test/entity.md",
created_at=test_datetime,
)
observation = ObservationSummary(
observation_id=1,
entity_id=1,
title="Test Observation",
file_path="test/observation.md",
permalink="test/observation",
category="note",
content="Test content",
created_at=test_datetime,
)
context_result = ContextResult(
primary_result=entity, observations=[observation], related_results=[]
)
# Test model_dump_json() produces ISO format for nested models
json_str = context_result.model_dump_json()
data = json.loads(json_str)
# Entity created_at kept, entity_id excluded
assert data["primary_result"]["created_at"] == "2023-12-08T09:30:15"
assert "entity_id" not in data["primary_result"]
# Observation created_at kept (needed for primary_result use), internal IDs excluded
assert data["observations"][0]["created_at"] == "2023-12-08T09:30:15"
assert "observation_id" not in data["observations"][0]
assert "entity_id" not in data["observations"][0]
def test_graph_context_full_serialization(self):
"""Test full GraphContext serialization with all datetime fields."""
test_datetime = datetime(2023, 12, 8, 14, 20, 10)
entity = EntitySummary(
external_id="550e8400-e29b-41d4-a716-446655440000",
entity_id=1,
permalink="test/entity",
title="Test Entity",
file_path="test/entity.md",
created_at=test_datetime,
)
metadata = MemoryMetadata(
depth=1, generated_at=test_datetime, primary_count=1, related_count=0
)
context_result = ContextResult(primary_result=entity, observations=[], related_results=[])
graph_context = GraphContext(
results=[context_result], metadata=metadata, page=1, page_size=10
)
# Test full serialization
json_str = graph_context.model_dump_json()
data = json.loads(json_str)
# Metadata excluded fields
assert "generated_at" not in data["metadata"]
assert "total_results" not in data["metadata"]
# Entity created_at kept, entity_id excluded
assert data["results"][0]["primary_result"]["created_at"] == "2023-12-08T14:20:10"
assert "entity_id" not in data["results"][0]["primary_result"]
def test_datetime_with_microseconds_serialization(self):
"""Test datetime with microseconds serializes correctly."""
test_datetime = datetime(2023, 12, 8, 10, 30, 0, 123456)
entity = EntitySummary(
external_id="550e8400-e29b-41d4-a716-446655440000",
entity_id=1,
permalink="test/entity",
title="Test Entity",
file_path="test/entity.md",
created_at=test_datetime,
)
json_str = entity.model_dump_json()
data = json.loads(json_str)
# Should include microseconds in ISO format
assert data["created_at"] == "2023-12-08T10:30:00.123456"
def test_mcp_schema_validation_compatibility(self):
"""Test that serialized datetime format is compatible with MCP schema validation."""
test_datetime = datetime(2023, 12, 8, 10, 30, 0)
entity = EntitySummary(
external_id="550e8400-e29b-41d4-a716-446655440000",
entity_id=1,
permalink="test/entity",
title="Test Entity",
file_path="test/entity.md",
created_at=test_datetime,
)
# Serialize to JSON
json_str = entity.model_dump_json()
data = json.loads(json_str)
# Verify the format matches expected MCP "date-time" format
datetime_str = data["created_at"]
# Should be parseable back to datetime (ISO format validation)
parsed_datetime = datetime.fromisoformat(datetime_str)
assert parsed_datetime == test_datetime
# Should match the expected ISO format pattern
assert "T" in datetime_str # Contains date-time separator
assert len(datetime_str) >= 19 # At least YYYY-MM-DDTHH:MM:SS format
def test_entity_and_relation_serializers_still_active(self):
"""Test that EntitySummary and RelationSummary datetime serializers work with model_dump."""
test_datetime = datetime(2023, 12, 8, 10, 30, 0)
entity = EntitySummary(
external_id="550e8400-e29b-41d4-a716-446655440000",
entity_id=1,
permalink="test",
title="Test",
file_path="test.md",
created_at=test_datetime,
)
relation = RelationSummary(
relation_id=1,
entity_id=1,
title="Test",
file_path="test.md",
permalink="test",
relation_type="test",
created_at=test_datetime,
)
# model_dump should serialize datetimes via field_serializer
entity_data = entity.model_dump()
assert entity_data["created_at"] == "2023-12-08T10:30:00"
assert "entity_id" not in entity_data # excluded
relation_data = relation.model_dump()
assert relation_data["created_at"] == "2023-12-08T15:45:30" or True # serializer active
assert relation_data["created_at"] == "2023-12-08T10:30:00"
assert "relation_id" not in relation_data # excluded
def test_related_results_serialization_round_trip(self):
"""Test that related_results serialize correctly with identifying fields preserved.
This validates the fix for #627: related_results must retain title, file_path,
and created_at so consumers can identify them without the parent entity context.
"""
test_datetime = datetime(2023, 12, 8, 14, 0, 0)
# Primary entity
primary = EntitySummary(
external_id="aaa",
entity_id=1,
permalink="test/primary",
title="Primary",
file_path="test/primary.md",
created_at=test_datetime,
)
# Related entity — should keep title, file_path, created_at
related_entity = EntitySummary(
external_id="bbb",
entity_id=2,
permalink="test/related",
title="Related Entity",
file_path="test/related.md",
created_at=test_datetime,
)
# Related relation — should keep title, file_path, created_at, relation_type
related_relation = RelationSummary(
relation_id=10,
entity_id=1,
title="Related Via",
file_path="test/primary.md",
permalink="test/primary",
relation_type="relates_to",
from_entity="Primary",
from_entity_id=1,
to_entity="Related Entity",
to_entity_id=2,
created_at=test_datetime,
)
# Observation nested under primary
obs = ObservationSummary(
observation_id=100,
entity_id=1,
title="Primary",
file_path="test/primary.md",
permalink="test/primary",
category="note",
content="Some observation",
created_at=test_datetime,
)
context_result = ContextResult(
primary_result=primary,
observations=[obs],
related_results=[related_entity, related_relation],
)
graph = GraphContext(
results=[context_result],
metadata=MemoryMetadata(
depth=1, generated_at=test_datetime, primary_count=1, related_count=2
),
page=1,
page_size=10,
)
# Serialize via model_dump (same path as build_context tool)
data = graph.model_dump()
result = data["results"][0]
# Primary entity: created_at present, entity_id excluded
assert result["primary_result"]["title"] == "Primary"
assert result["primary_result"]["created_at"] == "2023-12-08T14:00:00"
assert "entity_id" not in result["primary_result"]
# Observation: internal IDs excluded, file_path/created_at kept
obs_data = result["observations"][0]
assert obs_data["category"] == "note"
assert obs_data["content"] == "Some observation"
assert obs_data["permalink"] == "test/primary"
assert obs_data["file_path"] == "test/primary.md"
assert obs_data["created_at"] == "2023-12-08T14:00:00"
assert "observation_id" not in obs_data
assert "entity_id" not in obs_data
assert "title" not in obs_data
# Related entity: identifying fields present
rel_entity = result["related_results"][0]
assert rel_entity["type"] == "entity"
assert rel_entity["title"] == "Related Entity"
assert rel_entity["file_path"] == "test/related.md"
assert rel_entity["created_at"] == "2023-12-08T14:00:00"
assert "entity_id" not in rel_entity
# Related relation: identifying fields present, internal IDs excluded
rel_relation = result["related_results"][1]
assert rel_relation["type"] == "relation"
assert rel_relation["relation_type"] == "relates_to"
assert rel_relation["title"] == "Related Via"
assert rel_relation["file_path"] == "test/primary.md"
assert rel_relation["created_at"] == "2023-12-08T14:00:00"
assert "relation_id" not in rel_relation
assert "entity_id" not in rel_relation
assert "from_entity_id" not in rel_relation
assert "to_entity_id" not in rel_relation
assert "from_entity_external_id" not in rel_relation
assert "to_entity_external_id" not in rel_relation
# Metadata: excluded fields absent
assert "generated_at" not in data["metadata"]
assert "total_results" not in data["metadata"]
assert data["metadata"]["depth"] == 1
assert data["metadata"]["primary_count"] == 1
assert data["metadata"]["related_count"] == 2
# Round-trip: re-parse from JSON
json_str = graph.model_dump_json()
reparsed = json.loads(json_str)
assert reparsed["results"][0]["related_results"][0]["title"] == "Related Entity"
assert reparsed["results"][0]["related_results"][1]["relation_type"] == "relates_to"
def test_observation_as_primary_result_preserves_fields(self):
"""Test that ObservationSummary retains file_path and created_at when used as primary_result.
This covers the case raised in PR review: recent_activity uses
primary_result.created_at and primary_result.file_path for activity
tracking. These fields must survive the JSON round-trip.
"""
test_datetime = datetime(2023, 12, 8, 16, 0, 0)
obs_primary = ObservationSummary(
observation_id=42,
entity_id=7,
entity_external_id="obs-ext-id",
title="Parent Entity Title",
file_path="notes/daily.md",
permalink="notes/daily",
category="status",
content="Shipped the feature today",
created_at=test_datetime,
)
context_result = ContextResult(
primary_result=obs_primary,
observations=[],
related_results=[],
)
graph = GraphContext(
results=[context_result],
metadata=MemoryMetadata(
depth=1, generated_at=test_datetime, primary_count=1, related_count=0
),
page=1,
page_size=10,
)
# Serialize and round-trip through JSON (same path as API responses)
json_str = graph.model_dump_json()
data = json.loads(json_str)
primary = data["results"][0]["primary_result"]
# file_path and created_at must be present for recent_activity tracking
assert primary["file_path"] == "notes/daily.md"
assert primary["created_at"] == "2023-12-08T16:00:00"
assert primary["permalink"] == "notes/daily"
assert primary["category"] == "status"
assert primary["content"] == "Shipped the feature today"
assert primary["type"] == "observation"
# Internal IDs still excluded
assert "observation_id" not in primary
assert "entity_id" not in primary
assert "entity_external_id" not in primary
assert "title" not in primary
# Round-trip: deserialize back into GraphContext
reparsed = GraphContext.model_validate_json(json_str)
reparsed_primary = reparsed.results[0].primary_result
assert reparsed_primary.file_path == "notes/daily.md"
assert reparsed_primary.created_at == test_datetime