Relationships¶
Overview¶
Relationships define how entities connect to each other in your data model. They enable AI agents to traverse your data graph naturally.
The Relationship Class¶
Relationship(*, description: str)
¶
Creates a relationship field on an entity.
Parameters:
- description
: Required description of the relationship
Example:
from enrichmcp import Relationship
class User(EnrichModel):
orders: list["Order"] = Relationship(description="Orders placed by this user")
Relationship Types¶
One-to-One¶
A single related entity:
@app.entity
class User(EnrichModel):
"""User account."""
id: int = Field(description="User ID")
profile: "UserProfile" = Relationship(description="User's profile information")
@app.entity
class UserProfile(EnrichModel):
"""User profile details."""
user_id: int = Field(description="User ID")
bio: str = Field(description="Biography")
One-to-Many¶
A list of related entities:
@app.entity
class Author(EnrichModel):
"""Book author."""
id: int = Field(description="Author ID")
books: list["Book"] = Relationship(description="Books by this author")
@app.entity
class Book(EnrichModel):
"""Published book."""
id: int = Field(description="Book ID")
author_id: int = Field(description="Author ID")
Defining Resolvers¶
Every relationship needs a resolver function that fetches the data.
Basic Resolver¶
@Author.books.resolver
async def get_author_books(author_id: int) -> list["Book"]:
"""Fetch all books by an author.
Args:
author_id: The ID from the parent Author entity
Returns:
List of Book entities
"""
# Your data fetching logic here
books = await fetch_books_by_author(author_id)
return [Book(**book_data) for book_data in books]
Resolver Naming¶
The resolver decorator can be used with or without a name:
# Default resolver (no name)
@Product.reviews.resolver
async def get_reviews(product_id: int) -> list["Review"]:
"""Get all reviews."""
return await fetch_reviews(product_id)
# Named resolver
@Product.reviews.resolver(name="recent")
async def get_recent_reviews(product_id: int) -> list["Review"]:
"""Get recent reviews only."""
return await fetch_recent_reviews(product_id)
Type Safety¶
Resolver return types must match the relationship field type exactly:
# Relationship expects list["Order"]
orders: list["Order"] = Relationship(...)
# ✅ Correct - matches exactly
@User.orders.resolver
async def get_orders(user_id: int) -> list["Order"]:
return []
# ❌ Wrong - different type
@User.orders.resolver
async def bad_resolver(user_id: int) -> list[dict]:
return []
# ❌ Wrong - Optional not allowed
@User.orders.resolver
async def bad_optional(user_id: int) -> list["Order"] | None:
return None
Resolver Arguments¶
Resolvers receive the parent entity's ID as their first argument:
@app.entity
class Order(EnrichModel):
"""Order entity."""
id: int = Field(description="Order ID")
customer_id: int = Field(description="Customer ID")
customer: "Customer" = Relationship(description="Customer who placed the order")
@Order.customer.resolver
async def get_order_customer(order_id: int) -> "Customer":
"""
The resolver receives order_id (from Order.id),
not customer_id, even though that's what we need.
"""
# First get the order to find customer_id
order = await fetch_order(order_id)
# Then fetch the customer
return await fetch_customer(order.customer_id)
Resolver Registration¶
Resolvers are automatically registered as MCP resources with names following the pattern:
- get_{entity}_{relationship}
for default resolvers
- get_{entity}_{relationship}_{name}
for named resolvers
Examples:
- @User.orders.resolver
→ get_user_orders
- @Product.reviews.resolver(name="recent")
→ get_product_reviews_recent
Best Practices¶
1. Handle Missing Data Gracefully¶
@Order.customer.resolver
async def get_customer(order_id: int) -> "Customer":
"""Get customer with fallback."""
customer = await fetch_customer_by_order(order_id)
if not customer:
# Return a sensible default
return Customer(id=-1, name="Unknown Customer", email="unknown@example.com")
return customer
2. Keep Resolvers Simple¶
# ✅ Good - single responsibility
@Author.books.resolver
async def get_books(author_id: int) -> list["Book"]:
"""Just fetch books."""
return await fetch_author_books(author_id)
# ❌ Avoid - doing too much
@Author.books.resolver
async def get_books_complex(author_id: int) -> list["Book"]:
"""Don't do this."""
# Fetching extra data
author = await fetch_author(author_id)
# Complex business logic
if author.is_premium:
books = await fetch_all_books(author_id)
else:
books = await fetch_published_books(author_id)
# Sorting and filtering
books = sorted(books, key=lambda b: b.date)
return books[:10]
3. Document Thoroughly¶
@Customer.orders.resolver
async def get_customer_orders(customer_id: int) -> list["Order"]:
"""Get all orders for a customer.
Retrieves the complete order history for a customer,
including cancelled and refunded orders. Orders are
returned in reverse chronological order (newest first).
Args:
customer_id: The customer's unique identifier
Returns:
List of Order objects, empty list if no orders
Note:
This includes ALL orders regardless of status
"""
return await fetch_customer_orders(customer_id)
Complete Example¶
from enrichmcp import EnrichMCP, EnrichModel, Relationship
from pydantic import Field
app = EnrichMCP("Blog API", "Blog with posts and comments")
@app.entity
class Author(EnrichModel):
"""Blog post author."""
id: int = Field(description="Author ID")
name: str = Field(description="Author name")
posts: list["Post"] = Relationship(description="Posts written by this author")
@app.entity
class Post(EnrichModel):
"""Blog post."""
id: int = Field(description="Post ID")
title: str = Field(description="Post title")
author_id: int = Field(description="Author ID")
author: "Author" = Relationship(description="Post author")
comments: list["Comment"] = Relationship(description="Comments on this post")
@app.entity
class Comment(EnrichModel):
"""Comment on a post."""
id: int = Field(description="Comment ID")
post_id: int = Field(description="Post ID")
content: str = Field(description="Comment text")
post: "Post" = Relationship(description="Post this comment belongs to")
# Define all resolvers
@Author.posts.resolver
async def get_author_posts(author_id: int) -> list["Post"]:
"""Get all posts by an author."""
# Implementation
return []
@Post.author.resolver
async def get_post_author(post_id: int) -> "Author":
"""Get the author of a post."""
# Implementation
return Author(id=1, name="Unknown")
@Post.comments.resolver
async def get_post_comments(post_id: int) -> list["Comment"]:
"""Get all comments on a post."""
# Implementation
return []
@Comment.post.resolver
async def get_comment_post(comment_id: int) -> "Post":
"""Get the post a comment belongs to."""
# Implementation
return Post(id=1, title="Unknown", author_id=1)