Skip to Content
FeaturesModulesMCP (AI Agents)MCP Tools

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.

ToolDescription
list_appsLists all installed Django apps and their models.
get_model_schemaReturns field definitions for a specific model.
list_urlsReturns 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.

ToolDescription
query_modelFilter and list records from any exposed model.
get_objectRetrieve a single record by primary key.

Management commands (tools/command_tool.py)

ToolDescription
execute_commandRun 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)

ToolDescription
get_user_infoReturns 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.

ToolDescription
aggregate_modelCOUNT, SUM, AVG, MIN, MAX on any model field.
time_seriesGroup records by time period (day, week, month).
top_valuesTop N values for a field with occurrence counts.
distributionValue 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 string
  • name is what the LLM uses in a tool call. Must be unique across the registry.
  • description is the most important field — it determines when the LLM decides to use the tool. Write it precisely.
  • input_schema is sent verbatim to the LLM as the function parameter schema in OpenAI tool format.
  • execute() must always return a str. 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)

From your project’s mcp/tools/ directory — the simplest path, and the one most projects use. Every *.py file there whose name does not start with _ is imported during AppConfig.ready(), so registering at module level is enough:

# <project_root>/mcp/tools/my_tools.py from django_cfg.modules.django_mcp.tools.base import MCPTool, tool_registry class MyCustomTool(MCPTool): name = "my_custom_tool" description = "Does something specific. Use when the user asks about X." input_schema = {"type": "object", "properties": {}} def execute(self, context, arguments) -> str: return "Result" tool_registry.register(MyCustomTool())

Discovery requires mcp/__init__.py to exist (see Configuration). Prefix a filename with _ to keep it out of discovery — useful for shared helpers.

When a tool module fails to import

A tool file with an import-time error registers nothing, so its tools are simply absent from tools/list. An agent cannot tell that absence apart from “this data does not exist” — it reads as an empty result and the investigation stops, silently, for as long as nobody calls the tool.

Discovery therefore fails loudly in development:

  • debug=True — startup raises ImproperlyConfigured listing every broken module and its exception. All failures are reported together, so you do not fix one per restart.
  • debug=False — the failure is logged at ERROR with the traceback and the module is skipped, so one bad file cannot take production down.
django.core.exceptions.ImproperlyConfigured: 1 MCP tool module(s) in /app/mcp/tools failed to import: - my_tools.py: ModuleNotFoundError: No module named 'requests'

If a file in mcp/tools/ is not meant to be a tool module, rename it with a leading underscore rather than leaving it broken.

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 only

This 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.

Last updated on