Skip to content

Advanced Usage

Multiple registries

The module-level default_registry is enough for most projects, but a custom ToolRegistry isolates tool sets — useful for exposing different tool sets to different agent identities, or for test isolation:

from drf_llm_gateway import ToolRegistry, expose_as_tool, to_openai_tools

internal_registry = ToolRegistry()


@expose_as_tool(actions=["list", "retrieve", "create", "update", "destroy"], registry=internal_registry)
class ArticleViewSet(viewsets.ModelViewSet):
    ...


public_registry = ToolRegistry()


@expose_as_tool(actions=["list", "retrieve"], registry=public_registry, name_prefix="public_article")
class PublicArticleViewSet(viewsets.ReadOnlyModelViewSet):
    ...


internal_tools = to_openai_tools(internal_registry)
public_tools = to_openai_tools(public_registry)

Custom lookup fields

If your viewset uses a non-default lookup_field (e.g. a slug instead of a primary key), it's picked up automatically:

class ArticleViewSet(viewsets.ModelViewSet):
    lookup_field = "slug"
    ...


@expose_as_tool(actions=["retrieve"])
class ArticleViewSet(viewsets.ModelViewSet):
    ...
>>> registry.get("article_retrieve").input_json_schema()
{'type': 'object', 'properties': {'slug': {'type': 'string'}}, 'required': ['slug']}

The lookup field's JSON Schema type is inferred from the underlying model field — a CharField-backed slug becomes {"type": "string"}, a UUIDField becomes {"type": "string", "format": "uuid"}, and an AutoField/BigAutoField primary key becomes {"type": "integer"}.

Override explicitly with @expose_as_tool(lookup_field="slug") if the viewset itself doesn't declare lookup_field/lookup_url_kwarg.

Output schemas

serializer_to_json_schema(..., mode="output") describes what a serializer returns rather than what it accepts — every field is included, and read-only fields are marked "readOnly": true. Useful for documenting a tool's return shape to an agent framework that wants it:

from drf_llm_gateway import serializer_to_json_schema

output_schema = serializer_to_json_schema(ArticleSerializer, mode="output")

Per-action serializer overrides

If a viewset's create and update actions should validate against different serializers (a common pattern when creation requires fields that are immutable afterward):

@expose_as_tool(
    actions=["create", "update"],
    serializer_classes={"create": ArticleCreateSerializer, "update": ArticleUpdateSerializer},
)
class ArticleViewSet(viewsets.ModelViewSet):
    serializer_class = ArticleSerializer  # used for list/retrieve if also exposed
    ...

Schema versioning in practice

Every ToolDefinition exposes schema_hash — a content hash of its generated input schema. Store the hash your agent framework last saw alongside its cached copy of a tool's schema, and regenerate/re-fetch whenever the hash changes:

tool = default_registry.get("article_create")
if tool.schema_hash != cached_hash:
    # re-sync: the serializer changed since we last looked
    ...

Combine with the developer-supplied version field (set via ToolDefinition(..., version="2.0.0") if constructing definitions directly, or track it in your own registration wrapper) for human-readable semantic versioning alongside the automatic hash.

Building a fully custom MCP server

to_mcp_tools() gives you the tools/list response; execute_tool() gives you tools/call dispatch. A minimal MCP server built on this package's primitives:

def handle_tools_list(request):
    return {"tools": to_mcp_tools()}


def handle_tools_call(request, user):
    name = request["params"]["name"]
    arguments = request["params"]["arguments"]
    result = execute_tool(name, arguments, user=user)
    return {"content": [{"type": "text", "text": json.dumps(result.data)}]}

See Architecture for how execute_tool maps tool-call semantics onto DRF's real request/response cycle.