Skip to content

Commit d69911b

Browse files
simba-gitSimba Khadder
andauthored
Migration to FastMCP 2 (#162)
--------- Co-authored-by: Simba Khadder <[email protected]>
1 parent 6cdc517 commit d69911b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

78 files changed

+1381
-1050
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ local_settings.py
6161
db.sqlite3
6262
db.sqlite3-journal
6363

64+
# Example database files
65+
shop.db
66+
6467
# Flask stuff:
6568
instance/
6669
.webassets-cache

README.md

Lines changed: 69 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -39,17 +39,23 @@ Transform your existing SQLAlchemy models into an AI-navigable API:
3939

4040
```python
4141
from enrichmcp import EnrichMCP
42-
from enrichmcp.sqlalchemy import include_sqlalchemy_models, sqlalchemy_lifespan, EnrichSQLAlchemyMixin
42+
from enrichmcp.sqlalchemy import (
43+
include_sqlalchemy_models,
44+
sqlalchemy_lifespan,
45+
EnrichSQLAlchemyMixin,
46+
)
4347
from sqlalchemy import ForeignKey
4448
from sqlalchemy.ext.asyncio import create_async_engine
4549
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
4650

4751
engine = create_async_engine("postgresql+asyncpg://user:pass@localhost/db")
4852

53+
4954
# Add the mixin to your declarative base
5055
class Base(DeclarativeBase, EnrichSQLAlchemyMixin):
5156
pass
5257

58+
5359
class User(Base):
5460
"""User account."""
5561

@@ -58,17 +64,25 @@ class User(Base):
5864
id: Mapped[int] = mapped_column(primary_key=True, info={"description": "Unique user ID"})
5965
email: Mapped[str] = mapped_column(unique=True, info={"description": "Email address"})
6066
status: Mapped[str] = mapped_column(default="active", info={"description": "Account status"})
61-
orders: Mapped[list["Order"]] = relationship(back_populates="user", info={"description": "All orders for this user"})
67+
orders: Mapped[list["Order"]] = relationship(
68+
back_populates="user", info={"description": "All orders for this user"}
69+
)
70+
6271

6372
class Order(Base):
6473
"""Customer order."""
6574

6675
__tablename__ = "orders"
6776

6877
id: Mapped[int] = mapped_column(primary_key=True, info={"description": "Order ID"})
69-
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), info={"description": "Owner user ID"})
78+
user_id: Mapped[int] = mapped_column(
79+
ForeignKey("users.id"), info={"description": "Owner user ID"}
80+
)
7081
total: Mapped[float] = mapped_column(info={"description": "Order total"})
71-
user: Mapped[User] = relationship(back_populates="orders", info={"description": "User who placed the order"})
82+
user: Mapped[User] = relationship(
83+
back_populates="orders", info={"description": "User who placed the order"}
84+
)
85+
7286

7387
# That's it! Create your MCP app
7488
app = EnrichMCP(
@@ -100,52 +114,54 @@ import httpx
100114
app = EnrichMCP("API Gateway", "Wrapper around existing REST APIs")
101115
http = httpx.AsyncClient(base_url="https://api.example.com")
102116

103-
@app.entity
117+
118+
@app.entity()
104119
class Customer(EnrichModel):
105120
"""Customer in our CRM system."""
106121

107122
id: int = Field(description="Unique customer ID")
108123
email: str = Field(description="Primary contact email")
109-
tier: Literal["free", "pro", "enterprise"] = Field(
110-
description="Subscription tier"
111-
)
124+
tier: Literal["free", "pro", "enterprise"] = Field(description="Subscription tier")
112125

113126
# Define navigable relationships
114127
orders: list["Order"] = Relationship(description="Customer's purchase history")
115128

116-
@app.entity
129+
130+
@app.entity()
117131
class Order(EnrichModel):
118132
"""Customer order from our e-commerce platform."""
119133

120134
id: int = Field(description="Order ID")
121135
customer_id: int = Field(description="Associated customer")
122136
total: float = Field(description="Order total in USD")
123-
status: Literal["pending", "shipped", "delivered"] = Field(
124-
description="Order status"
125-
)
137+
status: Literal["pending", "shipped", "delivered"] = Field(description="Order status")
126138

127139
customer: Customer = Relationship(description="Customer who placed this order")
128140

141+
129142
# Define how to fetch data
130-
@app.retrieve
143+
@app.retrieve()
131144
async def get_customer(customer_id: int) -> Customer:
132145
"""Fetch customer from CRM API."""
133146
response = await http.get(f"/api/customers/{customer_id}")
134147
return Customer(**response.json())
135148

149+
136150
# Define relationship resolvers
137151
@Customer.orders.resolver
138152
async def get_customer_orders(customer_id: int) -> list[Order]:
139153
"""Fetch orders for a customer."""
140154
response = await http.get(f"/api/customers/{customer_id}/orders")
141155
return [Order(**order) for order in response.json()]
142156

157+
143158
@Order.customer.resolver
144159
async def get_order_customer(order_id: int) -> Customer:
145160
"""Fetch the customer for an order."""
146161
response = await http.get(f"/api/orders/{order_id}/customer")
147162
return Customer(**response.json())
148163

164+
149165
app.run()
150166
```
151167

@@ -163,7 +179,8 @@ app = EnrichMCP("Analytics Platform", "Custom analytics API")
163179

164180
db = ... # your database connection
165181

166-
@app.entity
182+
183+
@app.entity()
167184
class User(EnrichModel):
168185
"""User with computed analytics fields."""
169186

@@ -179,7 +196,8 @@ class User(EnrichModel):
179196
orders: list["Order"] = Relationship(description="Purchase history")
180197
segments: list["Segment"] = Relationship(description="Marketing segments")
181198

182-
@app.entity
199+
200+
@app.entity()
183201
class Segment(EnrichModel):
184202
"""Dynamic user segment for marketing."""
185203

@@ -188,14 +206,15 @@ class Segment(EnrichModel):
188206
users: list[User] = Relationship(description="Users in this segment")
189207

190208

191-
@app.entity
209+
@app.entity()
192210
class Order(EnrichModel):
193211
"""Simplified order record."""
194212

195213
id: int = Field(description="Order ID")
196214
user_id: int = Field(description="Owner user ID")
197215
total: Decimal = Field(description="Order total")
198216

217+
199218
@User.orders.resolver
200219
async def list_user_orders(user_id: int) -> list[Order]:
201220
"""Fetch orders for a user."""
@@ -205,6 +224,7 @@ async def list_user_orders(user_id: int) -> list[Order]:
205224
)
206225
return [Order(**row) for row in rows]
207226

227+
208228
@User.segments.resolver
209229
async def list_user_segments(user_id: int) -> list[Segment]:
210230
"""Fetch segments that include the user."""
@@ -214,6 +234,7 @@ async def list_user_segments(user_id: int) -> list[Segment]:
214234
)
215235
return [Segment(**row) for row in rows]
216236

237+
217238
@Segment.users.resolver
218239
async def list_segment_users(name: str) -> list[User]:
219240
"""List users in a segment."""
@@ -223,12 +244,11 @@ async def list_segment_users(name: str) -> list[User]:
223244
)
224245
return [User(**row) for row in rows]
225246

247+
226248
# Complex resource with business logic
227-
@app.retrieve
249+
@app.retrieve()
228250
async def find_high_value_at_risk_users(
229-
lifetime_value_min: Decimal = 1000,
230-
churn_risk_min: float = 0.7,
231-
limit: int = 100
251+
lifetime_value_min: Decimal = 1000, churn_risk_min: float = 0.7, limit: int = 100
232252
) -> list[User]:
233253
"""Find valuable customers likely to churn."""
234254
users = await db.query(
@@ -238,20 +258,21 @@ async def find_high_value_at_risk_users(
238258
ORDER BY lifetime_value DESC
239259
LIMIT ?
240260
""",
241-
lifetime_value_min, churn_risk_min, limit
261+
lifetime_value_min,
262+
churn_risk_min,
263+
limit,
242264
)
243265
return [User(**u) for u in users]
244266

267+
245268
# Async computed field resolver
246269
@User.lifetime_value.resolver
247270
async def calculate_lifetime_value(user_id: int) -> Decimal:
248271
"""Calculate total revenue from user's orders."""
249-
total = await db.query_single(
250-
"SELECT SUM(total) FROM orders WHERE user_id = ?",
251-
user_id
252-
)
272+
total = await db.query_single("SELECT SUM(total) FROM orders WHERE user_id = ?", user_id)
253273
return Decimal(str(total or 0))
254274

275+
255276
# ML-powered field
256277
@User.churn_risk.resolver
257278
async def predict_churn_risk(user_id: int) -> float:
@@ -261,6 +282,7 @@ async def predict_churn_risk(user_id: int) -> float:
261282
model = ctx.get("ml_models")["churn"]
262283
return float(model.predict_proba(features)[0][1])
263284

285+
264286
app.run()
265287
```
266288

@@ -291,7 +313,7 @@ products = await orders[0].products()
291313
Full Pydantic validation on every interaction:
292314

293315
```python
294-
@app.entity
316+
@app.entity()
295317
class Order(EnrichModel):
296318
total: float = Field(ge=0, description="Must be positive")
297319
email: EmailStr = Field(description="Customer email")
@@ -305,22 +327,22 @@ Fields are immutable by default. Mark them as mutable and use
305327
auto-generated patch models for updates:
306328

307329
```python
308-
@app.entity
330+
@app.entity()
309331
class Customer(EnrichModel):
310332
id: int = Field(description="ID")
311333
email: str = Field(json_schema_extra={"mutable": True}, description="Email")
312334

313-
@app.create
314-
async def create_customer(email: str) -> Customer:
315-
...
316335

317-
@app.update
318-
async def update_customer(cid: int, patch: Customer.PatchModel) -> Customer:
319-
...
336+
@app.create()
337+
async def create_customer(email: str) -> Customer: ...
338+
320339

321-
@app.delete
322-
async def delete_customer(cid: int) -> bool:
323-
...
340+
@app.update()
341+
async def update_customer(cid: int, patch: Customer.PatchModel) -> Customer: ...
342+
343+
344+
@app.delete()
345+
async def delete_customer(cid: int) -> bool: ...
324346
```
325347

326348
### 📄 Pagination Built-in
@@ -330,18 +352,11 @@ Handle large datasets elegantly:
330352
```python
331353
from enrichmcp import PageResult
332354

333-
@app.retrieve
334-
async def list_orders(
335-
page: int = 1,
336-
page_size: int = 50
337-
) -> PageResult[Order]:
355+
356+
@app.retrieve()
357+
async def list_orders(page: int = 1, page_size: int = 50) -> PageResult[Order]:
338358
orders, total = await db.get_orders_page(page, page_size)
339-
return PageResult.create(
340-
items=orders,
341-
page=page,
342-
page_size=page_size,
343-
total_items=total
344-
)
359+
return PageResult.create(items=orders, page=page, page_size=page_size, total_items=total)
345360
```
346361

347362
See the [Pagination Guide](https://featureform.github.io/enrichmcp/pagination) for more examples.
@@ -354,13 +369,15 @@ Pass auth, database connections, or any context:
354369
from pydantic import Field
355370
from enrichmcp import EnrichModel
356371

372+
357373
class UserProfile(EnrichModel):
358374
"""User profile information."""
359375

360376
user_id: int = Field(description="User ID")
361377
bio: str | None = Field(default=None, description="Short bio")
362378

363-
@app.retrieve
379+
380+
@app.retrieve()
364381
async def get_user_profile(user_id: int) -> UserProfile:
365382
ctx = app.get_context()
366383
# Access context provided by MCP client
@@ -375,10 +392,10 @@ async def get_user_profile(user_id: int) -> UserProfile:
375392
Reduce API overhead by storing results in a per-request, per-user, or global cache:
376393

377394
```python
378-
379-
@app.retrieve
395+
@app.retrieve()
380396
async def get_customer(cid: int) -> Customer:
381397
ctx = app.get_context()
398+
382399
async def fetch() -> Customer:
383400
return await db.get_customer(cid)
384401

@@ -392,7 +409,8 @@ Provide examples and metadata for tool parameters using `EnrichParameter`:
392409
```python
393410
from enrichmcp import EnrichParameter
394411

395-
@app.retrieve
412+
413+
@app.retrieve()
396414
async def greet_user(name: str = EnrichParameter(description="user name", examples=["bob"])) -> str:
397415
return f"Hello {name}"
398416
```

backup/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
"""
2-
enrichmcp - A Python-first Framework for Exposing Structured Data to AI Agents.
1+
"""enrichmcp - A Python-first Framework for Exposing Structured Data to AI Agents.
32
43
Powered by Pydantic & the Model-Context Protocol.
54
"""

docs/api.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ from enrichmcp import EnrichModel
2121
from pydantic import Field
2222

2323

24-
@app.entity
24+
@app.entity()
2525
class User(EnrichModel):
2626
id: int = Field(description="User ID")
2727
name: str = Field(description="Username")
@@ -38,8 +38,8 @@ class User(EnrichModel):
3838
orders: list["Order"] = Relationship(description="User's orders")
3939
```
4040

41-
### [EnrichContext](api/context.md)
42-
Context object with request scoped utilities including caching.
41+
### [Context](api/context.md)
42+
FastMCP's Context object with request scoped utilities including logging, progress reporting, and lifespan context access.
4343

4444
### [EnrichParameter](api/parameter.md)
4545
Attach metadata like descriptions and examples to function parameters.
@@ -53,10 +53,10 @@ Currently uses standard Python exceptions and Pydantic validation.
5353
## Key Concepts
5454

5555
### Entity Registration
56-
Entities must be registered with the app using the `@app.entity` decorator:
56+
Entities must be registered with the app using the `@app.entity()` decorator:
5757

5858
```python
59-
@app.entity
59+
@app.entity()
6060
class Product(EnrichModel):
6161
"""Product in our catalog."""
6262

@@ -78,7 +78,7 @@ async def get_user_orders(user_id: int) -> list["Order"]:
7878
Resources are the entry points for AI agents:
7979

8080
```python
81-
@app.retrieve
81+
@app.retrieve()
8282
async def list_users() -> list[User]:
8383
"""List all users in the system."""
8484
return fetch_all_users()

0 commit comments

Comments
 (0)