EnrichMCP
Turns your data model into a semantic layer for AI agents, automatically generating typed, discoverable tools with entity relationships and schema discovery.
README
EnrichMCP
The ORM for AI Agents - Turn your data model into a semantic MCP layer
EnrichMCP is a Python framework that helps AI agents understand and navigate your data. Built on MCP (Model Context Protocol), it adds a semantic layer that turns your data model into typed, discoverable tools - like an ORM for AI.
What is EnrichMCP?
Think of it as SQLAlchemy for AI agents. EnrichMCP automatically:
- Generates typed tools from your data models
- Handles relationships between entities (users → orders → products)
- Provides schema discovery so AI agents understand your data structure
- Validates all inputs/outputs with Pydantic models
- Works with any backend - databases, APIs, or custom logic
Installation
pip install enrichmcp
# With SQLAlchemy support
pip install enrichmcp[sqlalchemy]
Show Me Code
Option 1: I Have SQLAlchemy Models (30 seconds)
Transform your existing SQLAlchemy models into an AI-navigable API:
from enrichmcp import EnrichMCP
from enrichmcp.sqlalchemy import (
include_sqlalchemy_models,
sqlalchemy_lifespan,
EnrichSQLAlchemyMixin,
)
from sqlalchemy import ForeignKey
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
engine = create_async_engine("postgresql+asyncpg://user:pass@localhost/db")
# Add the mixin to your declarative base
class Base(DeclarativeBase, EnrichSQLAlchemyMixin):
pass
class User(Base):
"""User account."""
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True, info={"description": "Unique user ID"})
email: Mapped[str] = mapped_column(unique=True, info={"description": "Email address"})
status: Mapped[str] = mapped_column(default="active", info={"description": "Account status"})
orders: Mapped[list["Order"]] = relationship(
back_populates="user", info={"description": "All orders for this user"}
)
class Order(Base):
"""Customer order."""
__tablename__ = "orders"
id: Mapped[int] = mapped_column(primary_key=True, info={"description": "Order ID"})
user_id: Mapped[int] = mapped_column(
ForeignKey("users.id"), info={"description": "Owner user ID"}
)
total: Mapped[float] = mapped_column(info={"description": "Order total"})
user: Mapped[User] = relationship(
back_populates="orders", info={"description": "User who placed the order"}
)
# That's it! Create your MCP app
app = EnrichMCP(
"E-commerce Data",
"API generated from SQLAlchemy models",
lifespan=sqlalchemy_lifespan(Base, engine, cleanup_db_file=True),
)
include_sqlalchemy_models(app, Base)
if __name__ == "__main__":
app.run()
AI agents can now:
explore_data_model()- understand your entire schemalist_users(status='active')- query with filtersget_user(id=123)- fetch specific records- Navigate relationships:
user.orders→order.user
Option 2: I Have REST APIs (2 minutes)
Wrap your existing APIs with semantic understanding:
from typing import Literal
from enrichmcp import EnrichMCP, EnrichModel, Relationship
from pydantic import Field
import httpx
app = EnrichMCP("API Gateway", "Wrapper around existing REST APIs")
http = httpx.AsyncClient(base_url="https://api.example.com")
@app.entity()
class Customer(EnrichModel):
"""Customer in our CRM system."""
id: int = Field(description="Unique customer ID")
email: str = Field(description="Primary contact email")
tier: Literal["free", "pro", "enterprise"] = Field(description="Subscription tier")
# Define navigable relationships
orders: list["Order"] = Relationship(description="Customer's purchase history")
@app.entity()
class Order(EnrichModel):
"""Customer order from our e-commerce platform."""
id: int = Field(description="Order ID")
customer_id: int = Field(description="Associated customer")
total: float = Field(description="Order total in USD")
status: Literal["pending", "shipped", "delivered"] = Field(description="Order status")
customer: Customer = Relationship(description="Customer who placed this order")
# Define how to fetch data
@app.retrieve()
async def get_customer(customer_id: int) -> Customer:
"""Fetch customer from CRM API."""
response = await http.get(f"/api/customers/{customer_id}")
return Customer(**response.json())
# Define relationship resolvers
@Customer.orders.resolver
async def get_customer_orders(customer_id: int) -> list[Order]:
"""Fetch orders for a customer."""
response = await http.get(f"/api/customers/{customer_id}/orders")
return [Order(**order) for order in response.json()]
@Order.customer.resolver
async def get_order_customer(order_id: int) -> Customer:
"""Fetch the customer for an order."""
response = await http.get(f"/api/orders/{order_id}/customer")
return Customer(**response.json())
app.run()
Option 3: I Want Full Control (5 minutes)
Build a complete data layer with custom logic:
from enrichmcp import EnrichMCP, EnrichModel, Relationship
from datetime import datetime
from decimal import Decimal
from pydantic import Field
app = EnrichMCP("Analytics Platform", "Custom analytics API")
db = ... # your database connection
@app.entity()
class User(EnrichModel):
"""User with computed analytics fields."""
id: int = Field(description="User ID")
email: str = Field(description="Contact email")
created_at: datetime = Field(description="Registration date")
# Computed fields
lifetime_value: Decimal = Field(description="Total revenue from user")
churn_risk: float = Field(description="ML-predicted churn probability 0-1")
# Relationships
orders: list["Order"] = Relationship(description="Purchase history")
segments: list["Segment"] = Relationship(description="Marketing segments")
@app.entity()
class Segment(EnrichModel):
"""Dynamic user segment for marketing."""
name: str = Field(description="Segment name")
criteria: dict = Field(description="Segment criteria")
users: list[User] = Relationship(description="Users in this segment")
@app.entity()
class Order(EnrichModel):
"""Simplified order record."""
id: int = Field(description="Order ID")
user_id: int = Field(description="Owner user ID")
total: Decimal = Field(description="Order total")
@User.orders.resolver
async def list_user_orders(user_id: int) -> list[Order]:
"""Fetch orders for a user."""
rows = await db.query(
"SELECT * FROM orders WHERE user_id = ? ORDER BY id DESC",
user_id,
)
return [Order(**row) for row in rows]
@User.segments.resolver
async def list_user_segments(user_id: int) -> list[Segment]:
"""Fetch segments that include the user."""
rows = await db.query(
"SELECT s.* FROM segments s JOIN user_segments us ON s.name = us.segment_name WHERE us.user_id = ?",
user_id,
)
return [Segment(**row) for row in rows]
@Segment.users.resolver
async def list_segment_users(name: str) -> list[User]:
"""List users in a segment."""
rows = await db.query(
"SELECT u.* FROM users u JOIN user_segments us ON u.id = us.user_id WHERE us.segment_name = ?",
name,
)
return [User(**row) for row in rows]
# Complex resource with business logic
@app.retrieve()
async def find_high_value_at_risk_users(
lifetime_value_min: Decimal = 1000, churn_risk_min: float = 0.7, limit: int = 100
) -> list[User]:
"""Find valuable customers likely to churn."""
users = await db.query(
"""
SELECT * FROM users
WHERE lifetime_value >= ? AND churn_risk >= ?
ORDER BY lifetime_value DESC
LIMIT ?
""",
lifetime_value_min,
churn_risk_min,
limit,
)
return [User(**u) for u in users]
# Async computed field resolver
@User.lifetime_value.resolver
async def calculate_lifetime_value(user_id: int) -> Decimal:
"""Calculate total revenue from user's orders."""
total = await db.query_single("SELECT SUM(total) FROM orders WHERE user_id = ?", user_id)
return Decimal(str(total or 0))
# ML-powered field
@User.churn_risk.resolver
async def predict_churn_risk(user_id: int) -> float:
"""Run churn prediction model."""
ctx = app.get_context()
features = await gather_user_features(user_id)
model = ctx.get("ml_models")["churn"]
return float(model.predict_proba(features)[0][1])
app.run()
Key Features
🔍 Automatic Schema Discovery
AI agents explore your entire data model with one call:
schema = await explore_data_model()
# Returns complete schema with entities, fields, types, and relationships
🔗 Relationship Navigation
Define relationships once, AI agents traverse naturally:
# AI can navigate: user → orders → products → categories
user = await get_user(123)
orders = await user.orders() # Automatic resolver
products = await orders[0].products()
🛡️ Type Safety & Validation
Full Pydantic validation on every interaction:
@app.entity()
class Order(EnrichModel):
total: float = Field(ge=0, description="Must be positive")
email: EmailStr = Field(description="Customer email")
status: Literal["pending", "shipped", "delivered"]
describe_model() will list these allowed values so agents know the valid options.
✏️ Mutability & CRUD
Fields are immutable by default. Mark them as mutable and use auto-generated patch models for updates:
@app.entity()
class Customer(EnrichModel):
id: int = Field(description="ID")
email: str = Field(json_schema_extra={"mutable": True}, description="Email")
@app.create()
async def create_customer(email: str) -> Customer: ...
@app.update()
async def update_customer(cid: int, patch: Customer.PatchModel) -> Customer: ...
@app.delete()
async def delete_customer(cid: int) -> bool: ...
📄 Pagination Built-in
Handle large datasets elegantly:
from enrichmcp import PageResult
@app.retrieve()
async def list_orders(page: int = 1, page_size: int = 50) -> PageResult[Order]:
orders, total = await db.get_orders_page(page, page_size)
return PageResult.create(items=orders, page=page, page_size=page_size, total_items=total)
See the Pagination Guide for more examples.
🔐 Context & Authentication
Pass auth, database connections, or any context:
from pydantic import Field
from enrichmcp import EnrichModel
class UserProfile(EnrichModel):
"""User profile information."""
user_id: int = Field(description="User ID")
bio: str | None = Field(default=None, description="Short bio")
@app.retrieve()
async def get_user_profile(user_id: int) -> UserProfile:
ctx = app.get_context()
# Access context provided by MCP client
auth_user = ctx.get("authenticated_user_id")
if auth_user != user_id:
raise PermissionError("Can only access your own profile")
return await db.get_profile(user_id)
⚡ Request Caching
Reduce API overhead by storing results in a per-request, per-user, or global cache:
@app.retrieve()
async def get_customer(cid: int) -> Customer:
ctx = app.get_context()
async def fetch() -> Customer:
return await db.get_customer(cid)
return await ctx.cache.get_or_set(f"customer:{cid}", fetch)
🧭 Parameter Hints
Provide examples and metadata for tool parameters using EnrichParameter:
from enrichmcp import EnrichParameter
@app.retrieve()
async def greet_user(name: str = EnrichParameter(description="user name", examples=["bob"])) -> str:
return f"Hello {name}"
Tool descriptions will include the parameter type, description, and examples.
🌐 HTTP & SSE Support
Serve your API over standard output (default), SSE, or HTTP:
app.run() # stdio default
app.run(transport="streamable-http")
Why EnrichMCP?
EnrichMCP adds three critical layers on top of MCP:
- Semantic Layer - AI agents understand what your data means, not just its structure
- Data Layer - Type-safe models with validation and relationships
- Control Layer - Authentication, pagination, and business logic
The result: AI agents can work with your data as naturally as a developer using an ORM.
Server-Side LLM Sampling
EnrichMCP can request language model completions through MCP's sampling
feature. Call ctx.ask_llm() or the ctx.sampling() alias from any resource
and the connected client will choose an LLM and pay for the usage. You can tune
behavior using options like model_preferences, allow_tools, and
max_tokens. See docs/server_side_llm.md for more
details.
Examples
Check out the examples directory:
- hello_world - The smallest possible EnrichMCP app
- hello_world_http - HTTP example using streamable HTTP
- shop_api - In-memory shop API with pagination and filters
- shop_api_sqlite - SQLite-backed version
- shop_api_gateway - EnrichMCP as a gateway in front of FastAPI
- sqlalchemy_shop - Auto-generated API from SQLAlchemy models
- mutable_crud - Demonstrates mutable fields and CRUD decorators
- caching - Demonstrates ContextCache usage
- basic_memory - Simple note-taking API using FileMemoryStore
- openai_chat_agent - Interactive chat client for MCP examples
Documentation
Contributing
We welcome contributions! See CONTRIBUTING.md for details.
Development Setup
The repository requires Python 3.11 or newer. The Makefile includes commands to create a virtual environment and run the tests:
make setup # create .venv and install dependencies
source .venv/bin/activate
make test # run the test suite
This installs all development extras and pre-commit hooks so commands like
make lint or make docs work right away.
License
Apache 2.0 - See LICENSE
Built by Featureform • MCP Protocol
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。