MCP Tools
Tool registry
tools/base.py defines MCPTool (abstract base class) and MCPToolRegistry. A single global tool_registry instance is shared across the whole Django process.
Tools are registered in handlers/tools.py at import time. That module is imported by auto_loader.py, which is called from AppConfig.ready(). By the time Django finishes startup, all tools are registered and available to both the MCP protocol server and the admin chat agent.
Built-in tools
Introspection (tools/introspection.py)
Controlled by DjangoMCPModuleConfig.introspection. When introspection.enabled = False, these tools exist in the registry but return permission-denied responses.
| Tool | Description |
|---|---|
list_apps | Lists all installed Django apps and their models. |
get_model_schema | Returns field definitions for a specific model. |
list_urls | Returns URL patterns (requires introspection.expose_urls = True). |
Model queries (tools/model_tools.py)
Both tools check config.is_model_exposed(app_label, model_name) before executing. Fields listed in ModelMCPConfig.hidden_fields are stripped from results. max_results caps the queryset size.
| Tool | Description |
|---|---|
query_model | Filter and list records from any exposed model. |
get_object | Retrieve a single record by primary key. |
Management commands (tools/command_tool.py)
| Tool | Description |
|---|---|
execute_command | Run a Django management command. |
Only commands in CommandMCPConfig.allowed_commands are permitted. Execution is capped at timeout_seconds. Output is captured and returned as a string.
User info (tools/user_info.py)
| Tool | Description |
|---|---|
get_user_info | Returns information about the current agent session. |
Advanced analytics (agent/orm_tools.py)
These tools bypass the per-model exposure check — they are always available when MCP is enabled. They are intended for analytical queries.
| Tool | Description |
|---|---|
aggregate_model | COUNT, SUM, AVG, MIN, MAX on any model field. |
time_series | Group records by time period (day, week, month). |
top_values | Top N values for a field with occurrence counts. |
distribution | Value distribution / histogram for a numeric field. |
How tools work
Every tool is a class inheriting MCPTool:
class MCPTool(ABC):
name: str = "" # unique identifier used in tool calls
description: str = "" # shown to the LLM — affects when the tool is chosen
input_schema: Dict # JSON Schema for the arguments object
def execute(self, context: MCPContext, arguments: Dict) -> str:
... # must return a stringnameis what the LLM uses in a tool call. Must be unique across the registry.descriptionis the most important field — it determines when the LLM decides to use the tool. Write it precisely.input_schemais sent verbatim to the LLM as the function parameter schema in OpenAI tool format.execute()must always return astr. For structured data, return a JSON-encoded string. For errors, return a plain error message string — do not raise exceptions (the runner catches them and returns the message).
Adding a custom tool
Step 1: Create the tool class
# my_extension/mcp_tools.py
from django_cfg.modules.django_mcp.tools.base import MCPTool
from django_cfg.modules.django_mcp.services.context import MCPContext
class MyCustomTool(MCPTool):
name = "my_custom_tool"
description = "Does something specific. Use when the user asks about X."
input_schema = {
"type": "object",
"properties": {
"param": {
"type": "string",
"description": "The input parameter",
},
},
"required": ["param"],
}
def execute(self, context: MCPContext, arguments: dict) -> str:
param = arguments.get("param", "")
# Access config through context
if not context.config.enabled:
return "MCP is not enabled"
# Do your work
return f"Result: {param}"
my_custom_tool = MyCustomTool()Step 2: Register the tool
From a Django extension’s AppConfig.ready():
# my_extension/apps.py
from django.apps import AppConfig
class MyExtensionConfig(AppConfig):
name = "my_extension"
def ready(self):
from django_cfg.modules.django_mcp.tools.base import tool_registry
from .mcp_tools import my_custom_tool
tool_registry.register(my_custom_tool)From the core module (when modifying django_mcp itself), add to handlers/tools.py:
from django_cfg.modules.django_mcp.tools.my_module import my_custom_tool
tool_registry.register(my_custom_tool)Context differences between subsystems
Tools are called with slightly different context depending on which subsystem calls them:
# MCP protocol server — full MCPContext with request info
result = tool.execute(context, arguments)
# ^^^^^^^ MCPContext: has access_key, tenant, user info
# Admin chat agent — config only
result = tool.execute(context.config, arguments)
# ^^^^^^^^^^^^^^ DjangoMCPModuleConfig onlyThis is a known inconsistency. Tools that need request-level information (tenant, IP address, user identity) cannot be used safely from the admin chat agent until it passes a full MCPContext. Built-in tools work correctly because they only use context.config or ignore context entirely.
PII redaction
MCPTool exposes execute_with_redaction() which applies the configured redactor after execution:
mode = RedactionMode(context.config.redaction.mode.lower())
return redactor.redact_string(result, mode)Redaction is opt-in per tool — ToolsHandler calls execute() directly, not execute_with_redaction(). This is intentional: not all tools produce output that needs redaction, and the check adds overhead.
Built-in patterns cover: email addresses, phone numbers, credit card numbers, SSNs. Add custom patterns via redaction.custom_patterns in the configuration.