Architecture¶
Memcove is a control plane (an MCP server agents talk to) over a data plane (object store + query engine + catalog + registry + a streaming server). One invariant runs through everything: writes go through PyIceberg; reads, derivations, and exports go through Trino — and every operation is confined to the caller's tenant namespace.
flowchart TB
agent["LLM agent (MCP client)"]
proxy["Auth proxy (prod only)\nsets x-memcove-tenant"]
subgraph control["Control plane — MCP"]
server["server.py (FastMCP, Streamable HTTP)"]
tools["tools/* — ingest, query, derive, artifacts, objects, discovery"]
guard["sql_guard.py — validate & rewrite SQL"]
tenancy["tenancy.py — resolve tenant"]
end
subgraph data["Data plane"]
catalog["catalog.py (PyIceberg) — WRITE"]
trino["trino_client.py (Trino) — READ / derive / export"]
storage["storage.py (S3 / MinIO)"]
registry["registry.py (Postgres) — metadata & lineage"]
flight["flight_server.py (Arrow Flight) — streaming"]
end
lake[("S3 + Iceberg tables")]
agent -->|MCP over HTTP| proxy --> server
server --> tenancy
server --> tools
tools --> guard
tools --> catalog
tools --> trino
tools --> storage
tools --> registry
agent -.->|bulk Arrow bytes, out of band| flight
catalog --> lake
trino --> lake
flight --> catalog
flight --> trino
Components¶
Control plane¶
server.py— the FastMCP server (Streamable HTTP). Registers the 12 tools and 2 resources, resolves the tenant from request headers (_tenant(ctx)), and delegates to the thin orchestration modules intools/.tools/*— one module per verb:ingest.py(ingest_object,request_upload),query.py(run_query),derive.py(derive_object),artifacts.py(export_artifact),objects.py(describe_object,get_object,list_objects,drop_object),discovery.py(discover_reference_data).
Core¶
catalog.py— PyIceberg write path. The only place data is physically written.write_arrow(namespace, label, table, mode)creates/replaces/appends an Iceberg table from a PyArrow table. Alsoensure_namespace,table_exists,list_labels,drop_table,load_schema.trino_client.py— read / derive / export engine. Wraps the Trino DBAPI:execute,execute_arrow,execute_update(DDL/CTAS),scalar,ensure_schema._principal(run_as)implements optional per-tenant impersonation;_connectapplies operator-configured session-property resource caps.storage.py— object store. boto3 S3/MinIO client.read_parquet_table,write_parquet_table,write_bytes, and the presigned-URL helperspresign_put/presign_get.registry.py— Postgres metadata & lineage. Source of truth for object metadata (memcove_objects) and lineage edges (memcove_lineage). The Iceberg catalog remains the source of truth for data and schema. Every query is tenant-scoped.sql_guard.py— SQL safety.validate_select(...)parses with sqlglot, enforces read-only single statements, and rewrites every table reference into the tenant namespace. See Tenant isolation & the SQL guard.tenancy.py— tenant resolution.normalize_tenantmaps a raw id tot_<id>;resolve_tenantpicks the tenant from trusted headers (fail-closed provisioning map, or direct-header dev mode).tickets.py— Flight ticket signing. HMAC-signs and verifies the short-lived JSON commands used by the streaming plane.arrow_io.py,models.py,naming.py— inline-payload decoding, the Pydantic return types (MemoryObject,PreviewResult,ArtifactRef,UploadTicket, …), and label validation.
Streaming data plane¶
flight_server.py— the gRPC Arrow Flight server.do_put(ingest),do_get/get_flight_info(read/query). Lets clients move bulk Arrow batches out-of-band.
The five flows¶
1. Ingest — remember_dataset → PyIceberg¶
Resolve the source into a PyArrow table (inline decode / allowlisted s3:// read /
tenant-bound upload handle), then write via catalog.write_arrow and record metadata
in the registry. Not Trino.
2. Query — query_memory → guard → Trino¶
sql_guard.validate_select validates and rewrites the SQL, wrap_preview caps it, and
trino_client.execute runs it (as the tenant when impersonation is on). Returns a capped
PreviewResult.
3. Derive — derive_dataset → Trino CTAS¶
The validated SELECT is wrapped in CREATE TABLE <catalog>.<tenant>.<new_label> AS ...
and run via Trino. Lineage (referenced parent datasets) is recorded in the registry.
4. Export — export_dataset → S3 presigned URL¶
The validated SELECT (capped at export_row_cap) runs through execute_arrow; the Arrow
table is serialized (parquet/csv/json), written to the artifacts bucket, and returned as
a presigned download URL.
5. Streaming — Arrow Flight¶
The control plane mints a signed, expiring ticket/descriptor; the client streams
bytes against the Flight server. Reads run the same guard + Trino path; writes go through
catalog.write_arrow. Bulk bytes never touch the MCP channel. See
Streaming.
The two write surfaces¶
The core invariant
The entire write surface is exactly two code paths — catalog.write_arrow (inline /
s3 / upload ingest, and Flight DoPut) and the Trino CTAS in derive_object. Both are
fed only validated labels and guard-rewritten SQL. Everything else is read-only. That
is what makes the isolation model tractable.