Skip to content

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.resolverget_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)