Skip to content

Commit a6c588d

Browse files
Add resources for Django project, app, and model exploration (#24)
1 parent 9f3bd4f commit a6c588d

File tree

6 files changed

+484
-10
lines changed

6 files changed

+484
-10
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@ and this project attempts to adhere to [Semantic Versioning](https://semver.org/
1818

1919
## [Unreleased]
2020

21+
### Added
22+
23+
- MCP resources for exploring the project environment, Django apps, and models without shell execution.
24+
25+
### Changed
26+
27+
- Updated server instructions to guide LLMs to use resources for project orientation before shell operations.
28+
2129
### Removed
2230

2331
- Removed redundant input field from `django_shell` tool response to reduce output verbosity.

README.md

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -158,20 +158,14 @@ Don't see your client? [Submit a PR](CONTRIBUTING.md) with setup instructions.
158158
}
159159
```
160160
161-
## Usage
161+
## Features
162162
163163
mcp-django-shell provides an MCP server with a stateful Django shell for AI assistants. It sets up Django, maintains session state between calls, and lets the AI write and execute Python code directly against your project.
164164
165-
The MCP server comes with just two tools:
166-
167-
- `django_shell` - Execute Python code in a persistent Django shell session
168-
- `django_reset` - Reset the session, clearing all variables and imports
169-
170-
Imports and variables persist between calls, so the AI can work iteratively - exploring your models, testing queries, debugging issues.
171-
172165
It wouldn't be an MCP server README without a gratuitous list of features punctuated by emojis, so:
173166

174-
- 🐚 **One tool** - `django_shell` executes Python code in your Django environment
167+
- 🐚 **Stateful shell** - `django_shell` executes Python code in your Django environment
168+
- 🔍 **Project exploration** - MCP resources for discovering apps, models, and configuration
175169
- 🔄 **Persistent state** - Imports and variables stick around between calls
176170
- 🧹 **Reset when needed** - `django_reset` clears the session when things get weird
177171
- 🚀 **Zero configuration** - No schemas, no settings, just Django
@@ -182,6 +176,25 @@ It wouldn't be an MCP server README without a gratuitous list of features punctu
182176

183177
Inspired by Armin Ronacher's [Your MCP Doesn't Need 30 Tools: It Needs Code](https://lucumr.pocoo.org/2025/8/18/code-mcps/).
184178

179+
### Resources
180+
181+
Read-only resources are provided for project exploration without executing code (note that resource support varies across MCP clients):
182+
183+
- `django://project` - Python environment and Django configuration details
184+
- `django://apps` - All installed Django applications with their models
185+
- `django://models` - Detailed model information with import paths and field types
186+
187+
The idea is to give just enough information about the project to hopefully guide the LLM assistant and prevent needless shell exploration, allowing it to get straight to work.
188+
189+
### Tools
190+
191+
Two tools handle shell operations and session management:
192+
193+
- `django_shell` - Execute Python code in a persistent Django shell session
194+
- `django_reset` - Reset the session, clearing all variables and imports
195+
196+
Imports and variables persist between calls within the shell tool, so the AI can work iteratively - exploring your models, testing queries, debugging issues.
197+
185198
## Development
186199

187200
For detailed instructions on setting up a development environment and contributing to this project, see [CONTRIBUTING.md](CONTRIBUTING.md).

src/mcp_django_shell/resources.py

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
from __future__ import annotations
2+
3+
import inspect
4+
import os
5+
import sys
6+
from pathlib import Path
7+
from typing import Any
8+
from typing import Literal
9+
10+
import django
11+
from django.apps import AppConfig
12+
from django.apps import apps
13+
from django.conf import settings
14+
from django.contrib.auth import get_user_model
15+
from django.db import models
16+
from pydantic import BaseModel
17+
from pydantic import field_serializer
18+
19+
20+
def get_source_file_path(obj: Any) -> Path:
21+
target = obj if inspect.isclass(obj) else obj.__class__
22+
try:
23+
return Path(inspect.getfile(target))
24+
except (TypeError, OSError):
25+
return Path("unknown")
26+
27+
28+
class ProjectResource(BaseModel):
29+
python: PythonResource
30+
django: DjangoResource
31+
32+
@classmethod
33+
def from_env(cls) -> ProjectResource:
34+
py = PythonResource.from_sys()
35+
dj = DjangoResource.from_django()
36+
return ProjectResource(python=py, django=dj)
37+
38+
39+
class PythonResource(BaseModel):
40+
base_prefix: Path
41+
executable: Path
42+
path: list[Path]
43+
platform: str
44+
prefix: Path
45+
version_info: tuple[
46+
int, int, int, Literal["alpha", "beta", "candidate", "final"], int
47+
]
48+
49+
@classmethod
50+
def from_sys(cls) -> PythonResource:
51+
return cls(
52+
base_prefix=Path(sys.base_prefix),
53+
executable=Path(sys.executable),
54+
path=[Path(p) for p in sys.path],
55+
platform=sys.platform,
56+
prefix=Path(sys.prefix),
57+
version_info=sys.version_info,
58+
)
59+
60+
61+
class DjangoResource(BaseModel):
62+
apps: list[str]
63+
auth_user_model: str | None
64+
base_dir: Path
65+
databases: dict[str, dict[str, str]]
66+
debug: bool
67+
settings_module: str
68+
version: tuple[int, int, int, Literal["alpha", "beta", "rc", "final"], int]
69+
70+
@classmethod
71+
def from_django(cls) -> DjangoResource:
72+
app_names = [app_config.name for app_config in apps.get_app_configs()]
73+
74+
databases = {
75+
db_alias: {
76+
"engine": db_config.get("ENGINE", ""),
77+
"name": str(db_config.get("NAME", "")),
78+
}
79+
for db_alias, db_config in settings.DATABASES.items()
80+
}
81+
82+
if "django.contrib.auth" in app_names:
83+
user_model = get_user_model()
84+
auth_user_model = f"{user_model.__module__}.{user_model.__name__}"
85+
else:
86+
auth_user_model = None
87+
88+
return cls(
89+
apps=app_names,
90+
auth_user_model=auth_user_model,
91+
base_dir=Path(getattr(settings, "BASE_DIR", Path.cwd())),
92+
databases=databases,
93+
debug=settings.DEBUG,
94+
settings_module=os.environ.get("DJANGO_SETTINGS_MODULE", ""),
95+
version=django.VERSION,
96+
)
97+
98+
99+
class AppResource(BaseModel):
100+
name: str
101+
label: str
102+
path: Path
103+
models: list[ModelResource]
104+
105+
@classmethod
106+
def from_app(cls, app: AppConfig) -> AppResource:
107+
appconfig = get_source_file_path(app)
108+
app_path = appconfig.parent if appconfig != Path("unknown") else Path("unknown")
109+
110+
app_models = (
111+
[
112+
ModelResource.from_model(model)
113+
for model in app.models.values()
114+
if not model._meta.auto_created
115+
]
116+
if app.models
117+
else []
118+
)
119+
120+
return cls(name=app.name, label=app.label, path=app_path, models=app_models)
121+
122+
@field_serializer("models")
123+
def serialize_models(self, models: list[ModelResource]) -> list[str]:
124+
return [model.model_dump()["model_class"] for model in models]
125+
126+
127+
class ModelResource(BaseModel):
128+
model_class: type[models.Model]
129+
import_path: str
130+
source_path: Path
131+
fields: dict[str, str]
132+
133+
@classmethod
134+
def from_model(cls, model: type[models.Model]):
135+
field_types = {
136+
field.name: field.__class__.__name__ for field in model._meta.fields
137+
}
138+
139+
return cls(
140+
model_class=model,
141+
import_path=f"{model.__module__}.{model.__name__}",
142+
source_path=get_source_file_path(model),
143+
fields=field_types,
144+
)
145+
146+
@field_serializer("model_class")
147+
def serialize_model_class(self, klass: type[models.Model]) -> str:
148+
return klass.__name__

src/mcp_django_shell/server.py

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,47 @@
33
import logging
44
from typing import Annotated
55

6+
from django.apps import apps
67
from fastmcp import Context
78
from fastmcp import FastMCP
89
from mcp.types import ToolAnnotations
910

1011
from .output import DjangoShellOutput
1112
from .output import ErrorOutput
13+
from .resources import AppResource
14+
from .resources import ModelResource
15+
from .resources import ProjectResource
1216
from .shell import DjangoShell
1317

1418
mcp = FastMCP(
1519
name="Django Shell",
16-
instructions="Provides a stateful Django shell environment for executing Python code, managing sessions, and exporting command history.",
20+
instructions="""Provides Django resource endpoints for project exploration and a stateful shell environment for executing Python code.
21+
22+
RESOURCES:
23+
Use resources for orientation, then use the shell for all actual work. Resources provide
24+
precise coordinates (import paths, file locations) to avoid exploration overhead.
25+
26+
- django://project - Python/Django environment metadata (versions, settings, database config)
27+
- django://apps - All Django apps with their file paths
28+
- django://models - All models with import paths and source locations
29+
30+
TOOLS:
31+
The shell maintains state between calls - imports and variables persist. Use django_reset to
32+
clear state when variables get messy or you need a fresh start.
33+
34+
- django_shell - Execute Python code in a stateful Django shell
35+
- django_reset - Reset the shell session
36+
37+
EXAMPLES:
38+
The pattern: Resource → Import Path → Shell Operation. Resources provide coordinates, shell does
39+
the work.
40+
41+
- Starting fresh? → Check django://project to understand environment and available apps
42+
- Need information about a model? → Check django://models → Get import path →
43+
`from app.models import ModelName` in django_shell
44+
- Need app structure? → Check django://apps for app labels and paths → Use paths in django_shell
45+
- Need to query data? → Get model from django://models → Import in django_shell → Run queries
46+
""",
1747
)
1848
shell = DjangoShell()
1949
logger = logging.getLogger(__name__)
@@ -100,3 +130,46 @@ async def django_reset(ctx: Context) -> str:
100130
)
101131

102132
return "Django shell session has been reset. All previously set variables and history cleared."
133+
134+
135+
@mcp.resource(
136+
"django://project",
137+
name="Django Project Information",
138+
mime_type="application/json",
139+
annotations={"readOnlyHint": True, "idempotentHint": True},
140+
)
141+
def get_project() -> ProjectResource:
142+
"""Get comprehensive project information including Python environment and Django configuration.
143+
144+
Use this to understand the project's runtime environment, installed apps, and database
145+
configuration.
146+
"""
147+
return ProjectResource.from_env()
148+
149+
150+
@mcp.resource(
151+
"django://apps",
152+
name="Installed Django Apps",
153+
mime_type="application/json",
154+
annotations={"readOnlyHint": True, "idempotentHint": True},
155+
)
156+
def get_apps() -> list[AppResource]:
157+
"""Get a list of all installed Django applications with their models.
158+
159+
Use this to explore the project structure and available models without executing code.
160+
"""
161+
return [AppResource.from_app(app) for app in apps.get_app_configs()]
162+
163+
164+
@mcp.resource(
165+
"django://models",
166+
name="Django Models",
167+
mime_type="application/json",
168+
annotations={"readOnlyHint": True, "idempotentHint": True},
169+
)
170+
def get_models() -> list[ModelResource]:
171+
"""Get detailed information about all Django models in the project.
172+
173+
Use this for quick model introspection without shell access.
174+
"""
175+
return [ModelResource.from_model(model) for model in apps.get_models()]

0 commit comments

Comments
 (0)