{"id":28403427,"url":"https://github.com/featureform/enrichmcp","last_synced_at":"2026-03-11T20:02:38.253Z","repository":{"id":285975378,"uuid":"958026130","full_name":"featureform/enrichmcp","owner":"featureform","description":"EnrichMCP is a python framework for building data driven MCP servers","archived":false,"fork":false,"pushed_at":"2025-11-22T01:16:52.000Z","size":344,"stargazers_count":620,"open_issues_count":6,"forks_count":28,"subscribers_count":6,"default_branch":"main","last_synced_at":"2025-11-22T03:29:13.467Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://featureform.com","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/featureform.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2025-03-31T14:20:04.000Z","updated_at":"2025-11-22T01:16:52.000Z","dependencies_parsed_at":"2025-07-09T18:12:59.599Z","dependency_job_id":"727818f2-3355-4556-aafc-77e203e04f17","html_url":"https://github.com/featureform/enrichmcp","commit_stats":null,"previous_names":["featureform/mcp-engine"],"tags_count":9,"template":false,"template_full_name":null,"purl":"pkg:github/featureform/enrichmcp","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/featureform%2Fenrichmcp","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/featureform%2Fenrichmcp/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/featureform%2Fenrichmcp/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/featureform%2Fenrichmcp/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/featureform","download_url":"https://codeload.github.com/featureform/enrichmcp/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/featureform%2Fenrichmcp/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30398177,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-11T18:46:22.935Z","status":"ssl_error","status_checked_at":"2026-03-11T18:46:17.045Z","response_time":84,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-06-01T17:36:50.656Z","updated_at":"2026-03-11T20:02:38.247Z","avatar_url":"https://github.com/featureform.png","language":"Python","funding_links":[],"categories":["Python","📚 Projects (1974 total)","🔎 Select Context"],"sub_categories":["MCP Servers","MCP Frameworks"],"readme":"# EnrichMCP\n\n**The ORM for AI Agents - Turn your data model into a semantic MCP layer**\n\n[![CI](https://github.com/featureform/enrichmcp/actions/workflows/ci.yml/badge.svg)](https://github.com/featureform/enrichmcp/actions/workflows/ci.yml)\n[![Coverage](https://codecov.io/gh/featureform/enrichmcp/branch/main/graph/badge.svg)](https://codecov.io/gh/featureform/enrichmcp)\n[![PyPI](https://img.shields.io/pypi/v/enrichmcp.svg)](https://pypi.org/project/enrichmcp/)\n[![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)\n[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/featureform/enrichmcp/blob/main/LICENSE)\n[![Docs](https://img.shields.io/badge/docs-website-blue.svg)](https://featureform.github.io/enrichmcp)\n\nEnrichMCP 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.\n\n## What is EnrichMCP?\n\nThink of it as SQLAlchemy for AI agents. EnrichMCP automatically:\n\n- **Generates typed tools** from your data models\n- **Handles relationships** between entities (users → orders → products)\n- **Provides schema discovery** so AI agents understand your data structure\n- **Validates all inputs/outputs** with Pydantic models\n- **Works with any backend** - databases, APIs, or custom logic\n\n## Installation\n\n```bash\npip install enrichmcp\n\n# With SQLAlchemy support\npip install enrichmcp[sqlalchemy]\n```\n\n## Show Me Code\n\n### Option 1: I Have SQLAlchemy Models (30 seconds)\n\nTransform your existing SQLAlchemy models into an AI-navigable API:\n\n\n```python\nfrom enrichmcp import EnrichMCP\nfrom enrichmcp.sqlalchemy import (\n    include_sqlalchemy_models,\n    sqlalchemy_lifespan,\n    EnrichSQLAlchemyMixin,\n)\nfrom sqlalchemy import ForeignKey\nfrom sqlalchemy.ext.asyncio import create_async_engine\nfrom sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship\n\nengine = create_async_engine(\"postgresql+asyncpg://user:pass@localhost/db\")\n\n\n# Add the mixin to your declarative base\nclass Base(DeclarativeBase, EnrichSQLAlchemyMixin):\n    pass\n\n\nclass User(Base):\n    \"\"\"User account.\"\"\"\n\n    __tablename__ = \"users\"\n\n    id: Mapped[int] = mapped_column(primary_key=True, info={\"description\": \"Unique user ID\"})\n    email: Mapped[str] = mapped_column(unique=True, info={\"description\": \"Email address\"})\n    status: Mapped[str] = mapped_column(default=\"active\", info={\"description\": \"Account status\"})\n    orders: Mapped[list[\"Order\"]] = relationship(\n        back_populates=\"user\", info={\"description\": \"All orders for this user\"}\n    )\n\n\nclass Order(Base):\n    \"\"\"Customer order.\"\"\"\n\n    __tablename__ = \"orders\"\n\n    id: Mapped[int] = mapped_column(primary_key=True, info={\"description\": \"Order ID\"})\n    user_id: Mapped[int] = mapped_column(\n        ForeignKey(\"users.id\"), info={\"description\": \"Owner user ID\"}\n    )\n    total: Mapped[float] = mapped_column(info={\"description\": \"Order total\"})\n    user: Mapped[User] = relationship(\n        back_populates=\"orders\", info={\"description\": \"User who placed the order\"}\n    )\n\n\n# That's it! Create your MCP app\napp = EnrichMCP(\n    \"E-commerce Data\",\n    \"API generated from SQLAlchemy models\",\n    lifespan=sqlalchemy_lifespan(Base, engine, cleanup_db_file=True),\n)\ninclude_sqlalchemy_models(app, Base)\n\nif __name__ == \"__main__\":\n    app.run()\n```\nAI agents can now:\n- `explore_data_model()` - understand your entire schema\n- `list_users(status='active')` - query with filters\n- `get_user(id=123)` - fetch specific records\n- Navigate relationships: `user.orders` → `order.user`\n\n### Option 2: I Have REST APIs (2 minutes)\n\nWrap your existing APIs with semantic understanding:\n\n```python\nfrom typing import Literal\nfrom enrichmcp import EnrichMCP, EnrichModel, Relationship\nfrom pydantic import Field\nimport httpx\n\napp = EnrichMCP(\"API Gateway\", \"Wrapper around existing REST APIs\")\nhttp = httpx.AsyncClient(base_url=\"https://api.example.com\")\n\n\n@app.entity()\nclass Customer(EnrichModel):\n    \"\"\"Customer in our CRM system.\"\"\"\n\n    id: int = Field(description=\"Unique customer ID\")\n    email: str = Field(description=\"Primary contact email\")\n    tier: Literal[\"free\", \"pro\", \"enterprise\"] = Field(description=\"Subscription tier\")\n\n    # Define navigable relationships\n    orders: list[\"Order\"] = Relationship(description=\"Customer's purchase history\")\n\n\n@app.entity()\nclass Order(EnrichModel):\n    \"\"\"Customer order from our e-commerce platform.\"\"\"\n\n    id: int = Field(description=\"Order ID\")\n    customer_id: int = Field(description=\"Associated customer\")\n    total: float = Field(description=\"Order total in USD\")\n    status: Literal[\"pending\", \"shipped\", \"delivered\"] = Field(description=\"Order status\")\n\n    customer: Customer = Relationship(description=\"Customer who placed this order\")\n\n\n# Define how to fetch data\n@app.retrieve()\nasync def get_customer(customer_id: int) -\u003e Customer:\n    \"\"\"Fetch customer from CRM API.\"\"\"\n    response = await http.get(f\"/api/customers/{customer_id}\")\n    return Customer(**response.json())\n\n\n# Define relationship resolvers\n@Customer.orders.resolver\nasync def get_customer_orders(customer_id: int) -\u003e list[Order]:\n    \"\"\"Fetch orders for a customer.\"\"\"\n    response = await http.get(f\"/api/customers/{customer_id}/orders\")\n    return [Order(**order) for order in response.json()]\n\n\n@Order.customer.resolver\nasync def get_order_customer(order_id: int) -\u003e Customer:\n    \"\"\"Fetch the customer for an order.\"\"\"\n    response = await http.get(f\"/api/orders/{order_id}/customer\")\n    return Customer(**response.json())\n\n\napp.run()\n```\n\n### Option 3: I Want Full Control (5 minutes)\n\nBuild a complete data layer with custom logic:\n\n```python\nfrom enrichmcp import EnrichMCP, EnrichModel, Relationship\nfrom datetime import datetime\nfrom decimal import Decimal\nfrom pydantic import Field\n\napp = EnrichMCP(\"Analytics Platform\", \"Custom analytics API\")\n\ndb = ...  # your database connection\n\n\n@app.entity()\nclass User(EnrichModel):\n    \"\"\"User with computed analytics fields.\"\"\"\n\n    id: int = Field(description=\"User ID\")\n    email: str = Field(description=\"Contact email\")\n    created_at: datetime = Field(description=\"Registration date\")\n\n    # Computed fields\n    lifetime_value: Decimal = Field(description=\"Total revenue from user\")\n    churn_risk: float = Field(description=\"ML-predicted churn probability 0-1\")\n\n    # Relationships\n    orders: list[\"Order\"] = Relationship(description=\"Purchase history\")\n    segments: list[\"Segment\"] = Relationship(description=\"Marketing segments\")\n\n\n@app.entity()\nclass Segment(EnrichModel):\n    \"\"\"Dynamic user segment for marketing.\"\"\"\n\n    name: str = Field(description=\"Segment name\")\n    criteria: dict = Field(description=\"Segment criteria\")\n    users: list[User] = Relationship(description=\"Users in this segment\")\n\n\n@app.entity()\nclass Order(EnrichModel):\n    \"\"\"Simplified order record.\"\"\"\n\n    id: int = Field(description=\"Order ID\")\n    user_id: int = Field(description=\"Owner user ID\")\n    total: Decimal = Field(description=\"Order total\")\n\n\n@User.orders.resolver\nasync def list_user_orders(user_id: int) -\u003e list[Order]:\n    \"\"\"Fetch orders for a user.\"\"\"\n    rows = await db.query(\n        \"SELECT * FROM orders WHERE user_id = ? ORDER BY id DESC\",\n        user_id,\n    )\n    return [Order(**row) for row in rows]\n\n\n@User.segments.resolver\nasync def list_user_segments(user_id: int) -\u003e list[Segment]:\n    \"\"\"Fetch segments that include the user.\"\"\"\n    rows = await db.query(\n        \"SELECT s.* FROM segments s JOIN user_segments us ON s.name = us.segment_name WHERE us.user_id = ?\",\n        user_id,\n    )\n    return [Segment(**row) for row in rows]\n\n\n@Segment.users.resolver\nasync def list_segment_users(name: str) -\u003e list[User]:\n    \"\"\"List users in a segment.\"\"\"\n    rows = await db.query(\n        \"SELECT u.* FROM users u JOIN user_segments us ON u.id = us.user_id WHERE us.segment_name = ?\",\n        name,\n    )\n    return [User(**row) for row in rows]\n\n\n# Complex resource with business logic\n@app.retrieve()\nasync def find_high_value_at_risk_users(\n    lifetime_value_min: Decimal = 1000, churn_risk_min: float = 0.7, limit: int = 100\n) -\u003e list[User]:\n    \"\"\"Find valuable customers likely to churn.\"\"\"\n    users = await db.query(\n        \"\"\"\n        SELECT * FROM users\n        WHERE lifetime_value \u003e= ? AND churn_risk \u003e= ?\n        ORDER BY lifetime_value DESC\n        LIMIT ?\n        \"\"\",\n        lifetime_value_min,\n        churn_risk_min,\n        limit,\n    )\n    return [User(**u) for u in users]\n\n\n# Async computed field resolver\n@User.lifetime_value.resolver\nasync def calculate_lifetime_value(user_id: int) -\u003e Decimal:\n    \"\"\"Calculate total revenue from user's orders.\"\"\"\n    total = await db.query_single(\"SELECT SUM(total) FROM orders WHERE user_id = ?\", user_id)\n    return Decimal(str(total or 0))\n\n\n# ML-powered field\n@User.churn_risk.resolver\nasync def predict_churn_risk(user_id: int) -\u003e float:\n    \"\"\"Run churn prediction model.\"\"\"\n    ctx = app.get_context()\n    features = await gather_user_features(user_id)\n    model = ctx.get(\"ml_models\")[\"churn\"]\n    return float(model.predict_proba(features)[0][1])\n\n\napp.run()\n```\n\n## Key Features\n\n### 🔍 Automatic Schema Discovery\n\nAI agents explore your entire data model with one call:\n\n```python\nschema = await explore_data_model()\n# Returns complete schema with entities, fields, types, and relationships\n```\n\n### 🔗 Relationship Navigation\n\nDefine relationships once, AI agents traverse naturally:\n\n```python\n# AI can navigate: user → orders → products → categories\nuser = await get_user(123)\norders = await user.orders()  # Automatic resolver\nproducts = await orders[0].products()\n```\n\n### 🛡️ Type Safety \u0026 Validation\n\nFull Pydantic validation on every interaction:\n\n```python\n@app.entity()\nclass Order(EnrichModel):\n    total: float = Field(ge=0, description=\"Must be positive\")\n    email: EmailStr = Field(description=\"Customer email\")\n    status: Literal[\"pending\", \"shipped\", \"delivered\"]\n```\n`describe_model()` will list these allowed values so agents know the valid options.\n\n### ✏️ Mutability \u0026 CRUD\n\nFields are immutable by default. Mark them as mutable and use\nauto-generated patch models for updates:\n\n```python\n@app.entity()\nclass Customer(EnrichModel):\n    id: int = Field(description=\"ID\")\n    email: str = Field(json_schema_extra={\"mutable\": True}, description=\"Email\")\n\n\n@app.create()\nasync def create_customer(email: str) -\u003e Customer: ...\n\n\n@app.update()\nasync def update_customer(cid: int, patch: Customer.PatchModel) -\u003e Customer: ...\n\n\n@app.delete()\nasync def delete_customer(cid: int) -\u003e bool: ...\n```\n\n### 📄 Pagination Built-in\n\nHandle large datasets elegantly:\n\n```python\nfrom enrichmcp import PageResult\n\n\n@app.retrieve()\nasync def list_orders(page: int = 1, page_size: int = 50) -\u003e PageResult[Order]:\n    orders, total = await db.get_orders_page(page, page_size)\n    return PageResult.create(items=orders, page=page, page_size=page_size, total_items=total)\n```\n\nSee the [Pagination Guide](https://featureform.github.io/enrichmcp/pagination) for more examples.\n\n### 🔐 Context \u0026 Authentication\n\nPass auth, database connections, or any context:\n\n```python\nfrom pydantic import Field\nfrom enrichmcp import EnrichModel\n\n\nclass UserProfile(EnrichModel):\n    \"\"\"User profile information.\"\"\"\n\n    user_id: int = Field(description=\"User ID\")\n    bio: str | None = Field(default=None, description=\"Short bio\")\n\n\n@app.retrieve()\nasync def get_user_profile(user_id: int) -\u003e UserProfile:\n    ctx = app.get_context()\n    # Access context provided by MCP client\n    auth_user = ctx.get(\"authenticated_user_id\")\n    if auth_user != user_id:\n        raise PermissionError(\"Can only access your own profile\")\n    return await db.get_profile(user_id)\n```\n\n### ⚡ Request Caching\n\nReduce API overhead by storing results in a per-request, per-user, or global cache:\n\n```python\n@app.retrieve()\nasync def get_customer(cid: int) -\u003e Customer:\n    ctx = app.get_context()\n\n    async def fetch() -\u003e Customer:\n        return await db.get_customer(cid)\n\n    return await ctx.cache.get_or_set(f\"customer:{cid}\", fetch)\n```\n\n### 🧭 Parameter Hints\n\nProvide examples and metadata for tool parameters using `EnrichParameter`:\n\n```python\nfrom enrichmcp import EnrichParameter\n\n\n@app.retrieve()\nasync def greet_user(name: str = EnrichParameter(description=\"user name\", examples=[\"bob\"])) -\u003e str:\n    return f\"Hello {name}\"\n```\n\nTool descriptions will include the parameter type, description, and examples.\n\n### 🌐 HTTP \u0026 SSE Support\n\nServe your API over standard output (default), SSE, or HTTP:\n\n```python\napp.run()  # stdio default\napp.run(transport=\"streamable-http\")\n```\n\n## Why EnrichMCP?\n\nEnrichMCP adds three critical layers on top of MCP:\n\n1. **Semantic Layer** - AI agents understand what your data means, not just its structure\n2. **Data Layer** - Type-safe models with validation and relationships\n3. **Control Layer** - Authentication, pagination, and business logic\n\nThe result: AI agents can work with your data as naturally as a developer using an ORM.\n\n## Server-Side LLM Sampling\n\nEnrichMCP can request language model completions through MCP's **sampling**\nfeature. Call `ctx.ask_llm()` or the `ctx.sampling()` alias from any resource\nand the connected client will choose an LLM and pay for the usage. You can tune\nbehavior using options like `model_preferences`, `allow_tools`, and\n`max_tokens`. See [docs/server_side_llm.md](docs/server_side_llm.md) for more\ndetails.\n\n## Examples\n\nCheck out the [examples directory](examples/README.md):\n\n- [hello_world](examples/hello_world) - The smallest possible EnrichMCP app\n- [hello_world_http](examples/hello_world_http) - HTTP example using streamable HTTP\n- [shop_api](examples/shop_api) - In-memory shop API with pagination and filters\n- [shop_api_sqlite](examples/shop_api_sqlite) - SQLite-backed version\n- [shop_api_gateway](examples/shop_api_gateway) - EnrichMCP as a gateway in front of FastAPI\n- [sqlalchemy_shop](examples/sqlalchemy_shop) - Auto-generated API from SQLAlchemy models\n- [mutable_crud](examples/mutable_crud) - Demonstrates mutable fields and CRUD decorators\n- [caching](examples/caching) - Demonstrates ContextCache usage\n- [basic_memory](examples/basic_memory) - Simple note-taking API using FileMemoryStore\n- [openai_chat_agent](examples/openai_chat_agent) - Interactive chat client for MCP examples\n\n## Documentation\n\n- 📖 [Full Documentation](https://featureform.github.io/enrichmcp)\n- 🚀 [Getting Started Guide](https://featureform.github.io/enrichmcp/getting-started)\n- 🔧 [API Reference](https://featureform.github.io/enrichmcp/api)\n\n## Contributing\n\nWe welcome contributions! See [CONTRIBUTING.md](CONTRIBUTING.md) for details.\n\n## Development Setup\n\nThe repository requires **Python\u0026nbsp;3.11** or newer. The Makefile includes\ncommands to create a virtual environment and run the tests:\n\n```bash\nmake setup            # create .venv and install dependencies\nsource .venv/bin/activate\nmake test             # run the test suite\n```\n\nThis installs all development extras and pre-commit hooks so commands like\n`make lint` or `make docs` work right away.\n\n## License\n\nApache 2.0 - See [LICENSE](LICENSE)\n\n---\n\nBuilt by [Featureform](https://featureform.com) • [MCP Protocol](https://modelcontextprotocol.io)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffeatureform%2Fenrichmcp","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffeatureform%2Fenrichmcp","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffeatureform%2Fenrichmcp/lists"}