Event Sourcing
MCP Hangar persists domain events in an append-only Event Store. This supports auditing, projections, and rebuilding state from history.
Persistence format
Events are serialized as JSON and stored with:
stream_id(e.g.provider:math)stream_version(0-based, optimistic concurrency)event_type(e.g.ProviderStarted)data(JSON payload)
Schema versioning
Every serialized payload includes a schema version field:
{
"_version": 1,
"provider_id": "math",
"mode": "subprocess",
"tools_count": 3,
"startup_duration_ms": 50.0
}Backwards compatibility rule:
- Events persisted without
_versionare treated as v1.
Upcasting (schema evolution)
When schemas evolve between releases, older persisted events might not match the current event constructor signature.
MCP Hangar supports upcasting: converting an event payload from an older schema version to the current one at read time.
Rules
- Upcasting only happens on read (deserialization).
- Upcasters are pure functions (no I/O, no time dependence).
- Upcasters must advance exactly one version step:
vN -> vN+1. - Updating
EVENT_VERSION_MAPrequires providing the full upcaster chain.
Where versions are defined
Current schema versions live in:
mcp_hangar/infrastructure/persistence/event_serializer.pyEVENT_VERSION_MAPget_current_version(event_type)
Writing an upcaster
Create an upcaster in mcp_hangar/infrastructure/persistence/upcasters/:
from typing import Any
from mcp_hangar.infrastructure.persistence.event_upcaster import IEventUpcaster
class ProviderStartedV1ToV2(IEventUpcaster):
"""Example evolution: add a `tags` field introduced in v2."""
@property
def event_type(self) -> str:
return "ProviderStarted"
@property
def from_version(self) -> int:
return 1
@property
def to_version(self) -> int:
return 2
def upcast(self, data: dict[str, Any]) -> dict[str, Any]:
return {**data, "tags": []}Registering upcasters (composition root)
Upcaster chain is built at startup:
from mcp_hangar.infrastructure.persistence import EventSerializer, SQLiteEventStore
from mcp_hangar.infrastructure.persistence.event_upcaster import UpcasterChain
from mcp_hangar.infrastructure.persistence.upcasters.provider_started import ProviderStartedV1ToV2
chain = UpcasterChain()
chain.register(ProviderStartedV1ToV2())
serializer = EventSerializer(upcaster_chain=chain)
store = SQLiteEventStore(db_path, serializer=serializer)Forward compatibility (extra payload keys)
Deserializer ignores unknown payload keys when reconstructing event instances. This means newer payloads can contain additional fields without breaking older code paths.
Troubleshooting
UpcastingError: Missing upcaster...usually means:EVENT_VERSION_MAPwas bumped, but the full set of upcasters was not registered.
- If you need to rename an event type, treat it as a new type and keep the old one readable via:
- keeping the old
event_typeregistered inEVENT_TYPE_MAP, or - a custom adapter at the serialization boundary.
- keeping the old