#
tokens: 17103/50000 34/34 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .gitignore
├── docker
│   └── entrypoint.sh
├── docker-commands.json
├── Dockerfile
├── migationsenv
│   ├── bin
│   │   ├── activate
│   │   ├── activate.csh
│   │   ├── activate.fish
│   │   ├── Activate.ps1
│   │   ├── coverage
│   │   ├── coverage-3.12
│   │   ├── coverage3
│   │   ├── django-admin
│   │   ├── dotenv
│   │   ├── httpx
│   │   ├── mcp
│   │   ├── pip
│   │   ├── pip3
│   │   ├── pip3.12
│   │   ├── py.test
│   │   ├── pytest
│   │   ├── python
│   │   ├── python3
│   │   ├── python3.12
│   │   ├── sqlformat
│   │   └── uvicorn
│   └── pyvenv.cfg
├── migrations_mcp
│   ├── __init__.py
│   ├── handlers
│   │   ├── __init__.py
│   │   └── utils.py
│   ├── service.py
│   └── tests
│       ├── __init__.py
│       └── test_handlers.py
├── pytest.ini
├── README.md
├── requirements.txt
├── setup.py
└── testproject
    ├── manage 2.py
    ├── manage.py
    ├── testapp
    │   ├── __init__.py
    │   ├── admin.py
    │   ├── apps.py
    │   ├── migrations
    │   │   ├── __init__.py
    │   │   └── 0001_initial.py
    │   ├── models.py
    │   ├── tests
    │   │   ├── __init__.py
    │   │   ├── conftest.py
    │   │   └── test_migrations.py
    │   ├── tests.py
    │   └── views.py
    └── testproject
        ├── __init__.py
        ├── asgi.py
        ├── settings.py
        ├── urls.py
        └── wsgi.py
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
  1 | # Python
  2 | __pycache__/
  3 | *.py[cod]
  4 | *$py.class
  5 | *.so
  6 | .Python
  7 | build/
  8 | develop-eggs/
  9 | dist/
 10 | downloads/
 11 | eggs/
 12 | .eggs/
 13 | lib/
 14 | lib64/
 15 | parts/
 16 | sdist/
 17 | var/
 18 | wheels/
 19 | *.egg-info/
 20 | .installed.cfg
 21 | *.egg
 22 | MANIFEST
 23 | 
 24 | # Virtual Environment
 25 | venv/
 26 | env/
 27 | ENV/
 28 | .env
 29 | .venv
 30 | env.bak/
 31 | venv.bak/
 32 | 
 33 | # Django
 34 | *.log
 35 | local_settings.py
 36 | db.sqlite3
 37 | db.sqlite3-journal
 38 | media/
 39 | staticfiles/
 40 | 
 41 | # IDE
 42 | .idea/
 43 | .vscode/
 44 | *.swp
 45 | *.swo
 46 | *~
 47 | .project
 48 | .pydevproject
 49 | .settings/
 50 | 
 51 | # Testing
 52 | .coverage
 53 | .coverage.*
 54 | .cache
 55 | nosetests.xml
 56 | coverage.xml
 57 | *.cover
 58 | .hypothesis/
 59 | .pytest_cache/
 60 | htmlcov/
 61 | 
 62 | # Distribution / packaging
 63 | .Python
 64 | env/
 65 | build/
 66 | develop-eggs/
 67 | dist/
 68 | downloads/
 69 | eggs/
 70 | .eggs/
 71 | lib/
 72 | lib64/
 73 | parts/
 74 | sdist/
 75 | var/
 76 | wheels/
 77 | *.egg-info/
 78 | .installed.cfg
 79 | *.egg
 80 | 
 81 | # Docker
 82 | .docker/
 83 | docker-compose.override.yml
 84 | 
 85 | # Logs
 86 | logs/
 87 | *.log
 88 | npm-debug.log*
 89 | yarn-debug.log*
 90 | yarn-error.log*
 91 | 
 92 | # System Files
 93 | .DS_Store
 94 | .DS_Store?
 95 | ._*
 96 | .Spotlight-V100
 97 | .Trashes
 98 | ehthumbs.db
 99 | Thumbs.db
100 | 
101 | # Environment Variables
102 | .env
103 | .env.local
104 | .env.*.local
105 | 
106 | # Migration files (optional, uncomment if you want to ignore migrations)
107 | # */migrations/*.py
108 | # !*/migrations/__init__.py
109 | 
110 | # Documentation
111 | /site
112 | docs/_build/
113 | 
114 | # mypy
115 | .mypy_cache/
116 | .dmypy.json
117 | dmypy.json
118 | 
119 | # Jupyter Notebook
120 | .ipynb_checkpoints
121 | 
122 | # pyenv
123 | .python-version
124 | 
125 | # celery
126 | celerybeat-schedule
127 | celerybeat.pid
128 | 
129 | # SageMath parsed files
130 | *.sage.py
131 | 
132 | # Spyder project settings
133 | .spyderproject
134 | .spyproject
135 | 
136 | # Rope project settings
137 | .ropeproject
138 | 
139 | # mkdocs documentation
140 | /site
141 | 
142 | # Temporary files
143 | *.bak
144 | *.tmp
145 | *.temp 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | [![MseeP.ai Security Assessment Badge](https://mseep.net/pr/mrrobotke-django-migrations-mcp-badge.png)](https://mseep.ai/app/mrrobotke-django-migrations-mcp)
  2 | 
  3 | # Django Migrations MCP Service
  4 | 
  5 | A Model Context Protocol (MCP) service for managing Django migrations in distributed environments. This service wraps Django's migration commands and exposes them as MCP endpoints, making it easy to manage migrations across multiple services and integrate with CI/CD pipelines.
  6 | 
  7 | ## Features
  8 | 
  9 | - Check migration status (equivalent to `showmigrations`)
 10 | - Create new migrations with validation (equivalent to `makemigrations`)
 11 | - Apply migrations with safety checks (equivalent to `migrate`)
 12 | - Additional validation and safety checks:
 13 |   - Sequential migration order verification
 14 |   - Conflict detection
 15 |   - Dependency validation
 16 |   - Safety analysis of migration operations
 17 | 
 18 | ## Installation
 19 | 
 20 | ### Local Development
 21 | 
 22 | 1. Clone the repository:
 23 | ```bash
 24 | git clone https://github.com/mrrobotke/django-migrations-mcp.git
 25 | cd django-migrations-mcp
 26 | ```
 27 | 
 28 | 2. Install dependencies:
 29 | ```bash
 30 | pip install -r requirements.txt
 31 | ```
 32 | 
 33 | ## Configuration
 34 | 
 35 | Set the following environment variables:
 36 | 
 37 | ```bash
 38 | export DJANGO_SETTINGS_MODULE="your_project.settings"
 39 | export MCP_SERVICE_PORT=8000  # Optional, defaults to 8000
 40 | ```
 41 | 
 42 | ## Usage
 43 | 
 44 | ### Running the Service
 45 | 
 46 | 1. Directly with Python:
 47 | ```bash
 48 | python -m migrations_mcp.service
 49 | ```
 50 | 
 51 | 2. Using Docker:
 52 | ```bash
 53 | docker build -t django-migrations-mcp .
 54 | docker run -e DJANGO_SETTINGS_MODULE=your_project.settings \
 55 |           -v /path/to/your/django/project:/app/project \
 56 |           -p 8000:8000 \
 57 |           django-migrations-mcp
 58 | ```
 59 | 
 60 | ### MCP Endpoints
 61 | 
 62 | 1. Show Migrations:
 63 | ```python
 64 | from mcp import MCPClient
 65 | 
 66 | client = MCPClient()
 67 | migrations = await client.call("show_migrations")
 68 | ```
 69 | 
 70 | 2. Make Migrations:
 71 | ```python
 72 | result = await client.call("make_migrations", {
 73 |     "app_labels": ["myapp"],  # Optional
 74 |     "dry_run": True  # Optional
 75 | })
 76 | ```
 77 | 
 78 | 3. Apply Migrations:
 79 | ```python
 80 | result = await client.call("migrate", {
 81 |     "app_label": "myapp",  # Optional
 82 |     "migration_name": "0001",  # Optional
 83 |     "fake": False,  # Optional
 84 |     "plan": True  # Optional
 85 | })
 86 | ```
 87 | 
 88 | ## CI/CD Integration
 89 | 
 90 | Example GitHub Actions workflow:
 91 | 
 92 | ```yaml
 93 | name: Django Migrations Check
 94 | 
 95 | on:
 96 |   pull_request:
 97 |     paths:
 98 |       - '*/migrations/*.py'
 99 |       - '*/models.py'
100 | 
101 | jobs:
102 |   check-migrations:
103 |     runs-on: ubuntu-latest
104 |     
105 |     steps:
106 |     - uses: actions/checkout@v2
107 |     
108 |     - name: Set up Python
109 |       uses: actions/setup-python@v2
110 |       with:
111 |         python-version: '3.11'
112 |     
113 |     - name: Install dependencies
114 |       run: |
115 |         pip install -r requirements.txt
116 |     
117 |     - name: Start MCP service
118 |       run: |
119 |         python -m migrations_mcp.service &
120 |     
121 |     - name: Check migrations
122 |       run: |
123 |         python ci/check_migrations.py
124 | ```
125 | 
126 | Example check_migrations.py script:
127 | 
128 | ```python
129 | import asyncio
130 | from mcp import MCPClient
131 | 
132 | async def check_migrations():
133 |     client = MCPClient()
134 |     
135 |     # Check current status
136 |     migrations = await client.call("show_migrations")
137 |     
138 |     # Try making migrations
139 |     result = await client.call("make_migrations", {"dry_run": True})
140 |     if not result.success:
141 |         print(f"Error: {result.message}")
142 |         exit(1)
143 |     
144 |     print("Migration check passed!")
145 | 
146 | if __name__ == "__main__":
147 |     asyncio.run(check_migrations())
148 | ```
149 | 
150 | ## Development
151 | 
152 | ### Running Tests
153 | 
154 | ```bash
155 | pytest migrations_mcp/tests/
156 | ```
157 | 
158 | ### Code Style
159 | 
160 | The project follows PEP 8 guidelines. Format your code using:
161 | 
162 | ```bash
163 | black migrations_mcp/
164 | isort migrations_mcp/
165 | ```
166 | 
167 | ## License
168 | 
169 | MIT License. See LICENSE file for details.
170 | 
171 | ## Contributing
172 | 
173 | 1. Fork the repository
174 | 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
175 | 3. Commit your changes (`git commit -m 'Add amazing feature'`)
176 | 4. Push to the branch (`git push origin feature/amazing-feature`)
177 | 5. Open a Pull Request
178 | 
179 | ## Docker Usage
180 | 
181 | The project includes a `docker-commands.json` file that provides structured commands for different deployment scenarios. You can use these commands directly or parse them in your scripts.
182 | 
183 | ### Available Docker Configurations
184 | 
185 | 1. **Redis MCP Server**
186 | ```bash
187 | # Run Redis MCP server
188 | docker run -i --rm mcp/redis redis://host.docker.internal:6379
189 | ```
190 | 
191 | 2. **Django Migrations MCP Server**
192 | ```bash
193 | # Basic setup
194 | docker run -d \
195 |   --name django-migrations-mcp \
196 |   -e DJANGO_SETTINGS_MODULE=your_project.settings \
197 |   -e MCP_SERVICE_PORT=8000 \
198 |   -v /path/to/your/django/project:/app/project \
199 |   -p 8000:8000 \
200 |   django-migrations-mcp
201 | 
202 | # With Redis integration
203 | docker run -d \
204 |   --name django-migrations-mcp \
205 |   -e DJANGO_SETTINGS_MODULE=your_project.settings \
206 |   -e MCP_SERVICE_PORT=8000 \
207 |   -e REDIS_URL=redis://host.docker.internal:6379 \
208 |   -v /path/to/your/django/project:/app/project \
209 |   -p 8000:8000 \
210 |   --network host \
211 |   django-migrations-mcp
212 | ```
213 | 
214 | 3. **Development Environment**
215 | ```bash
216 | # Using docker-compose
217 | docker-compose up -d --build
218 | ```
219 | 
220 | 4. **Testing Environment**
221 | ```bash
222 | # Run tests in container
223 | docker run --rm \
224 |   -e DJANGO_SETTINGS_MODULE=your_project.settings \
225 |   -e PYTHONPATH=/app \
226 |   -v ${PWD}:/app \
227 |   django-migrations-mcp \
228 |   pytest
229 | ```
230 | 
231 | 5. **Production Environment**
232 | ```bash
233 | # Production setup with health check
234 | docker run -d \
235 |   --name django-migrations-mcp \
236 |   -e DJANGO_SETTINGS_MODULE=your_project.settings \
237 |   -e MCP_SERVICE_PORT=8000 \
238 |   -e REDIS_URL=redis://your-redis-host:6379 \
239 |   -v /path/to/your/django/project:/app/project \
240 |   -p 8000:8000 \
241 |   --restart unless-stopped \
242 |   --network your-network \
243 |   django-migrations-mcp
244 | ```
245 | 
246 | ### Using the Commands Programmatically
247 | 
248 | You can parse and use the commands programmatically:
249 | 
250 | ```python
251 | import json
252 | import subprocess
253 | 
254 | # Load commands
255 | with open('docker-commands.json') as f:
256 |     commands = json.load(f)
257 | 
258 | # Run Redis MCP server
259 | redis_config = commands['mcpServers']['redis']
260 | subprocess.run([redis_config['command']] + redis_config['args'])
261 | 
262 | # Run Django Migrations MCP server
263 | django_config = commands['mcpServers']['djangoMigrations']
264 | subprocess.run([django_config['command']] + django_config['args'])
265 | ```
266 | 
267 | ### Network Setup
268 | 
269 | 1. **Development Network**
270 | ```bash
271 | docker network create mcp-dev-network
272 | ```
273 | 
274 | 2. **Production Network**
275 | ```bash
276 | docker network create --driver overlay --attachable mcp-prod-network
277 | ```
278 | 
279 | ### Using MCP Tools
280 | 
281 | The service exposes several endpoints that can be accessed via curl or any HTTP client:
282 | 
283 | 1. **Show Migrations**
284 | ```bash
285 | curl -X POST http://localhost:8000/mcp \
286 |   -H "Content-Type: application/json" \
287 |   -d '{"method": "show_migrations"}'
288 | ```
289 | 
290 | 2. **Make Migrations**
291 | ```bash
292 | curl -X POST http://localhost:8000/mcp \
293 |   -H "Content-Type: application/json" \
294 |   -d '{"method": "make_migrations", "params": {"apps": ["your_app"]}}'
295 | ```
296 | 
297 | 3. **Apply Migrations**
298 | ```bash
299 | curl -X POST http://localhost:8000/mcp \
300 |   -H "Content-Type: application/json" \
301 |   -d '{"method": "migrate", "params": {"app": "your_app"}}'
302 | ``` 
```

--------------------------------------------------------------------------------
/migrations_mcp/handlers/__init__.py:
--------------------------------------------------------------------------------

```python
1 | 
```

--------------------------------------------------------------------------------
/migrations_mcp/tests/__init__.py:
--------------------------------------------------------------------------------

```python
1 | 
```

--------------------------------------------------------------------------------
/testproject/testapp/__init__.py:
--------------------------------------------------------------------------------

```python
1 | 
```

--------------------------------------------------------------------------------
/testproject/testapp/migrations/__init__.py:
--------------------------------------------------------------------------------

```python
1 | 
```

--------------------------------------------------------------------------------
/testproject/testapp/tests/__init__.py:
--------------------------------------------------------------------------------

```python
1 | 
```

--------------------------------------------------------------------------------
/testproject/testproject/__init__.py:
--------------------------------------------------------------------------------

```python
1 | 
```

--------------------------------------------------------------------------------
/testproject/testapp/tests.py:
--------------------------------------------------------------------------------

```python
1 | from django.test import TestCase
2 | 
3 | # Create your tests here.
4 | 
```

--------------------------------------------------------------------------------
/testproject/testapp/admin.py:
--------------------------------------------------------------------------------

```python
1 | from django.contrib import admin
2 | 
3 | # Register your models here.
4 | 
```

--------------------------------------------------------------------------------
/testproject/testapp/views.py:
--------------------------------------------------------------------------------

```python
1 | from django.shortcuts import render
2 | 
3 | # Create your views here.
4 | 
```

--------------------------------------------------------------------------------
/migrations_mcp/__init__.py:
--------------------------------------------------------------------------------

```python
1 | """Django migrations MCP package."""
2 | 
3 | from .service import DjangoMigrationsMCP
4 | 
5 | __all__ = ['DjangoMigrationsMCP']
6 | 
```

--------------------------------------------------------------------------------
/testproject/testapp/apps.py:
--------------------------------------------------------------------------------

```python
1 | from django.apps import AppConfig
2 | 
3 | 
4 | class TestappConfig(AppConfig):
5 |     default_auto_field = "django.db.models.BigAutoField"
6 |     name = "testapp"
7 | 
```

--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------

```
 1 | Django>=5.1.0
 2 | mcp
 3 | python-dotenv>=1.0.0
 4 | pydantic>=2.0.0
 5 | structlog>=23.1.0
 6 | pytest>=7.3.1
 7 | pytest-asyncio>=0.21.0
 8 | pytest-cov>=4.1.0
 9 | black>=23.7.0
10 | isort>=5.12.0
11 | mypy>=1.4.1
12 | pylint>=2.17.5 
```

--------------------------------------------------------------------------------
/pytest.ini:
--------------------------------------------------------------------------------

```
 1 | [pytest]
 2 | DJANGO_SETTINGS_MODULE = testproject.settings
 3 | python_paths = .
 4 | testpaths = testproject/testapp/tests
 5 | filterwarnings =
 6 |     ignore::DeprecationWarning
 7 |     ignore::UserWarning
 8 | addopts = -v --tb=short
 9 | asyncio_mode = auto
10 | asyncio_default_fixture_loop_scope = function 
```

--------------------------------------------------------------------------------
/testproject/testapp/models.py:
--------------------------------------------------------------------------------

```python
 1 | from django.db import models
 2 | 
 3 | # Create your models here.
 4 | 
 5 | class TestModel(models.Model):
 6 |     name = models.CharField(max_length=100)
 7 |     description = models.TextField(blank=True)
 8 |     created_at = models.DateTimeField(auto_now_add=True)
 9 |     updated_at = models.DateTimeField(auto_now=True)
10 | 
11 |     def __str__(self):
12 |         return self.name
13 | 
```

--------------------------------------------------------------------------------
/testproject/testproject/asgi.py:
--------------------------------------------------------------------------------

```python
 1 | """
 2 | ASGI config for testproject project.
 3 | 
 4 | It exposes the ASGI callable as a module-level variable named ``application``.
 5 | 
 6 | For more information on this file, see
 7 | https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/
 8 | """
 9 | 
10 | import os
11 | 
12 | from django.core.asgi import get_asgi_application
13 | 
14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testproject.settings")
15 | 
16 | application = get_asgi_application()
17 | 
```

--------------------------------------------------------------------------------
/testproject/testproject/wsgi.py:
--------------------------------------------------------------------------------

```python
 1 | """
 2 | WSGI config for testproject project.
 3 | 
 4 | It exposes the WSGI callable as a module-level variable named ``application``.
 5 | 
 6 | For more information on this file, see
 7 | https://docs.djangoproject.com/en/5.1/howto/deployment/wsgi/
 8 | """
 9 | 
10 | import os
11 | 
12 | from django.core.wsgi import get_wsgi_application
13 | 
14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testproject.settings")
15 | 
16 | application = get_wsgi_application()
17 | 
```

--------------------------------------------------------------------------------
/docker/entrypoint.sh:
--------------------------------------------------------------------------------

```bash
 1 | #!/bin/bash
 2 | set -e
 3 | 
 4 | # Validate environment variables
 5 | if [ -z "$DJANGO_SETTINGS_MODULE" ]; then
 6 |     echo "Error: DJANGO_SETTINGS_MODULE environment variable is required"
 7 |     exit 1
 8 | fi
 9 | 
10 | # Wait for database if needed (uncomment and modify as needed)
11 | # until nc -z $DB_HOST $DB_PORT; do
12 | #     echo "Waiting for database..."
13 | #     sleep 1
14 | # done
15 | 
16 | # Start the MCP service
17 | exec python -m migrations_mcp.service 
```

--------------------------------------------------------------------------------
/testproject/testapp/tests/conftest.py:
--------------------------------------------------------------------------------

```python
 1 | """Test configuration for pytest."""
 2 | import pytest
 3 | from django.conf import settings
 4 | 
 5 | @pytest.fixture(scope="function")
 6 | def enable_db_access_for_all_tests(db):
 7 |     """Enable database access for all tests."""
 8 |     pass
 9 | 
10 | @pytest.fixture(scope="session")
11 | def django_db_setup():
12 |     """Configure Django database for tests."""
13 |     settings.DATABASES = {
14 |         'default': {
15 |             'ENGINE': 'django.db.backends.sqlite3',
16 |             'NAME': ':memory:',
17 |         }
18 |     } 
```

--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------

```python
 1 | from setuptools import setup, find_packages
 2 | 
 3 | setup(
 4 |     name="migrations-mcp",
 5 |     version="0.1.0",
 6 |     packages=find_packages(),
 7 |     install_requires=[
 8 |         "Django>=5.1.0",
 9 |         "mcp",
10 |         "python-dotenv>=1.0.0",
11 |         "pydantic>=2.0.0",
12 |         "structlog>=23.1.0",
13 |     ],
14 |     extras_require={
15 |         "dev": [
16 |             "pytest>=7.3.1",
17 |             "pytest-asyncio>=0.21.0",
18 |             "pytest-cov>=4.1.0",
19 |             "black>=23.7.0",
20 |             "isort>=5.12.0",
21 |             "mypy>=1.4.1",
22 |             "pylint>=2.17.5",
23 |         ]
24 |     },
25 | ) 
```

--------------------------------------------------------------------------------
/testproject/manage 2.py:
--------------------------------------------------------------------------------

```python
 1 | #!/usr/bin/env python
 2 | """Django's command-line utility for administrative tasks."""
 3 | import os
 4 | import sys
 5 | 
 6 | 
 7 | def main():
 8 |     """Run administrative tasks."""
 9 |     os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testproject.settings")
10 |     try:
11 |         from django.core.management import execute_from_command_line
12 |     except ImportError as exc:
13 |         raise ImportError(
14 |             "Couldn't import Django. Are you sure it's installed and "
15 |             "available on your PYTHONPATH environment variable? Did you "
16 |             "forget to activate a virtual environment?"
17 |         ) from exc
18 |     execute_from_command_line(sys.argv)
19 | 
20 | 
21 | if __name__ == "__main__":
22 |     main()
23 | 
```

--------------------------------------------------------------------------------
/testproject/manage.py:
--------------------------------------------------------------------------------

```python
 1 | #!/usr/bin/env python
 2 | """Django's command-line utility for administrative tasks."""
 3 | import os
 4 | import sys
 5 | 
 6 | 
 7 | def main():
 8 |     """Run administrative tasks."""
 9 |     os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testproject.settings")
10 |     try:
11 |         from django.core.management import execute_from_command_line
12 |     except ImportError as exc:
13 |         raise ImportError(
14 |             "Couldn't import Django. Are you sure it's installed and "
15 |             "available on your PYTHONPATH environment variable? Did you "
16 |             "forget to activate a virtual environment?"
17 |         ) from exc
18 |     execute_from_command_line(sys.argv)
19 | 
20 | 
21 | if __name__ == "__main__":
22 |     main()
23 | 
```

--------------------------------------------------------------------------------
/testproject/testproject/urls.py:
--------------------------------------------------------------------------------

```python
 1 | """
 2 | URL configuration for testproject project.
 3 | 
 4 | The `urlpatterns` list routes URLs to views. For more information please see:
 5 |     https://docs.djangoproject.com/en/5.1/topics/http/urls/
 6 | Examples:
 7 | Function views
 8 |     1. Add an import:  from my_app import views
 9 |     2. Add a URL to urlpatterns:  path('', views.home, name='home')
10 | Class-based views
11 |     1. Add an import:  from other_app.views import Home
12 |     2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
13 | Including another URLconf
14 |     1. Import the include() function: from django.urls import include, path
15 |     2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
16 | """
17 | 
18 | from django.contrib import admin
19 | from django.urls import path
20 | 
21 | urlpatterns = [
22 |     path("admin/", admin.site.urls),
23 | ]
24 | 
```

--------------------------------------------------------------------------------
/testproject/testapp/migrations/0001_initial.py:
--------------------------------------------------------------------------------

```python
 1 | # Generated by Django 5.1.6 on 2025-02-11 21:07
 2 | 
 3 | from django.db import migrations, models
 4 | 
 5 | 
 6 | class Migration(migrations.Migration):
 7 | 
 8 |     initial = True
 9 | 
10 |     dependencies = []
11 | 
12 |     operations = [
13 |         migrations.CreateModel(
14 |             name="TestModel",
15 |             fields=[
16 |                 (
17 |                     "id",
18 |                     models.BigAutoField(
19 |                         auto_created=True,
20 |                         primary_key=True,
21 |                         serialize=False,
22 |                         verbose_name="ID",
23 |                     ),
24 |                 ),
25 |                 ("name", models.CharField(max_length=100)),
26 |                 ("description", models.TextField(blank=True)),
27 |                 ("created_at", models.DateTimeField(auto_now_add=True)),
28 |                 ("updated_at", models.DateTimeField(auto_now=True)),
29 |             ],
30 |         ),
31 |     ]
32 | 
```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
 1 | # Build stage
 2 | FROM python:3.11-slim as builder
 3 | 
 4 | WORKDIR /app
 5 | 
 6 | # Install build dependencies
 7 | RUN apt-get update && apt-get install -y --no-install-recommends \
 8 |     build-essential \
 9 |     && rm -rf /var/lib/apt/lists/*
10 | 
11 | # Install Python dependencies
12 | COPY requirements.txt .
13 | RUN pip wheel --no-cache-dir --no-deps --wheel-dir /app/wheels -r requirements.txt
14 | 
15 | # Final stage
16 | FROM python:3.11-slim
17 | 
18 | WORKDIR /app
19 | 
20 | # Create non-root user
21 | RUN useradd -m -u 1000 mcp && \
22 |     chown -R mcp:mcp /app
23 | 
24 | # Copy wheels from builder
25 | COPY --from=builder /app/wheels /wheels
26 | COPY --from=builder /app/requirements.txt .
27 | 
28 | # Install dependencies
29 | RUN pip install --no-cache /wheels/*
30 | 
31 | # Copy application code
32 | COPY migrations_mcp ./migrations_mcp
33 | COPY docker/entrypoint.sh .
34 | 
35 | # Set permissions
36 | RUN chmod +x entrypoint.sh && \
37 |     chown -R mcp:mcp /app
38 | 
39 | USER mcp
40 | 
41 | # Environment variables
42 | ENV PYTHONPATH=/app
43 | ENV DJANGO_SETTINGS_MODULE=""
44 | ENV MCP_SERVICE_PORT=8000
45 | 
46 | # Expose port
47 | EXPOSE 8000
48 | 
49 | # Run the service
50 | ENTRYPOINT ["./entrypoint.sh"] 
```

--------------------------------------------------------------------------------
/testproject/testapp/tests/test_migrations.py:
--------------------------------------------------------------------------------

```python
 1 | """Test Django migrations with MCP service."""
 2 | import os
 3 | import pytest
 4 | from migrations_mcp.service import DjangoMigrationsMCP
 5 | 
 6 | # Configure Django settings for tests
 7 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'testproject.settings')
 8 | 
 9 | @pytest.fixture
10 | def service():
11 |     """Create a DjangoMigrationsMCP service instance."""
12 |     return DjangoMigrationsMCP()
13 | 
14 | @pytest.mark.django_db
15 | @pytest.mark.asyncio
16 | async def test_show_migrations(service):
17 |     """Test show_migrations command."""
18 |     result = await service.show_migrations()
19 |     assert isinstance(result, list)
20 |     # At least one migration should exist (initial migration)
21 |     assert len(result) > 0
22 | 
23 | @pytest.mark.django_db
24 | @pytest.mark.asyncio
25 | async def test_make_migrations(service):
26 |     """Test make_migrations command."""
27 |     result = await service.make_migrations(['testapp'])
28 |     assert result.success
29 |     assert "Migrations created successfully" in result.message
30 | 
31 | @pytest.mark.django_db
32 | @pytest.mark.asyncio
33 | async def test_migrate(service):
34 |     """Test migrate command."""
35 |     result = await service.migrate('testapp')
36 |     assert result.success
37 |     assert "Migrations applied successfully" in result.message 
```

--------------------------------------------------------------------------------
/migationsenv/bin/activate.fish:
--------------------------------------------------------------------------------

```
 1 | # This file must be used with "source <venv>/bin/activate.fish" *from fish*
 2 | # (https://fishshell.com/). You cannot run it directly.
 3 | 
 4 | function deactivate  -d "Exit virtual environment and return to normal shell environment"
 5 |     # reset old environment variables
 6 |     if test -n "$_OLD_VIRTUAL_PATH"
 7 |         set -gx PATH $_OLD_VIRTUAL_PATH
 8 |         set -e _OLD_VIRTUAL_PATH
 9 |     end
10 |     if test -n "$_OLD_VIRTUAL_PYTHONHOME"
11 |         set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME
12 |         set -e _OLD_VIRTUAL_PYTHONHOME
13 |     end
14 | 
15 |     if test -n "$_OLD_FISH_PROMPT_OVERRIDE"
16 |         set -e _OLD_FISH_PROMPT_OVERRIDE
17 |         # prevents error when using nested fish instances (Issue #93858)
18 |         if functions -q _old_fish_prompt
19 |             functions -e fish_prompt
20 |             functions -c _old_fish_prompt fish_prompt
21 |             functions -e _old_fish_prompt
22 |         end
23 |     end
24 | 
25 |     set -e VIRTUAL_ENV
26 |     set -e VIRTUAL_ENV_PROMPT
27 |     if test "$argv[1]" != "nondestructive"
28 |         # Self-destruct!
29 |         functions -e deactivate
30 |     end
31 | end
32 | 
33 | # Unset irrelevant variables.
34 | deactivate nondestructive
35 | 
36 | set -gx VIRTUAL_ENV "/Users/antonyngigge/Library/Mobile Documents/com~apple~CloudDocs/iworldafric/djangomigrationsmcp/migationsenv"
37 | 
38 | set -gx _OLD_VIRTUAL_PATH $PATH
39 | set -gx PATH "$VIRTUAL_ENV/bin" $PATH
40 | 
41 | # Unset PYTHONHOME if set.
42 | if set -q PYTHONHOME
43 |     set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME
44 |     set -e PYTHONHOME
45 | end
46 | 
47 | if test -z "$VIRTUAL_ENV_DISABLE_PROMPT"
48 |     # fish uses a function instead of an env var to generate the prompt.
49 | 
50 |     # Save the current fish_prompt function as the function _old_fish_prompt.
51 |     functions -c fish_prompt _old_fish_prompt
52 | 
53 |     # With the original prompt function renamed, we can override with our own.
54 |     function fish_prompt
55 |         # Save the return status of the last command.
56 |         set -l old_status $status
57 | 
58 |         # Output the venv prompt; color taken from the blue of the Python logo.
59 |         printf "%s%s%s" (set_color 4B8BBE) "(migationsenv) " (set_color normal)
60 | 
61 |         # Restore the return status of the previous command.
62 |         echo "exit $old_status" | .
63 |         # Output the original/"old" prompt.
64 |         _old_fish_prompt
65 |     end
66 | 
67 |     set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV"
68 |     set -gx VIRTUAL_ENV_PROMPT "(migationsenv) "
69 | end
70 | 
```

--------------------------------------------------------------------------------
/migrations_mcp/service.py:
--------------------------------------------------------------------------------

```python
 1 | """
 2 | Django Migrations MCP Service.
 3 | This service provides endpoints for managing Django migrations through MCP.
 4 | """
 5 | import asyncio
 6 | import logging
 7 | from typing import Any, Dict, List, Optional
 8 | 
 9 | from django.core.management import call_command
10 | from django.db import connection
11 | from mcp.server.fastmcp import FastMCP
12 | from mcp.server.fastmcp.tools.base import Tool
13 | from pydantic import BaseModel
14 | from asgiref.sync import sync_to_async
15 | 
16 | # Configure logging
17 | logging.basicConfig(level=logging.INFO)
18 | logger = logging.getLogger(__name__)
19 | 
20 | class MigrationStatus(BaseModel):
21 |     """Model representing migration status."""
22 |     app: str
23 |     name: str
24 |     applied: bool
25 |     dependencies: List[str] = []
26 | 
27 | class MigrationResult(BaseModel):
28 |     """Model representing migration operation result."""
29 |     success: bool
30 |     message: str
31 |     details: Optional[Dict[str, Any]] = None
32 | 
33 | class DjangoMigrationsMCP(FastMCP):
34 |     """MCP service for managing Django migrations."""
35 | 
36 |     async def show_migrations(self) -> List[str]:
37 |         """Show all migrations."""
38 |         try:
39 |             @sync_to_async
40 |             def _show_migrations():
41 |                 with connection.cursor() as cursor:
42 |                     call_command('showmigrations', stdout=cursor)
43 |                     return cursor.fetchall()
44 |             return await _show_migrations()
45 |         except Exception as e:
46 |             return [f"Error showing migrations: {str(e)}"]
47 | 
48 |     show_migrations_tool = Tool.from_function(show_migrations)
49 | 
50 |     async def make_migrations(self, apps: Optional[List[str]] = None) -> MigrationResult:
51 |         """Make migrations for specified apps or all apps."""
52 |         try:
53 |             @sync_to_async
54 |             def _make_migrations():
55 |                 call_command('makemigrations', *apps if apps else [])
56 |             await _make_migrations()
57 |             return MigrationResult(
58 |                 success=True,
59 |                 message="Migrations created successfully"
60 |             )
61 |         except Exception as e:
62 |             return MigrationResult(
63 |                 success=False,
64 |                 message=f"Error creating migrations: {str(e)}"
65 |             )
66 | 
67 |     make_migrations_tool = Tool.from_function(make_migrations)
68 | 
69 |     async def migrate(self, app: Optional[str] = None) -> MigrationResult:
70 |         """Apply migrations for specified app or all apps."""
71 |         try:
72 |             @sync_to_async
73 |             def _migrate():
74 |                 call_command('migrate', app if app else '')
75 |             await _migrate()
76 |             return MigrationResult(
77 |                 success=True,
78 |                 message="Migrations applied successfully"
79 |             )
80 |         except Exception as e:
81 |             return MigrationResult(
82 |                 success=False,
83 |                 message=f"Error applying migrations: {str(e)}"
84 |             )
85 | 
86 |     migrate_tool = Tool.from_function(migrate)
87 | 
88 | if __name__ == "__main__":
89 |     service = DjangoMigrationsMCP()
90 |     service.run() 
```

--------------------------------------------------------------------------------
/testproject/testproject/settings.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Django settings for testproject project.
  3 | 
  4 | Generated by 'django-admin startproject' using Django 5.1.5.
  5 | 
  6 | For more information on this file, see
  7 | https://docs.djangoproject.com/en/5.1/topics/settings/
  8 | 
  9 | For the full list of settings and their values, see
 10 | https://docs.djangoproject.com/en/5.1/ref/settings/
 11 | """
 12 | 
 13 | from pathlib import Path
 14 | 
 15 | # Build paths inside the project like this: BASE_DIR / 'subdir'.
 16 | BASE_DIR = Path(__file__).resolve().parent.parent
 17 | 
 18 | 
 19 | # Quick-start development settings - unsuitable for production
 20 | # See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/
 21 | 
 22 | # SECURITY WARNING: keep the secret key used in production secret!
 23 | SECRET_KEY = 'django-insecure-test-key-for-development-only'
 24 | 
 25 | # SECURITY WARNING: don't run with debug turned on in production!
 26 | DEBUG = True
 27 | 
 28 | ALLOWED_HOSTS = []
 29 | 
 30 | 
 31 | # Application definition
 32 | 
 33 | INSTALLED_APPS = [
 34 |     "django.contrib.admin",
 35 |     "django.contrib.auth",
 36 |     "django.contrib.contenttypes",
 37 |     "django.contrib.sessions",
 38 |     "django.contrib.messages",
 39 |     "django.contrib.staticfiles",
 40 |     "testapp",  # Add our test app
 41 | ]
 42 | 
 43 | MIDDLEWARE = [
 44 |     "django.middleware.security.SecurityMiddleware",
 45 |     "django.contrib.sessions.middleware.SessionMiddleware",
 46 |     "django.middleware.common.CommonMiddleware",
 47 |     "django.middleware.csrf.CsrfViewMiddleware",
 48 |     "django.contrib.auth.middleware.AuthenticationMiddleware",
 49 |     "django.contrib.messages.middleware.MessageMiddleware",
 50 |     "django.middleware.clickjacking.XFrameOptionsMiddleware",
 51 | ]
 52 | 
 53 | ROOT_URLCONF = "testproject.urls"
 54 | 
 55 | TEMPLATES = [
 56 |     {
 57 |         "BACKEND": "django.template.backends.django.DjangoTemplates",
 58 |         "DIRS": [],
 59 |         "APP_DIRS": True,
 60 |         "OPTIONS": {
 61 |             "context_processors": [
 62 |                 "django.template.context_processors.debug",
 63 |                 "django.template.context_processors.request",
 64 |                 "django.contrib.auth.context_processors.auth",
 65 |                 "django.contrib.messages.context_processors.messages",
 66 |             ],
 67 |         },
 68 |     },
 69 | ]
 70 | 
 71 | WSGI_APPLICATION = "testproject.wsgi.application"
 72 | 
 73 | 
 74 | # Database
 75 | # https://docs.djangoproject.com/en/5.1/ref/settings/#databases
 76 | 
 77 | DATABASES = {
 78 |     "default": {
 79 |         "ENGINE": "django.db.backends.sqlite3",
 80 |         "NAME": BASE_DIR / "db.sqlite3",
 81 |     }
 82 | }
 83 | 
 84 | 
 85 | # Password validation
 86 | # https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
 87 | 
 88 | AUTH_PASSWORD_VALIDATORS = [
 89 |     {
 90 |         "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
 91 |     },
 92 |     {
 93 |         "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
 94 |     },
 95 |     {
 96 |         "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
 97 |     },
 98 |     {
 99 |         "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
100 |     },
101 | ]
102 | 
103 | 
104 | # Internationalization
105 | # https://docs.djangoproject.com/en/5.1/topics/i18n/
106 | 
107 | LANGUAGE_CODE = "en-us"
108 | 
109 | TIME_ZONE = "UTC"
110 | 
111 | USE_I18N = True
112 | 
113 | USE_TZ = True
114 | 
115 | 
116 | # Static files (CSS, JavaScript, Images)
117 | # https://docs.djangoproject.com/en/5.1/howto/static-files/
118 | 
119 | STATIC_URL = "static/"
120 | 
121 | # Default primary key field type
122 | # https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
123 | 
124 | DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
125 | 
```

--------------------------------------------------------------------------------
/docker-commands.json:
--------------------------------------------------------------------------------

```json
  1 | {
  2 |   "mcpServers": {
  3 |     "redis": {
  4 |       "command": "docker",
  5 |       "args": [
  6 |         "run",
  7 |         "-i",
  8 |         "--rm",
  9 |         "mcp/redis",
 10 |         "redis://host.docker.internal:6379"
 11 |       ]
 12 |     },
 13 |     "djangoMigrations": {
 14 |       "command": "docker",
 15 |       "args": [
 16 |         "run",
 17 |         "-d",
 18 |         "--name", "django-migrations-mcp",
 19 |         "-e", "DJANGO_SETTINGS_MODULE=your_project.settings",
 20 |         "-e", "MCP_SERVICE_PORT=8000",
 21 |         "-v", "/path/to/your/django/project:/app/project",
 22 |         "-p", "8000:8000",
 23 |         "django-migrations-mcp"
 24 |       ]
 25 |     },
 26 |     "djangoMigrationsWithRedis": {
 27 |       "command": "docker",
 28 |       "args": [
 29 |         "run",
 30 |         "-d",
 31 |         "--name", "django-migrations-mcp",
 32 |         "-e", "DJANGO_SETTINGS_MODULE=your_project.settings",
 33 |         "-e", "MCP_SERVICE_PORT=8000",
 34 |         "-e", "REDIS_URL=redis://host.docker.internal:6379",
 35 |         "-v", "/path/to/your/django/project:/app/project",
 36 |         "-p", "8000:8000",
 37 |         "--network", "host",
 38 |         "django-migrations-mcp"
 39 |       ]
 40 |     },
 41 |     "development": {
 42 |       "command": "docker-compose",
 43 |       "args": [
 44 |         "up",
 45 |         "-d",
 46 |         "--build"
 47 |       ],
 48 |       "environment": {
 49 |         "DJANGO_SETTINGS_MODULE": "your_project.settings",
 50 |         "MCP_SERVICE_PORT": "8000",
 51 |         "REDIS_URL": "redis://localhost:6379"
 52 |       }
 53 |     },
 54 |     "testing": {
 55 |       "command": "docker",
 56 |       "args": [
 57 |         "run",
 58 |         "--rm",
 59 |         "-e", "DJANGO_SETTINGS_MODULE=your_project.settings",
 60 |         "-e", "PYTHONPATH=/app",
 61 |         "-v", "${PWD}:/app",
 62 |         "django-migrations-mcp",
 63 |         "pytest"
 64 |       ]
 65 |     },
 66 |     "production": {
 67 |       "command": "docker",
 68 |       "args": [
 69 |         "run",
 70 |         "-d",
 71 |         "--name", "django-migrations-mcp",
 72 |         "-e", "DJANGO_SETTINGS_MODULE=your_project.settings",
 73 |         "-e", "MCP_SERVICE_PORT=8000",
 74 |         "-e", "REDIS_URL=redis://your-redis-host:6379",
 75 |         "-v", "/path/to/your/django/project:/app/project",
 76 |         "-p", "8000:8000",
 77 |         "--restart", "unless-stopped",
 78 |         "--network", "your-network",
 79 |         "django-migrations-mcp"
 80 |       ],
 81 |       "healthCheck": {
 82 |         "test": ["CMD", "curl", "-f", "http://localhost:8000/health"],
 83 |         "interval": "30s",
 84 |         "timeout": "10s",
 85 |         "retries": 3
 86 |       }
 87 |     }
 88 |   },
 89 |   "tools": {
 90 |     "showMigrations": {
 91 |       "command": "curl",
 92 |       "args": [
 93 |         "-X", "POST",
 94 |         "http://localhost:8000/mcp",
 95 |         "-H", "Content-Type: application/json",
 96 |         "-d", "{\"method\": \"show_migrations\"}"
 97 |       ]
 98 |     },
 99 |     "makeMigrations": {
100 |       "command": "curl",
101 |       "args": [
102 |         "-X", "POST",
103 |         "http://localhost:8000/mcp",
104 |         "-H", "Content-Type: application/json",
105 |         "-d", "{\"method\": \"make_migrations\", \"params\": {\"apps\": [\"your_app\"]}}"
106 |       ]
107 |     },
108 |     "migrate": {
109 |       "command": "curl",
110 |       "args": [
111 |         "-X", "POST",
112 |         "http://localhost:8000/mcp",
113 |         "-H", "Content-Type: application/json",
114 |         "-d", "{\"method\": \"migrate\", \"params\": {\"app\": \"your_app\"}}"
115 |       ]
116 |     }
117 |   },
118 |   "networks": {
119 |     "development": {
120 |       "command": "docker",
121 |       "args": [
122 |         "network",
123 |         "create",
124 |         "mcp-dev-network"
125 |       ]
126 |     },
127 |     "production": {
128 |       "command": "docker",
129 |       "args": [
130 |         "network",
131 |         "create",
132 |         "--driver", "overlay",
133 |         "--attachable",
134 |         "mcp-prod-network"
135 |       ]
136 |     }
137 |   }
138 | } 
```

--------------------------------------------------------------------------------
/migrations_mcp/handlers/utils.py:
--------------------------------------------------------------------------------

```python
  1 | """Utility functions for migration validation and checks."""
  2 | import os
  3 | import re
  4 | from pathlib import Path
  5 | from typing import Dict, List, Optional, Set, Tuple
  6 | 
  7 | from django.apps import apps
  8 | from django.db.migrations.loader import MigrationLoader
  9 | 
 10 | 
 11 | def get_migration_files(app_label: str) -> List[str]:
 12 |     """Get all migration files for an app."""
 13 |     app_config = apps.get_app_config(app_label)
 14 |     migrations_dir = Path(app_config.path) / 'migrations'
 15 |     
 16 |     if not migrations_dir.exists():
 17 |         return []
 18 |     
 19 |     return [
 20 |         f.name for f in migrations_dir.iterdir()
 21 |         if f.is_file() and f.name.endswith('.py')
 22 |         and not f.name.startswith('__')
 23 |     ]
 24 | 
 25 | def parse_migration_number(filename: str) -> Optional[int]:
 26 |     """Extract migration number from filename."""
 27 |     match = re.match(r'^(\d{4})_.*\.py$', filename)
 28 |     return int(match.group(1)) if match else None
 29 | 
 30 | def check_sequential_order(app_label: str) -> Tuple[bool, List[str]]:
 31 |     """Check if migrations are in sequential order."""
 32 |     files = get_migration_files(app_label)
 33 |     numbers = [parse_migration_number(f) for f in files]
 34 |     numbers = [n for n in numbers if n is not None]
 35 |     
 36 |     if not numbers:
 37 |         return True, []
 38 |     
 39 |     expected = list(range(min(numbers), max(numbers) + 1))
 40 |     missing = set(expected) - set(numbers)
 41 |     
 42 |     if missing:
 43 |         return False, [
 44 |             f"Missing migration number(s): {', '.join(map(str, missing))}"
 45 |         ]
 46 |     return True, []
 47 | 
 48 | def detect_conflicts(app_label: str) -> List[str]:
 49 |     """Detect migration conflicts."""
 50 |     loader = MigrationLoader(None)
 51 |     conflicts = []
 52 |     
 53 |     # Check for conflicts in the migration graph
 54 |     if loader.graph.conflicts:
 55 |         for app, nodes in loader.graph.conflicts.items():
 56 |             if app == app_label:
 57 |                 conflicts.extend([
 58 |                     f"Conflict in {app}: migrations {', '.join(nodes)}"
 59 |                 ])
 60 |     
 61 |     return conflicts
 62 | 
 63 | def validate_dependencies(app_label: str) -> List[str]:
 64 |     """Validate migration dependencies."""
 65 |     loader = MigrationLoader(None)
 66 |     errors = []
 67 |     
 68 |     for migration in loader.disk_migrations.values():
 69 |         if migration.app_label == app_label:
 70 |             for dep_app, dep_name in migration.dependencies:
 71 |                 # Check if dependency exists
 72 |                 if (dep_app, dep_name) not in loader.disk_migrations:
 73 |                     errors.append(
 74 |                         f"Missing dependency: {dep_app}.{dep_name} "
 75 |                         f"required by {app_label}.{migration.name}"
 76 |                     )
 77 |     
 78 |     return errors
 79 | 
 80 | def get_migration_plan(app_label: Optional[str] = None) -> List[Tuple[str, bool]]:
 81 |     """Get the migration plan showing what needs to be applied."""
 82 |     from django.db import connections
 83 |     from django.db.migrations.executor import MigrationExecutor
 84 |     
 85 |     executor = MigrationExecutor(connections['default'])
 86 |     plan = executor.migration_plan(executor.loader.graph.leaf_nodes())
 87 |     
 88 |     if app_label:
 89 |         plan = [
 90 |             (migration, backwards)
 91 |             for migration, backwards in plan
 92 |             if migration.app_label == app_label
 93 |         ]
 94 |     
 95 |     return plan
 96 | 
 97 | def check_migration_safety(
 98 |     app_label: str,
 99 |     migration_name: str
100 | ) -> Tuple[bool, List[str]]:
101 |     """Check if a migration is safe to apply."""
102 |     warnings = []
103 |     is_safe = True
104 |     
105 |     # Load the migration
106 |     loader = MigrationLoader(None)
107 |     try:
108 |         migration = loader.get_migration_by_prefix(app_label, migration_name)
109 |     except KeyError:
110 |         return False, ["Migration not found"]
111 |     
112 |     # Check for dangerous operations
113 |     for operation in migration.operations:
114 |         op_name = operation.__class__.__name__
115 |         
116 |         if op_name == 'DeleteModel':
117 |             warnings.append(f"Warning: Migration deletes model {operation.name}")
118 |             is_safe = False
119 |         
120 |         elif op_name == 'RemoveField':
121 |             warnings.append(
122 |                 f"Warning: Migration removes field {operation.model_name}."
123 |                 f"{operation.name}"
124 |             )
125 |             is_safe = False
126 |         
127 |         elif op_name == 'AlterField':
128 |             warnings.append(
129 |                 f"Warning: Migration alters field {operation.model_name}."
130 |                 f"{operation.name}"
131 |             )
132 |     
133 |     return is_safe, warnings 
```

--------------------------------------------------------------------------------
/migrations_mcp/tests/test_handlers.py:
--------------------------------------------------------------------------------

```python
  1 | """Tests for the Django Migrations MCP service."""
  2 | import os
  3 | import pytest
  4 | from pathlib import Path
  5 | from typing import List, Tuple
  6 | from unittest.mock import AsyncMock, MagicMock, patch
  7 | 
  8 | from django.apps import apps
  9 | from django.core.management import call_command
 10 | from django.db.migrations.loader import MigrationLoader
 11 | 
 12 | from migrations_mcp.service import DjangoMigrationsMCP
 13 | from migrations_mcp.handlers.utils import (
 14 |     check_sequential_order,
 15 |     detect_conflicts,
 16 |     validate_dependencies,
 17 |     check_migration_safety
 18 | )
 19 | 
 20 | @pytest.fixture
 21 | def service():
 22 |     """Create a DjangoMigrationsMCP service instance."""
 23 |     return DjangoMigrationsMCP()
 24 | 
 25 | @pytest.fixture
 26 | def mock_app_config():
 27 |     """Mock Django app configuration."""
 28 |     mock = MagicMock()
 29 |     mock.path = str(Path(__file__).parent / 'test_migrations')
 30 |     return mock
 31 | 
 32 | @pytest.fixture
 33 | def mock_migration_loader():
 34 |     """Mock Django migration loader."""
 35 |     mock = MagicMock()
 36 |     mock.disk_migrations = {}
 37 |     mock.graph.conflicts = {}
 38 |     return mock
 39 | 
 40 | @pytest.mark.asyncio
 41 | async def test_show_migrations(service):
 42 |     """Test show_migrations handler."""
 43 |     with patch('django.core.management.call_command') as mock_call:
 44 |         mock_call.return_value = None
 45 |         result = await service.show_migrations()
 46 |         assert isinstance(result, list)
 47 |         mock_call.assert_called_once_with(
 48 |             'showmigrations',
 49 |             list=True,
 50 |             _callback=pytest.ANY
 51 |         )
 52 | 
 53 | @pytest.mark.asyncio
 54 | async def test_make_migrations(service):
 55 |     """Test make_migrations handler."""
 56 |     with patch('django.core.management.call_command') as mock_call:
 57 |         mock_call.return_value = "Created migration"
 58 |         result = await service.make_migrations(
 59 |             app_labels=['testapp'],
 60 |             dry_run=True
 61 |         )
 62 |         assert result.success
 63 |         assert "successfully" in result.message
 64 |         mock_call.assert_called_once_with(
 65 |             'makemigrations',
 66 |             'testapp',
 67 |             dry_run=True,
 68 |             verbosity=2
 69 |         )
 70 | 
 71 | @pytest.mark.asyncio
 72 | async def test_migrate(service):
 73 |     """Test migrate handler."""
 74 |     with patch('django.core.management.call_command') as mock_call:
 75 |         mock_call.return_value = "Applied migration"
 76 |         result = await service.migrate(
 77 |             app_label='testapp',
 78 |             migration_name='0001',
 79 |             fake=True
 80 |         )
 81 |         assert result.success
 82 |         assert "successfully" in result.message
 83 |         mock_call.assert_called_once_with(
 84 |             'migrate',
 85 |             'testapp',
 86 |             '0001',
 87 |             fake=True,
 88 |             plan=False,
 89 |             verbosity=2
 90 |         )
 91 | 
 92 | def test_check_sequential_order(mock_app_config):
 93 |     """Test migration sequential order checking."""
 94 |     with patch('django.apps.apps.get_app_config', return_value=mock_app_config):
 95 |         # Create test migration files
 96 |         migrations_dir = Path(mock_app_config.path)
 97 |         migrations_dir.mkdir(parents=True, exist_ok=True)
 98 |         
 99 |         # Create test migration files
100 |         migrations = ['0001_initial.py', '0002_update.py', '0004_change.py']
101 |         for migration in migrations:
102 |             (migrations_dir / migration).touch()
103 |         
104 |         is_sequential, errors = check_sequential_order('testapp')
105 |         assert not is_sequential
106 |         assert any('Missing migration number(s): 3' in error for error in errors)
107 |         
108 |         # Cleanup
109 |         for migration in migrations:
110 |             (migrations_dir / migration).unlink()
111 |         migrations_dir.rmdir()
112 | 
113 | def test_detect_conflicts(mock_migration_loader):
114 |     """Test migration conflict detection."""
115 |     with patch('migrations_mcp.handlers.utils.MigrationLoader',
116 |               return_value=mock_migration_loader):
117 |         mock_migration_loader.graph.conflicts = {
118 |             'testapp': ['0001_initial', '0001_other']
119 |         }
120 |         
121 |         conflicts = detect_conflicts('testapp')
122 |         assert len(conflicts) == 1
123 |         assert 'Conflict in testapp' in conflicts[0]
124 | 
125 | def test_validate_dependencies(mock_migration_loader):
126 |     """Test migration dependency validation."""
127 |     with patch('migrations_mcp.handlers.utils.MigrationLoader',
128 |               return_value=mock_migration_loader):
129 |         # Mock a migration with missing dependency
130 |         migration = MagicMock()
131 |         migration.app_label = 'testapp'
132 |         migration.name = '0001_initial'
133 |         migration.dependencies = [('other_app', '0001_initial')]
134 |         
135 |         mock_migration_loader.disk_migrations = {
136 |             ('testapp', '0001_initial'): migration
137 |         }
138 |         
139 |         errors = validate_dependencies('testapp')
140 |         assert len(errors) == 1
141 |         assert 'Missing dependency' in errors[0]
142 | 
143 | def test_check_migration_safety():
144 |     """Test migration safety checking."""
145 |     with patch('migrations_mcp.handlers.utils.MigrationLoader') as mock_loader:
146 |         # Mock a migration with unsafe operations
147 |         migration = MagicMock()
148 |         delete_op = MagicMock()
149 |         delete_op.__class__.__name__ = 'DeleteModel'
150 |         delete_op.name = 'TestModel'
151 |         migration.operations = [delete_op]
152 |         
153 |         mock_loader_instance = MagicMock()
154 |         mock_loader_instance.get_migration_by_prefix.return_value = migration
155 |         mock_loader.return_value = mock_loader_instance
156 |         
157 |         is_safe, warnings = check_migration_safety('testapp', '0001')
158 |         assert not is_safe
159 |         assert len(warnings) == 1
160 |         assert 'deletes model' in warnings[0]
161 | 
162 | if __name__ == '__main__':
163 |     pytest.main([__file__]) 
```

--------------------------------------------------------------------------------
/migationsenv/bin/Activate.ps1:
--------------------------------------------------------------------------------

```
  1 | <#
  2 | .Synopsis
  3 | Activate a Python virtual environment for the current PowerShell session.
  4 | 
  5 | .Description
  6 | Pushes the python executable for a virtual environment to the front of the
  7 | $Env:PATH environment variable and sets the prompt to signify that you are
  8 | in a Python virtual environment. Makes use of the command line switches as
  9 | well as the `pyvenv.cfg` file values present in the virtual environment.
 10 | 
 11 | .Parameter VenvDir
 12 | Path to the directory that contains the virtual environment to activate. The
 13 | default value for this is the parent of the directory that the Activate.ps1
 14 | script is located within.
 15 | 
 16 | .Parameter Prompt
 17 | The prompt prefix to display when this virtual environment is activated. By
 18 | default, this prompt is the name of the virtual environment folder (VenvDir)
 19 | surrounded by parentheses and followed by a single space (ie. '(.venv) ').
 20 | 
 21 | .Example
 22 | Activate.ps1
 23 | Activates the Python virtual environment that contains the Activate.ps1 script.
 24 | 
 25 | .Example
 26 | Activate.ps1 -Verbose
 27 | Activates the Python virtual environment that contains the Activate.ps1 script,
 28 | and shows extra information about the activation as it executes.
 29 | 
 30 | .Example
 31 | Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv
 32 | Activates the Python virtual environment located in the specified location.
 33 | 
 34 | .Example
 35 | Activate.ps1 -Prompt "MyPython"
 36 | Activates the Python virtual environment that contains the Activate.ps1 script,
 37 | and prefixes the current prompt with the specified string (surrounded in
 38 | parentheses) while the virtual environment is active.
 39 | 
 40 | .Notes
 41 | On Windows, it may be required to enable this Activate.ps1 script by setting the
 42 | execution policy for the user. You can do this by issuing the following PowerShell
 43 | command:
 44 | 
 45 | PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
 46 | 
 47 | For more information on Execution Policies: 
 48 | https://go.microsoft.com/fwlink/?LinkID=135170
 49 | 
 50 | #>
 51 | Param(
 52 |     [Parameter(Mandatory = $false)]
 53 |     [String]
 54 |     $VenvDir,
 55 |     [Parameter(Mandatory = $false)]
 56 |     [String]
 57 |     $Prompt
 58 | )
 59 | 
 60 | <# Function declarations --------------------------------------------------- #>
 61 | 
 62 | <#
 63 | .Synopsis
 64 | Remove all shell session elements added by the Activate script, including the
 65 | addition of the virtual environment's Python executable from the beginning of
 66 | the PATH variable.
 67 | 
 68 | .Parameter NonDestructive
 69 | If present, do not remove this function from the global namespace for the
 70 | session.
 71 | 
 72 | #>
 73 | function global:deactivate ([switch]$NonDestructive) {
 74 |     # Revert to original values
 75 | 
 76 |     # The prior prompt:
 77 |     if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) {
 78 |         Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt
 79 |         Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT
 80 |     }
 81 | 
 82 |     # The prior PYTHONHOME:
 83 |     if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) {
 84 |         Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME
 85 |         Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME
 86 |     }
 87 | 
 88 |     # The prior PATH:
 89 |     if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) {
 90 |         Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH
 91 |         Remove-Item -Path Env:_OLD_VIRTUAL_PATH
 92 |     }
 93 | 
 94 |     # Just remove the VIRTUAL_ENV altogether:
 95 |     if (Test-Path -Path Env:VIRTUAL_ENV) {
 96 |         Remove-Item -Path env:VIRTUAL_ENV
 97 |     }
 98 | 
 99 |     # Just remove VIRTUAL_ENV_PROMPT altogether.
100 |     if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) {
101 |         Remove-Item -Path env:VIRTUAL_ENV_PROMPT
102 |     }
103 | 
104 |     # Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether:
105 |     if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) {
106 |         Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force
107 |     }
108 | 
109 |     # Leave deactivate function in the global namespace if requested:
110 |     if (-not $NonDestructive) {
111 |         Remove-Item -Path function:deactivate
112 |     }
113 | }
114 | 
115 | <#
116 | .Description
117 | Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the
118 | given folder, and returns them in a map.
119 | 
120 | For each line in the pyvenv.cfg file, if that line can be parsed into exactly
121 | two strings separated by `=` (with any amount of whitespace surrounding the =)
122 | then it is considered a `key = value` line. The left hand string is the key,
123 | the right hand is the value.
124 | 
125 | If the value starts with a `'` or a `"` then the first and last character is
126 | stripped from the value before being captured.
127 | 
128 | .Parameter ConfigDir
129 | Path to the directory that contains the `pyvenv.cfg` file.
130 | #>
131 | function Get-PyVenvConfig(
132 |     [String]
133 |     $ConfigDir
134 | ) {
135 |     Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg"
136 | 
137 |     # Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue).
138 |     $pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue
139 | 
140 |     # An empty map will be returned if no config file is found.
141 |     $pyvenvConfig = @{ }
142 | 
143 |     if ($pyvenvConfigPath) {
144 | 
145 |         Write-Verbose "File exists, parse `key = value` lines"
146 |         $pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath
147 | 
148 |         $pyvenvConfigContent | ForEach-Object {
149 |             $keyval = $PSItem -split "\s*=\s*", 2
150 |             if ($keyval[0] -and $keyval[1]) {
151 |                 $val = $keyval[1]
152 | 
153 |                 # Remove extraneous quotations around a string value.
154 |                 if ("'""".Contains($val.Substring(0, 1))) {
155 |                     $val = $val.Substring(1, $val.Length - 2)
156 |                 }
157 | 
158 |                 $pyvenvConfig[$keyval[0]] = $val
159 |                 Write-Verbose "Adding Key: '$($keyval[0])'='$val'"
160 |             }
161 |         }
162 |     }
163 |     return $pyvenvConfig
164 | }
165 | 
166 | 
167 | <# Begin Activate script --------------------------------------------------- #>
168 | 
169 | # Determine the containing directory of this script
170 | $VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
171 | $VenvExecDir = Get-Item -Path $VenvExecPath
172 | 
173 | Write-Verbose "Activation script is located in path: '$VenvExecPath'"
174 | Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)"
175 | Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)"
176 | 
177 | # Set values required in priority: CmdLine, ConfigFile, Default
178 | # First, get the location of the virtual environment, it might not be
179 | # VenvExecDir if specified on the command line.
180 | if ($VenvDir) {
181 |     Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values"
182 | }
183 | else {
184 |     Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir."
185 |     $VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/")
186 |     Write-Verbose "VenvDir=$VenvDir"
187 | }
188 | 
189 | # Next, read the `pyvenv.cfg` file to determine any required value such
190 | # as `prompt`.
191 | $pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir
192 | 
193 | # Next, set the prompt from the command line, or the config file, or
194 | # just use the name of the virtual environment folder.
195 | if ($Prompt) {
196 |     Write-Verbose "Prompt specified as argument, using '$Prompt'"
197 | }
198 | else {
199 |     Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value"
200 |     if ($pyvenvCfg -and $pyvenvCfg['prompt']) {
201 |         Write-Verbose "  Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'"
202 |         $Prompt = $pyvenvCfg['prompt'];
203 |     }
204 |     else {
205 |         Write-Verbose "  Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)"
206 |         Write-Verbose "  Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'"
207 |         $Prompt = Split-Path -Path $venvDir -Leaf
208 |     }
209 | }
210 | 
211 | Write-Verbose "Prompt = '$Prompt'"
212 | Write-Verbose "VenvDir='$VenvDir'"
213 | 
214 | # Deactivate any currently active virtual environment, but leave the
215 | # deactivate function in place.
216 | deactivate -nondestructive
217 | 
218 | # Now set the environment variable VIRTUAL_ENV, used by many tools to determine
219 | # that there is an activated venv.
220 | $env:VIRTUAL_ENV = $VenvDir
221 | 
222 | if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) {
223 | 
224 |     Write-Verbose "Setting prompt to '$Prompt'"
225 | 
226 |     # Set the prompt to include the env name
227 |     # Make sure _OLD_VIRTUAL_PROMPT is global
228 |     function global:_OLD_VIRTUAL_PROMPT { "" }
229 |     Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT
230 |     New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt
231 | 
232 |     function global:prompt {
233 |         Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) "
234 |         _OLD_VIRTUAL_PROMPT
235 |     }
236 |     $env:VIRTUAL_ENV_PROMPT = $Prompt
237 | }
238 | 
239 | # Clear PYTHONHOME
240 | if (Test-Path -Path Env:PYTHONHOME) {
241 |     Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME
242 |     Remove-Item -Path Env:PYTHONHOME
243 | }
244 | 
245 | # Add the venv to the PATH
246 | Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH
247 | $Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH"
248 | 
```