Model Database Protocol

Protocolo de acceso a bases de datos seguro y basado en intenciones para sistemas de IA: los LLMs envían intenciones estructuradas en lugar de SQL sin procesar.

Documentación

MDBP - Protocolo de Base de Datos para Modelos

Protocolo de acceso a datos basado en intenciones para sistemas de IA.

MDBP permite el acceso seguro a bases de datos para LLMs. En lugar de generar SQL crudo, los LLMs producen objetos de intención estructurados. MDBP valida estas intenciones contra un registro de esquemas, aplica políticas de acceso, construye consultas parametrizadas mediante SQLAlchemy y devuelve respuestas amigables para LLMs.

LLM Intent (JSON) -> Schema Validation -> Policy Check -> SQLAlchemy Query -> Response

Tabla de Contenidos


Instalación

pip install mdbp

Para desarrollo:

pip install mdbp[dev]

Requisitos:

  • Python >= 3.10
  • SQLAlchemy >= 2.0
  • Pydantic >= 2.0
  • mcp >= 1.0

Bases de Datos Soportadas: Cualquier backend compatible con SQLAlchemy: PostgreSQL, MySQL, SQLite, MSSQL, Oracle, BigQuery, etc.


Inicio Rápido

En Funcionamiento en 3 Líneas

from mdbp import MDBP

mdbp = MDBP(db_url="sqlite:///my.db")
result = mdbp.query({"intent": "list", "entity": "product", "limit": 10})

Cuando se llama a MDBP(db_url=...), todas las tablas y columnas se descubren automáticamente desde la base de datos. No se requiere registro manual.

Ejemplo de Salida

{
    "success": true,
    "intent": "list",
    "entity": "product",
    "summary": "10 product(s) found",
    "data": [
        {"id": 1, "name": "Laptop", "price": 15000},
        {"id": 2, "name": "Mouse", "price": 250}
    ]
}

Respuesta de Error

{
    "success": false,
    "intent": "list",
    "entity": "spaceship",
    "error": {
        "code": "MDBP_SCHEMA_ENTITY_NOT_FOUND",
        "message": "Entity 'spaceship' not found in schema registry.",
        "details": {
            "entity": "spaceship",
            "available_entities": ["product", "order", "customer"]
        }
    }
}

Cuando un LLM alucina un nombre de tabla, MDBP lo detecta y devuelve la lista de entidades disponibles. El LLM puede autocorregirse usando esta retroalimentación.

Ejemplo del Mundo Real (PostgreSQL)

from mdbp import MDBP

mdbp = MDBP(
    db_url="postgresql+psycopg2://user:password@localhost:5432/mydb",
    allowed_intents=["list", "get", "count", "aggregate"],  # read-only mode
)

# Auto-discovers all tables and columns
schema = mdbp.describe_schema()
for entity, info in schema.items():
    print(f"{entity}: {len(info['fields'])} fields")

# List with sorting and limit
result = mdbp.query({
    "intent": "list",
    "entity": "stock_price",
    "fields": ["Date", "Close", "Volume"],
    "sort": [{"field": "Date", "order": "desc"}],
    "limit": 5,
})
for row in result["data"]:
    print(f"{row['Date']} | ${row['Close']:.2f} | Vol: {row['Volume']:,}")

# Aggregation
result = mdbp.query({
    "intent": "aggregate",
    "entity": "stock_price",
    "aggregation": {"op": "avg", "field": "Close"},
})
print(f"Average close: ${float(result['data'][0]['result']):.2f}")

# Count with filters
result = mdbp.query({
    "intent": "count",
    "entity": "stock_price",
    "filters": {"Close__gte": 100},
})
print(f"Days above $100: {result['data']['count']}")

# Hallucination protection
result = mdbp.query({"intent": "list", "entity": "nonexistent_table"})
print(result["error"]["code"])           # MDBP_SCHEMA_ENTITY_NOT_FOUND
print(result["error"]["details"])        # {"available_entities": [...]}

mdbp.dispose()

Conexión a Base de Datos Sin Configuración

MDBP soporta cualquier base de datos compatible con SQLAlchemy sin código adicional. Solo instala el controlador y conéctate:

pip install mdbp
mdbp-server --db-url <DATABASE_URL>

Todas las tablas, columnas y tipos se descubren automáticamente — no se necesita definición de esquema ni código de servidor.

Ejemplos:

# SQLite
mdbp-server --db-url sqlite:///my.db

# PostgreSQL
pip install psycopg2
mdbp-server --db-url postgresql+psycopg2://user:pass@localhost/mydb

# MySQL
pip install pymysql
mdbp-server --db-url mysql+pymysql://user:pass@localhost/mydb

# BigQuery
pip install sqlalchemy-bigquery
gcloud auth application-default login
mdbp-server --db-url bigquery://project-id/dataset

# SQL Server
pip install pyodbc
mdbp-server --db-url mssql+pyodbc://user:pass@host/db?driver=ODBC+Driver+18+for+SQL+Server

Claude Desktop / Cursor / Clientes MCP:

{
    "mcpServers": {
        "my-database": {
            "command": "mdbp-server",
            "args": ["--db-url", "sqlite:///my.db"]
        }
    }
}

Conceptos Clave

¿Qué es una Intención?

Una intención es un objeto JSON estructurado que describe una operación de base de datos. Cada intención contiene estos campos principales:

CampoTipoRequeridoDescripción
intentstringTipo de operación: list, get, count, aggregate, create, update, delete
entitystringNombre de la tabla/entidad objetivo
filtersobjectNoCondiciones de filtro
fieldsarrayNoCampos a devolver (vacío = todos)
sortarrayNoOrdenamiento
limitintegerNoLímite de resultados
offsetintegerNoDesplazamiento de paginación

Pipeline

Cada llamada a mdbp.query() pasa por estas etapas:

1. Parse       -> Convert dict to Intent model (Pydantic validation)
2. Whitelist   -> Check allowed_intents (global restriction)
3. Schema      -> Verify entity and fields exist in schema registry
4. Policy      -> Role-based access control, field restrictions
5. Plan        -> Convert Intent to SQLAlchemy statement
6. [Dry-run?]  -> Return compiled SQL without executing (if enabled)
7. Execute     -> Run parameterized query
8. Mask        -> Apply data masking to result fields (if configured)
9. Format      -> Convert result to LLM-friendly JSON

Tipos de Intención

list - Listar Registros

mdbp.query({
    "intent": "list",
    "entity": "product",
    "fields": ["name", "price"],
    "filters": {"price__gte": 100},
    "sort": [{"field": "price", "order": "desc"}],
    "limit": 10,
    "offset": 0,
    "distinct": True
})

get - Obtener un Solo Registro

mdbp.query({
    "intent": "get",
    "entity": "product",
    "id": 42
})

Devuelve un solo registro por clave primaria. Devuelve error MDBP_NOT_FOUND si no existe el registro.

count - Contar Registros

mdbp.query({
    "intent": "count",
    "entity": "product",
    "filters": {"category": "electronics"}
})

Salida:

{"success": true, "data": {"count": 156}}

aggregate - Agregar

mdbp.query({
    "intent": "aggregate",
    "entity": "order",
    "aggregation": {"op": "sum", "field": "amount"}
})

Operaciones soportadas: sum, avg, min, max, count

Múltiples agregaciones:

mdbp.query({
    "intent": "aggregate",
    "entity": "order",
    "aggregations": [
        {"op": "count", "field": "id"},
        {"op": "sum", "field": "amount"},
        {"op": "avg", "field": "amount"}
    ],
    "group_by": ["status"]
})

create - Crear Registro

mdbp.query({
    "intent": "create",
    "entity": "product",
    "data": {"name": "Laptop", "price": 999.99},
    "returning": ["id", "name"]
})

update - Actualizar Registro

mdbp.query({
    "intent": "update",
    "entity": "product",
    "id": 5,
    "data": {"price": 899.99}
})

Actualización masiva con filtros:

mdbp.query({
    "intent": "update",
    "entity": "product",
    "filters": {"status": "draft"},
    "data": {"status": "published"}
})

delete - Eliminar Registro

mdbp.query({
    "intent": "delete",
    "entity": "product",
    "id": 5
})

Filtrado

Filtros Simples (Sufijo de Operador)

Agrega un sufijo al nombre del campo en el diccionario filters para especificar el operador:

mdbp.query({
    "intent": "list",
    "entity": "product",
    "filters": {
        "category": "electronics",       # equality (=)
        "price__gt": 100,                # greater than (>)
        "price__lte": 5000,              # less than or equal (<=)
        "name__like": "%laptop%",        # LIKE
        "status__ne": "deleted",          # not equal (!=)
        "color__in": ["red", "blue"],     # IN (...)
        "stock__not_null": True,          # IS NOT NULL
    }
})

Todos los Operadores:

SufijoEquivalente SQLEjemplo
(ninguno)={"city": "Istanbul"}
__gt>{"price__gt": 100}
__gte>={"price__gte": 100}
__lt<{"price__lt": 500}
__lte<={"price__lte": 500}
__ne!={"status__ne": "deleted"}
__likeLIKE{"name__like": "%phone%"}
__ilikeILIKE{"name__ilike": "%Phone%"}
__not_likeNOT LIKE{"name__not_like": "%test%"}
__inIN (...){"id__in": [1, 2, 3]}
__not_inNOT IN{"id__not_in": [4, 5]}
__betweenBETWEEN{"price__between": [100, 500]}
__nullIS NULL{"email__null": true}
__not_nullIS NOT NULL{"email__not_null": true}

Filtros Complejos (where)

Usa el campo where para lógica anidada AND/OR/NOT:

mdbp.query({
    "intent": "list",
    "entity": "product",
    "where": {
        "logic": "or",
        "conditions": [
            {"field": "category", "op": "eq", "value": "electronics"},
            {
                "logic": "and",
                "conditions": [
                    {"field": "price", "op": "lt", "value": 50},
                    {"field": "stock", "op": "gt", "value": 0}
                ]
            }
        ]
    }
})

Equivalente SQL:

WHERE category = 'electronics' OR (price < 50 AND stock > 0)

Ejemplo con NOT:

"where": {
    "logic": "not",
    "conditions": [
        {"field": "status", "op": "eq", "value": "deleted"}
    ]
}

Ejemplo con EXISTS:

"where": {
    "logic": "and",
    "conditions": [
        {
            "op": "exists",
            "subquery": {
                "intent": "list",
                "entity": "order",
                "fields": ["id"],
                "filters": {"customer_id": 1}
            }
        }
    ]
}

Filtros con Subconsultas

Usa $query en los valores de filtro para subconsultas:

mdbp.query({
    "intent": "list",
    "entity": "product",
    "filters": {
        "category_id__in": {
            "$query": {
                "intent": "list",
                "entity": "category",
                "fields": ["id"],
                "filters": {"name": "electronics"}
            }
        }
    }
})

Equivalente SQL:

SELECT * FROM products
WHERE category_id IN (SELECT id FROM categories WHERE name = 'electronics')

Operaciones JOIN

JOIN Básico

mdbp.query({
    "intent": "list",
    "entity": "order",
    "fields": ["product", "amount", "customer.name"],
    "join": [{
        "entity": "customer",
        "type": "inner",
        "on": {"customer_id": "id"}
    }]
})
  • on: formato {local_field: foreign_field}
  • Notación de punto en fields: "customer.name" se resuelve a la columna de la tabla unida
  • type: inner, left, right, full

Múltiples JOINs

mdbp.query({
    "intent": "list",
    "entity": "order_item",
    "fields": ["quantity", "order.status", "product.name"],
    "join": [
        {"entity": "order", "type": "inner", "on": {"order_id": "id"}},
        {"entity": "product", "type": "inner", "on": {"product_id": "id"}}
    ]
})

Auto-JOIN (Alias)

mdbp.query({
    "intent": "list",
    "entity": "employee",
    "fields": ["name", "manager.name"],
    "join": [{
        "entity": "employee",
        "alias": "manager",
        "type": "left",
        "on": {"manager_id": "id"}
    }]
})

Agregación

GROUP BY

mdbp.query({
    "intent": "aggregate",
    "entity": "order",
    "aggregation": {"op": "count", "field": "id"},
    "group_by": ["status"]
})

HAVING

mdbp.query({
    "intent": "aggregate",
    "entity": "order",
    "aggregation": {"op": "sum", "field": "amount"},
    "group_by": ["customer_id"],
    "having": [{
        "op": "sum",
        "field": "amount",
        "condition": "gt",
        "value": 10000
    }]
})

SQL: HAVING SUM(amount) > 10000

Modos Avanzados de GROUP BY

# ROLLUP
mdbp.query({
    "intent": "aggregate",
    "entity": "sale",
    "aggregation": {"op": "sum", "field": "amount"},
    "group_by": ["year", "quarter"],
    "group_by_mode": "rollup"
})

# CUBE
mdbp.query({
    "intent": "aggregate",
    "entity": "sale",
    "aggregation": {"op": "sum", "field": "amount"},
    "group_by": ["region", "product"],
    "group_by_mode": "cube"
})

# GROUPING SETS
mdbp.query({
    "intent": "aggregate",
    "entity": "sale",
    "aggregation": {"op": "sum", "field": "amount"},
    "group_by": ["region", "product"],
    "group_by_mode": "grouping_sets",
    "grouping_sets": [["region"], ["product"], []]
})

Campos Calculados

CASE WHEN

mdbp.query({
    "intent": "list",
    "entity": "product",
    "fields": ["name", "price"],
    "computed_fields": [{
        "name": "price_tier",
        "case": {
            "when": [
                {"condition": {"field": "price", "op": "gt", "value": 1000}, "then": "premium"},
                {"condition": {"field": "price", "op": "gt", "value": 100}, "then": "standard"}
            ],
            "else_value": "budget"
        }
    }]
})

Funciones de Ventana

mdbp.query({
    "intent": "list",
    "entity": "product",
    "fields": ["name", "price", "category_id"],
    "computed_fields": [{
        "name": "price_rank",
        "window": {
            "function": "rank",
            "partition_by": ["category_id"],
            "order_by": [{"field": "price", "order": "desc"}]
        }
    }]
})

Funciones de ventana soportadas: rank, dense_rank, row_number, ntile, lag, lead, first_value, last_value, sum, avg, min, max, count

Funciones Escalares

mdbp.query({
    "intent": "list",
    "entity": "user",
    "fields": ["id"],
    "computed_fields": [
        {
            "name": "email_upper",
            "function": {"name": "upper", "args": ["email"]}
        },
        {
            "name": "display_name",
            "function": {
                "name": "coalesce",
                "args": ["nickname", {"literal": "Anonymous"}]
            }
        },
        {
            "name": "price_int",
            "function": {"name": "cast", "args": ["price"], "cast_to": "integer"}
        },
        {
            "name": "order_year",
            "function": {"name": "extract", "args": [{"literal": "year"}, "created_at"]}
        }
    ]
})

Funciones escalares soportadas: coalesce, upper, lower, cast, concat, trim, length, abs, round, substring, extract, now, current_date, replace

Nota: extract requiere el primer argumento como {"literal": "part"} donde la parte es year, month, day, hour, minute, o second.


CTE (Expresiones de Tabla Común)

mdbp.query({
    "intent": "list",
    "entity": "product",
    "fields": ["name", "price"],
    "cte": [{
        "name": "expensive_categories",
        "query": {
            "intent": "aggregate",
            "entity": "product",
            "aggregation": {"op": "avg", "field": "price"},
            "group_by": ["category_id"],
            "having": [{"op": "avg", "field": "price", "condition": "gt", "value": 500}]
        }
    }],
    "filters": {
        "category_id__in": {"$cte": "expensive_categories", "field": "category_id"}
    }
})

Operaciones de Escritura

batch_create - Inserción Masiva

mdbp.query({
    "intent": "batch_create",
    "entity": "product",
    "rows": [
        {"name": "Laptop", "price": 15000},
        {"name": "Mouse", "price": 250},
        {"name": "Keyboard", "price": 800}
    ]
})

upsert - Insertar o Actualizar

mdbp.query({
    "intent": "upsert",
    "entity": "product",
    "data": {"id": 1, "name": "Laptop Pro", "price": 18000},
    "conflict_target": ["id"],
    "conflict_update": ["name", "price"]
})

SQL: INSERT ... ON CONFLICT (id) DO UPDATE SET name=..., price=...

UPDATE con JOIN

mdbp.query({
    "intent": "update",
    "entity": "order",
    "data": {"status": "vip_order"},
    "from_entity": "customer",
    "from_join_on": {"customer_id": "id"},
    "from_filters": {"tier": "vip"}
})

RETURNING

mdbp.query({
    "intent": "create",
    "entity": "product",
    "data": {"name": "Tablet", "price": 3000},
    "returning": ["id", "name"]
})

Operaciones de Conjuntos

UNION

mdbp.query({
    "intent": "union",
    "entity": "customer",
    "union_all": False,
    "union_queries": [
        {"intent": "list", "entity": "customer", "fields": ["name"], "filters": {"city": "Istanbul"}},
        {"intent": "list", "entity": "customer", "fields": ["name"], "filters": {"city": "Ankara"}}
    ]
})

Las intenciones intersect y except también se soportan de la misma manera.


Registro de Esquemas

Auto-Descubrimiento (Predeterminado)

mdbp = MDBP(db_url="sqlite:///my.db")
# All tables and columns are automatically registered

Conversión de nombre de tabla a nombre de entidad:

  • products -> product
  • categories -> category
  • order_items -> order_item

Soporte para BigQuery: El controlador SQLAlchemy de BigQuery no puede listar tablas mediante el estándar MetaData.reflect(). MDBP recurre automáticamente a INFORMATION_SCHEMA.TABLES para descubrir tablas y refleja cada una individualmente. No se necesita configuración adicional — solo pasa una URL de BigQuery:

mdbp = MDBP(db_url="bigquery://project-id/dataset")

Registro Manual

Anula el auto-descubrimiento o proporciona nombres personalizados:

from mdbp.core.schema_registry import EntitySchema, FieldSchema

mdbp.register_entity(EntitySchema(
    entity="order",
    table="orders",
    primary_key="id",
    fields={
        "id": FieldSchema(column="id", dtype="integer"),
        "customer_name": FieldSchema(
            column="cust_name",
            dtype="text",
            description="Full name of the customer"
        ),
        "total": FieldSchema(
            column="total_amount",
            dtype="numeric",
            description="Total order amount"
        ),
        "status": FieldSchema(
            column="order_status",
            dtype="text",
            filterable=True,
            sortable=True
        ),
    },
    description="Customer orders"
))

Parámetros de FieldSchema:

ParámetroTipoPredeterminadoDescripción
columnstr-Nombre de columna física
dtypestr"text"Tipo de dato: text, integer, numeric, boolean, datetime
descriptionstrNoneDescripción del campo amigable para LLMs
filterableboolTruePuede usarse en filtros
sortableboolTruePuede usarse en ordenamiento

Visualización del Esquema

schema = mdbp.describe_schema()

Salida:

{
    "product": {
        "description": "Product catalog",
        "fields": {
            "id": {"type": "integer", "description": null, "filterable": true, "sortable": true},
            "name": {"type": "text", "description": null, "filterable": true, "sortable": true},
            "price": {"type": "numeric", "description": null, "filterable": true, "sortable": true}
        }
    }
}

Esta salida puede incluirse en el prompt del sistema del LLM.


Motor de Políticas

El Motor de Políticas proporciona control de acceso basado en roles.

Definición de Políticas

from mdbp.core.policy import Policy

# Analyst: read-only, sensitive fields hidden
mdbp.add_policy(Policy(
    entity="user",
    role="analyst",
    allowed_fields=["id", "name", "email", "created_at"],
    denied_fields=["password_hash", "ssn"],
    max_rows=100,
    allowed_intents=["list", "get", "count"]
))

Parámetros de Política:

ParámetroTipoPredeterminadoDescripción
entitystr-Entidad objetivo
rolestr"*"Nombre del rol ("*" = todos los roles)
allowed_fieldslistNoneCampos permitidos (None = todos)
denied_fieldslist[]Campos denegados (anula permitidos)
max_rowsint1000Máximo de filas devueltas
allowed_intentslist[list,get,count,aggregate]Operaciones permitidas
row_filterdictNoneFiltro inyectado automáticamente
masked_fieldsdict{}Campos a enmascarar en resultados (ver Enmascaramiento de Datos)

Aislamiento de Inquilinos (Tenant)

mdbp.add_policy(Policy(
    entity="order",
    role="customer",
    row_filter={"tenant_id": current_user.tenant_id}
))

Cuando esta política está activa, WHERE tenant_id = :value se agrega automáticamente a todas las consultas. El LLM no puede acceder a los datos de otros inquilinos.

Restricción Global de Intenciones

# Read-only mode
mdbp = MDBP(
    db_url="sqlite:///my.db",
    allowed_intents=["list", "get", "count", "aggregate"]
)

Esto funciona independientemente del motor de políticas. Las intenciones create, update, delete están bloqueadas globalmente.

Consulta con un Rol

result = mdbp.query({
    "intent": "list",
    "entity": "user",
    "fields": ["name", "password_hash"],
    "role": "analyst"
})
# Error: MDBP_POLICY_FIELD_DENIED

Enmascaramiento de Datos

El enmascaramiento de datos permite devolver valores enmascarados para campos sensibles en lugar de bloquear la consulta por completo. A diferencia de denied_fields (que rechaza la consulta), masked_fields permite la consulta pero enmascara los valores en la respuesta.

Uso Básico

from mdbp.core.policy import Policy

mdbp.add_policy(Policy(
    entity="customer",
    role="support",
    masked_fields={
        "email": "email",       # d***@example.com
        "phone": "last_n",      # ******4567
    },
    # name, city, etc. are returned unmasked
))

Solo los campos listados en masked_fields se enmascaran. Todos los demás campos se devuelven tal cual.

Estrategias Incorporadas

EstrategiaDescripciónEjemplo
"partial"Muestra el primer y último carácter"doruk""d***k"
"redact"Reemplaza por completo"doruk""***"
"email"Enmascara la parte local, conserva el dominio"d@x.com""d***@x.com"
"last_n"Muestra solo los últimos N caracteres (predeterminado 4)"5551234567""******4567"
"first_n"Muestra solo los primeros N caracteres (predeterminado 4)"5551234567""5551******"
"hash"Hash SHA-256 (primeros 8 caracteres)"doruk""a1b2c3d4"

Opciones de Estrategia (MaskingRule)

Usa MaskingRule para estrategias que requieren configuración:

from mdbp import MaskingRule

mdbp.add_policy(Policy(
    entity="customer",
    role="support",
    masked_fields={
        "phone": MaskingRule(strategy="last_n", options={"n": 4}),
        "credit_card": MaskingRule(strategy="last_n", options={"n": 4}),
        "email": MaskingRule(strategy="hash", options={"length": 12}),
    },
))

Funciones de Enmascaramiento Personalizadas

Proporciona cualquier invocable para control total:

mdbp.add_policy(Policy(
    entity="customer",
    role="support",
    masked_fields={
        "ssn": lambda v: "***-**-" + str(v)[-4:],
        "email": lambda v: v.split("@")[0][0] + "***@" + v.split("@")[1],
    },
))

Enmascaramiento + Campos Denegados

masked_fields y denied_fields funcionan juntos:

mdbp.add_policy(Policy(
    entity="customer",
    role="support",
    denied_fields=["password_hash"],       # query is rejected if requested
    masked_fields={"email": "email"},      # query succeeds, value is masked
))

Soporte de Archivo de Configuración

{
    "policies": [{
        "entity": "customer",
        "role": "support",
        "masked_fields": {
            "email": "email",
            "phone": {"strategy": "last_n", "options": {"n": 4}}
        }
    }]
}

Notas

  • Los valores None/null nunca se enmascaran — permanecen como None
  • Los valores numéricos se convierten a string antes de enmascarar
  • El enmascaramiento lo aplica la librería después de la ejecución de la consulta, no la IA
  • Funciona con todos los tipos de intención que devuelven datos: list, get, aggregate, mutaciones con returning

Modo de Prueba (Dry-Run)

Cualquier intención puede incluir "dry_run": true para obtener el SQL compilado y los parámetros sin ejecutar la consulta. La validación de esquema y la aplicación de políticas siguen vigentes.

result = mdbp.query({
    "intent": "list",
    "entity": "product",
    "filters": {"price__gte": 100},
    "fields": ["name", "price"],
    "dry_run": True
})

Salida:

{
    "success": true,
    "intent": "list",
    "entity": "product",
    "dry_run": true,
    "sql": "SELECT products.name, products.price FROM products WHERE products.price >= :price_1",
    "params": {"price_1": 100}
}

Útil para:

  • Depuración: Ver el SQL exacto que genera MDBP
  • Pruebas: Validar la estructura de la consulta sin tocar la base de datos
  • Flujos de aprobación: Revisar consultas antes de la ejecución

Servidor MCP

MDBP puede exponerse a Claude, Cursor y otros clientes compatibles con MCP mediante el Protocolo de Contexto de Modelos.

Inicio mediante CLI

# stdio (default) — for Claude Desktop, Cursor, etc.
mdbp-server --db-url "postgresql://user:pass@localhost/mydb"

# SSE — HTTP + Server-Sent Events at /sse
mdbp-server --db-url "postgresql://..." --transport sse --port 8000

# Streamable HTTP — newer MCP HTTP protocol at /mcp
mdbp-server --db-url "postgresql://..." --transport streamable-http --port 8000

# WebSocket — WebSocket at /ws
mdbp-server --db-url "postgresql://..." --transport websocket --port 8000

# With config file
mdbp-server --db-url "sqlite:///my.db" --config config.json --transport sse

Archivo de Configuración

{
    "entities": [
        {
            "entity": "product",
            "table": "products",
            "primary_key": "id",
            "description": "Product catalog",
            "fields": {
                "id": {"column": "id", "dtype": "integer"},
                "name": {"column": "product_name", "dtype": "text", "description": "Product name"},
                "price": {"column": "unit_price", "dtype": "numeric"}
            }
        }
    ],
    "policies": [
        {
            "entity": "product",
            "role": "viewer",
            "allowed_intents": ["list", "get", "count"],
            "max_rows": 100
        }
    ]
}

Integración con Claude Desktop

claude_desktop_config.json:

{
    "mcpServers": {
        "mdbp": {
            "command": "python",
            "args": ["-u", "path/to/server.py"],
            "env": {
                "PYTHONPATH": "path/to/mdbp/project"
            }
        }
    }
}

Uso Programático

Todos los transportes están disponibles como funciones de una línea:

from mdbp import MDBP
from mdbp.transport.server import run_sse, run_streamable_http, run_websocket, run_stdio

mdbp = MDBP(db_url="postgresql://user:pass@localhost/mydb")

run_sse(mdbp, host="0.0.0.0", port=8000)                # SSE at /sse
run_streamable_http(mdbp, host="0.0.0.0", port=8000)     # Streamable HTTP at /mcp
run_websocket(mdbp, host="0.0.0.0", port=8000)           # WebSocket at /ws
run_stdio(mdbp)                                           # stdin/stdout

Aplicaciones ASGI (para middleware personalizado o montaje):

from mdbp.transport.server import sse_app, streamable_http_app, websocket_app

app = sse_app(mdbp)                # Starlette ASGI app — /sse endpoint
app = streamable_http_app(mdbp)    # Starlette ASGI app — /mcp endpoint
app = websocket_app(mdbp)          # Starlette ASGI app — /ws endpoint

Bajo nivel (control total):

from mdbp.transport.server import create_server

server = create_server(mdbp)  # Returns mcp.server.Server — wire any transport yourself

Herramientas MCP expuestas

HerramientaDescripción
mdbp_queryEjecutar una consulta de base de datos basada en intenciones
mdbp_describe_schemaListar entidades y campos disponibles

Manejo de errores

MDBP captura todos los errores y devuelve JSON estructurado. Nunca lanza excepciones desde query().

Estructura de error

{
    "success": false,
    "intent": "list",
    "entity": "product",
    "error": {
        "code": "MDBP_SCHEMA_FIELD_NOT_FOUND",
        "message": "Field 'colour' not found on entity 'product'.",
        "details": {
            "entity": "product",
            "field": "colour",
            "available_fields": ["id", "name", "price", "color", "category_id"]
        }
    }
}

Códigos de error

Errores de esquema (MDBP_SCHEMA_*)

CódigoSignificadoDetalles
MDBP_SCHEMA_ENTITY_NOT_FOUNDLa entidad no existe en el registrolista available_entities
MDBP_SCHEMA_FIELD_NOT_FOUNDEl campo no existe en la entidadlista available_fields
MDBP_SCHEMA_ENTITY_REF_NOT_FOUNDEntidad JOIN referenciada no encontradaentity_reference, field

Errores de política (MDBP_POLICY_*)

CódigoSignificadoDetalles
MDBP_POLICY_INTENT_NOT_ALLOWEDTipo de intención no permitido para el rolintent_type, entity, role
MDBP_POLICY_FIELD_DENIEDEl campo está en la lista denied_fieldsentity, denied_fields
MDBP_POLICY_FIELD_NOT_ALLOWEDEl campo no está en la lista allowed_fieldsentity, allowed_fields

Errores de intención (MDBP_INTENT_*)

CódigoSignificadoDetalles
MDBP_INTENT_TYPE_NOT_ALLOWEDTipo de intención bloqueado globalmenteintent_type, allowed_intents
MDBP_INTENT_VALIDATION_ERROREstructura de intención inválida (Pydantic)lista de errores

Errores de consulta (MDBP_QUERY_*)

CódigoSignificadoDetalles
MDBP_QUERY_PLAN_ERRORFalló la planificación de la consulta-
MDBP_QUERY_MISSING_FIELDFalta un campo requeridointent_type, required_field
MDBP_QUERY_UNKNOWN_FILTER_OPOperador de filtro desconocidoop, supported_ops
MDBP_QUERY_UNION_REQUIRES_SUBQUERIESUNION necesita 2+ subconsultas-

Errores de conexión (MDBP_CONN_*)

CódigoSignificadoDetalles
MDBP_CONN_FAILEDFalló la conexión a la base de datos-
MDBP_CONN_EXECUTION_ERRORFalló la ejecución de la consultaoriginal_error
MDBP_NOT_FOUNDLa consulta GET no devolvió resultadosentity, id

Errores de configuración

CódigoSignificadoDetalles
MDBP_CONFIG_FILE_NOT_FOUNDEl archivo de configuración no existepath

Manejo de errores en código

from mdbp import MDBP

mdbp = MDBP(db_url="sqlite:///my.db")
result = mdbp.query({"intent": "list", "entity": "product"})

if not result["success"]:
    code = result["error"]["code"]
    if code == "MDBP_SCHEMA_ENTITY_NOT_FOUND":
        entities = result["error"]["details"]["available_entities"]
        print(f"Available entities: {entities}")

Referencia de la API

Clase MDBP

class MDBP:
    def __init__(
        self,
        db_url: str,
        auto_discover: bool = True,
        allowed_intents: list[str] | None = None,
    ) -> None

    def register_entity(schema: EntitySchema) -> None
    def add_policy(policy: Policy) -> None
    def query(raw_intent: dict | Intent) -> dict
    def describe_schema() -> dict
    def dispose() -> None
MétodoDescripción
register_entity()Registrar un esquema de entidad personalizado (anula el auto-descubrimiento)
add_policy()Agregar una política de control de acceso
query()Ejecutar el pipeline completo de MDBP. Acepta dict o Intent. Devuelve una respuesta estructurada.
describe_schema()Devolver una descripción de esquema amigable para LLM
dispose()Liberar todas las conexiones de base de datos

EntitySchema

class EntitySchema(BaseModel):
    entity: str                           # Logical entity name
    table: str                            # Physical table name
    primary_key: str = "id"               # Primary key column
    fields: dict[str, FieldSchema]        # Field definitions
    relations: dict[str, RelationSchema] = {}
    description: str | None = None

FieldSchema

class FieldSchema(BaseModel):
    column: str              # Physical column name
    dtype: str = "text"      # text, integer, numeric, boolean, datetime
    description: str | None = None
    filterable: bool = True
    sortable: bool = True

RelationSchema

class RelationSchema(BaseModel):
    target_entity: str                  # Related entity name
    join_column: str                    # Column on this entity's table
    target_column: str                  # Column on target entity's table
    relation_type: str = "many_to_one"  # one_to_one, many_to_one, one_to_many

Policy

class Policy(BaseModel):
    entity: str
    role: str = "*"
    allowed_fields: list[str] | None = None
    denied_fields: list[str] = []
    max_rows: int = 1000
    allowed_intents: list[IntentType] = [LIST, GET, COUNT, AGGREGATE]
    row_filter: dict | None = None
    masked_fields: dict[str, str | MaskingRule | Callable] = {}

IntentType Enum

class IntentType(str, Enum):
    LIST = "list"
    GET = "get"
    COUNT = "count"
    AGGREGATE = "aggregate"
    CREATE = "create"
    BATCH_CREATE = "batch_create"
    UPSERT = "upsert"
    UPDATE = "update"
    DELETE = "delete"
    UNION = "union"
    INTERSECT = "intersect"
    EXCEPT = "except"

Seguridad

Protección contra alucinaciones

Los LLM pueden generar nombres de tablas o columnas inexistentes. El registro de esquemas los detecta:

LLM: query "userz" table
MDBP: MDBP_SCHEMA_ENTITY_NOT_FOUND + list of available entities
LLM: self-corrects -> query "user" table

Prevención de inyección SQL

Todas las consultas se parametrizan mediante SQLAlchemy. Nunca se construyen cadenas SQL sin procesar.

Control de acceso

  • denied_fields: Los campos sensibles (password_hash, ssn) nunca pueden ser devueltos
  • allowed_fields: Solo los campos en la lista blanca son accesibles
  • masked_fields: Los campos sensibles se devuelven con valores enmascarados (email, phone, etc.)
  • allowed_intents: Las operaciones de escritura pueden bloquearse globalmente o por rol
  • max_rows: Limita los resultados de consultas grandes por rol
  • row_filter: Aislamiento automático de inquilinos mediante condiciones WHERE inyectadas

Pruébalo

El proyecto examples/ecommerce-mdbp-server es un ejemplo completo y funcional que puedes ejecutar localmente:

cd examples/ecommerce-mdbp-server
pip install mdbp
python setup_db.py    # Create e-commerce database with sample data
python server.py      # Start MDBP server on :8000

Este ejemplo demuestra:

  • Auto-descubrimiento (sin registro manual de esquemas)
  • Control de acceso basado en roles (customer, support, admin)
  • Aislamiento de inquilinos mediante row_filter
  • Protección de PII mediante denied_fields
  • Los 4 modos de transporte (stdio, sse, streamable-http, websocket)

Consulta el README del ejemplo para obtener detalles completos y consultas de muestra.


MDBP Logo
MDBP — Model Database Protocol