mdc·platform

MDC Platform

A server that stores AI models, images, documents, and ordinary database tables side by side — queried entirely in plain sentences. No SELECT, no JOIN, nothing your support team needs to be trained on before they can get an answer out of it.

Installation

git clone https://github.com/saji1970/ModelDB.git
cd ModelDB/mdc

python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

Two ways to run it:

CommandWhat it starts
python -m mdcThe conversational shell (mdc> prompt) - auto-creates and seeds data/mdc.duckdb at a small scale the first time, so startup stays fast.
python -m mdc serveThe REST API and the Storage Explorer browser UI (a Windows-Explorer-style app with a chat panel), at http://127.0.0.1:8000.

python -m mdc db init seeds the full spec-minimum synthetic payments dataset (10,000 merchants / 50,000 customers / 500,000 transactions, deterministic, seed 42) if you want the larger analytics dataset rather than the small dev seed.

Core concepts

MDC is two systems sharing one storage layer, kept deliberately separate because they solve different problems:

  • The polymorphic object store - classifies any uploaded bytes (a model checkpoint, an image, a document, a log file) by real content signature, not filename, and routes it through a tiering decision. This is what § AI models and § Other objects cover.
  • The database layer - unlimited named databases, each with its own schema-validated tables, created and queried entirely through chat or the CLI. This is what § The database layer covers.

Both are driven by the same principle: an LLM proposes an interpretation, but a deterministic validator - never the LLM itself - decides what operation actually reaches storage. No natural-language command generates raw SQL.

Nobody on your team has to learn SQL

Every example on this page - filtering by a threshold, scoping to a specific database, searching across tables - is plain sentences, not query syntax. That's not a simplified subset for beginners standing next to "the real interface"; it is the interface. Support staff, analysts, and anyone else who needs an answer from the data can just ask for it, with no SELECT/WHERE/JOIN training required, and nothing to get subtly wrong the way a hand-written query can.

What you wantTraditional SQLMDC
Find a cheap item by name SELECT * FROM products WHERE name LIKE '%widget%' AND price < 20000; find widget under 20000
Filter by a threshold SELECT * FROM merchants WHERE settlement_balance > 6000; list merchants with balance > 6000
Read another database without switching SELECT * FROM inventory.products; show data in products in database inventory
Why this is safe, not just easierThe natural-language layer never writes SQL itself - an LLM proposes what you might mean, but a deterministic validator decides the actual operation before anything touches storage. "Balance" resolving against three different fields (ledger/available/settlement) triggers a clarifying question instead of a guess; a destructive delete always asks for confirmation first. The easier interface and the safer one are the same interface, on purpose.

Storing & retrieving AI models

Upload a .safetensors file and MDC parses its real header, splits each tensor into row-aligned blocks, and stores every tensor as its own independently addressable, independently tiered object - not one opaque blob.

curl -X POST "http://127.0.0.1:8000/objects?filename=model.safetensors" \
  --data-binary @model.safetensors
# {"object_id":"AIM-58F0BB747C","type":"AI_MODEL","tensor_count":2,"total_parameters":16}

curl http://127.0.0.1:8000/models/AIM-58F0BB747C
# manifest: tensor_count, total_parameters, dtype, checksum, ...

curl http://127.0.0.1:8000/models/AIM-58F0BB747C/tensors/linear.weight
# raw bytes of just that one tensor - the rest of the model is never touched

The same thing through chat:

> list models 1 AI_MODEL object(s). > move AIM-58F0BB747C to hot Moved "AIM-58F0BB747C" and its 2 tensor block(s) to HOT.

move on a model cascades to every one of its tensor blocks - each tensor gets its own independent tier decision at upload time, so moving just the model's manifest would silently leave the actual weights wherever they were.

Storing other object types

The same upload endpoint handles images (real dimension parsing from file headers), documents (text extraction + chunking), logs, tensors, and tabular files - classified from content, never from a file extension.

> store ./report.md Stored "report.md" as DOC-30A3DC793C (DOCUMENT). > search for revenue 1 document matched "revenue". > explain DOC-30A3DC793C ARCHIVE (DNA-encoded): explicit archival request on an immutable, unaccessed object. Compressed with ZLIB: the content measured as compressible.

Every object - model, image, document, or table row - carries a SHA-256 checksum verified on every read. A tampered or corrupted read never silently returns bad bytes; it fails with a clear integrity error instead.

Storage tiers

Tier is a real consequence, not a label - only HOT maps to genuinely different physical storage (an in-memory backend) today; ARCHIVE is the one exception that goes further still, actually encoding bytes to simulated DNA base sequences (2 bits per base, A/C/G/T - a real, working prototype encoder, not physical synthesis).

TierWhenBackend
HOT≥ 10 reads/dayIn-memory (not persisted across a restart - by design)
WARM≥ 0.1 reads/day, or mutable & recently touchedDuckDB (durable)
COLDImmutable, zero access, zero mutationDuckDB (durable)
ARCHIVEExplicitly requested (never inferred from low access alone)Simulated DNA-base encoding
DNA double helix illustration with labeled A/C/G/T base pairs

The core concept: 2 bits per base

Every byte in the ARCHIVE tier is mapped directly to 4 DNA bases - the same information-density idea real DNA-storage research is built on:

00 → A      01 → C      10 → G      11 → T

A real, working error-correction scheme (N-way repetition with majority voting across independently-corrupted copies) and a seeded corruption simulator sit on top of the encoding - modeling substitution, insertion, deletion, and dropout errors the way real DNA storage research does. This is a simulation: a string of ACGT characters in a database, not a molecule - no physical synthesis or sequencing anywhere in this codebase.

Every payload is AES-256-GCM encrypted before it's DNA-encoded - the ACGT sequence is ciphertext-as-base-letters, not plaintext-as-base-letters, so it's unreadable without the key this backend holds. The mapping above is public (you're reading it), so encoding plaintext directly would only be obfuscation; encryption is what actually makes the archive tier unreadable outside MDC's own API/CLI. mdc-lite stores every entry the same way.

Read the full whitepaper → for the error-correction design, the corruption model, and an honest, specific answer to "does this use quantum encryption?" (short version: no - and here's what quantum computing actually does and doesn't threaten about long-retention archival data).

Phase 2 (planned, not yet built): extending DNA-tier features further on the platform side - stronger ECC, a Windows CLI bridge that can open an mdc-lite store directly when a phone/wearable is connected, and research into applying the same tier beyond archival storage.

Multiple databases

No hardcoded limit on how many databases exist - each is a fully isolated DuckDB file with its own schema registry, created lazily the moment you ask for it, entirely from chat:

> create database inventory Created database "inventory" and switched to it. > create table products with sku string, name string, price decimal Created table "products" in "inventory" with fields: sku (string), name (string), price (decimal). > insert into products sku=ABC123, name=Widget, price=9.99 Inserted PRO-461A9FC740 into "products".

Tables & queries

Read another database's table without switching to it, using an explicit qualifier:

show data in products in database inventory
describe table products in database inventory

Table creation only ever produces a validated, typed SchemaRegistry collection - never raw SQL DDL generated from chat text, the same injection-avoidance principle as everything else in MDC.

Universal search

find searches every database's tables and documents at once, with deterministic clause extraction for amount constraints and explicit scoping - no need to know which database or table something lives in:

> find widget under 20000 1 row(s) matched "widget": ABC123 - Widget - $9.99 (inventory.products) > find widget in table products in database inventory 1 row(s) matched.

REST API reference

Every route below requires Authorization: Bearer <token> - see Connect your own NLU or UI for how to issue one.

EndpointDoes
POST /objectsUpload any file - classified and routed automatically
GET /objects/{id}Metadata: type, tier, compression, checksum
POST /objects/{id}/readRaw content back
POST /objects/{id}/moveForce a tier - cascades to every tensor block for a model
GET /objects/{id}/strategyHuman-readable explanation of the current tier/compression decision
GET /models/{id}A model's manifest (tensor count, parameters, dtype, checksum)
GET /models/{id}/tensors/{name}One tensor's raw bytes, without touching the rest of the model
GET /databases / POST /databasesList or create a named database
GET /databases/{name}/tables/{table}/rowsA table's rows, from the browser UI or any HTTP client
POST /databases/{name}/tables/{table}Create a table from structured field data - no sentence to construct
POST /databases/{name}/tables/{table}/rowsInsert a row from structured field data
GET /findStructured universal search - ?q=&min=&max=&database=&table=
POST /chatThe same conversational layer the CLI and browser chat panel use

Chat command reference

Say thisTo
store ./file, list models, show <id>, archive <id>, move <id> to hot, search for <text>Work with polymorphic objects (models, images, documents, ...)
create database <name>, create table ... with ..., show data in <table>, insert into <table> ...Manage databases and tables
find <text> [under/over <amount>]Search every database and document at once
Show all merchants, Create a merchant called ...Merchants CRUD/analytics (a separate, earlier conversational domain - see the platform README for why)
Full referenceThe complete README, including the acceptance-test walkthroughs this documentation draws from, lives at mdc/README.md; the architecture write-up (including the honest scoping note on why merchants-CRUD and the object store stay separate systems) is at docs/ARCHITECTURE.md.

Connect your own NLU or UI

MDC's own chat layer doesn't have to be the only front door. Two integration patterns, depending on whether your system has already done its own language understanding by the time it talks to MDC:

Authenticate first — every route but the built-in UI needs a token

CORS is deliberately wide open (allow_origins=["*"]) so a third-party integration can call this API from a different origin without a browser rejecting the response first - that's the point of everything on this page. Open CORS was never itself a security boundary against a non-browser client though, so every route except the built-in browser UI's own HTML page requires a bearer token:

$ mdc token issue rasa-integration --role editor
mdc_kP3x...   (shown once - store it now)

Send it as Authorization: Bearer <token> on every request. Revoke it later with mdc token revoke rasa-integration, or set MDC_API_TOKENS (comma-separated) to accept fixed tokens without the on-disk store, for CI/deployment.

--role scopes what that specific token can do - omit it and a token defaults to admin (full access, matching the original all-tokens-are-equal behavior), or pick something narrower for an integration that shouldn't have it:

RoleCan do
viewerRead/query only - GET routes, /find
editor+ create tables, insert/update/delete rows, upload/replace/delete objects, use /chat
db_admin+ create brand-new databases (POST /databases)
admin+ manage user accounts (CLI only, not exposed over the API)

A token with an insufficient role still authenticates - a valid-but-underprivileged token gets 403, not the 401 reserved for a missing or invalid token.

User accounts and roles (CLI)

Separately from API tokens, the CLI itself now has real accounts. The first time you run mdc or mdc serve with no user configured yet, it interactively asks for an admin username and password and creates that first account as admin - the same four roles as tokens, since both share one vocabulary. Manage accounts afterward with:

$ mdc user create alice --role editor
Password: ********
Repeat for confirmation: ********
Created user alice with role editor.

$ mdc user list
  admin  (admin)
  alice  (editor)

$ mdc user set-role alice db_admin
$ mdc user delete alice

Passwords are never stored in a recoverable form - hashlib.scrypt derives a key from the password and a random per-user salt, and only that salt and derived key are persisted (mode 0600), the same reasoning already used for API tokens.

Pattern 1 — pass-through (simplest, works today, zero new code)

Forward raw user text to POST /chat and let MDC's own deterministic parser do the work - the same pipeline the browser chat panel and CLI already use. This is the right choice for most integrations: your system doesn't need to understand databases, models, or storage tiers at all, just detect "this is a data question" and relay the text.

# A RASA custom action (actions.py) - the entire integration
import requests
from rasa_sdk import Action

class ActionQueryMDC(Action):
    def name(self):
        return "action_query_mdc"

    def run(self, dispatcher, tracker, domain):
        reply = requests.post(
            "http://localhost:8000/chat",
            json={"message": tracker.latest_message.get("text"), "session_id": tracker.sender_id},
            headers={"Authorization": "Bearer mdc_kP3x..."},
        ).json()
        dispatcher.utter_message(text=reply["message"])
        return []

Note session_id: tracker.sender_id - reusing RASA's own conversation id keeps MDC's per-session state (which database you switched to, pronoun resolution) aligned with RASA's own multi-turn state, so "switch to database inventory" followed later by "show data in products" behaves correctly across turns.

Pattern 2 — structured (for a system that already extracted intent)

If your NLU has already parsed "add Widget at $9.99 to inventory" into its own slots, reconstructing MDC's sentence syntax from those slots is unnecessary indirection. Call the structured REST endpoints directly with the extracted values instead:

# RASA custom action calling the structured endpoint directly,
# using slots RASA's own NLU already extracted
requests.post(
    f"http://localhost:8000/databases/inventory/tables/products/rows",
    json={"values": {"name": tracker.get_slot("item_name"), "price": tracker.get_slot("price")}},
    headers={"Authorization": "Bearer mdc_kP3x..."},
)

Every structured endpoint runs through the exact same validation as the chat/CLI path it mirrors (schema-registry-only table creation, typed field coercion on insert) - there's no separate, less-checked code path for programmatic callers.

A custom "ChatGPT-style" UI

A minimal chat interface is one fetch() call - the same POST /chat endpoint the Storage Explorer's own chat panel uses:

async function ask(message) {
  const res = await fetch("http://localhost:8000/chat", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Authorization": "Bearer " + MDC_TOKEN,
    },
    body: JSON.stringify({ message, session_id: crypto.randomUUID() }),
  });
  const { message: reply, data } = await res.json();
  return { reply, data };  // data is a table's rows / a search result list, when there is one
}
Before you deploy this reachable from anywhere but localhostTokens now carry roles, and there are real user accounts for the CLI - but there's still no rate limiting, and API access control stops at "this token's role permits this route," not a per-request audit trail tied to an individual person. Fine for local development and internal, network-isolated use with tokens distributed by hand; put a reverse proxy with rate limiting in front of it before exposing it any more broadly than that.