drupal-rag-mcp 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
File without changes
@@ -0,0 +1,279 @@
1
+ from pathlib import Path
2
+ import subprocess
3
+ import yaml
4
+ from fastapi import FastAPI, HTTPException
5
+ from pydantic import BaseModel
6
+ import uvicorn
7
+ from langchain_chroma import Chroma
8
+ from langchain_huggingface import HuggingFaceEmbeddings
9
+
10
+ from drupal_rag_mcp.config import DB_DIR, DRUPAL_ROOT, ALLOWED_COMMANDS
11
+
12
+ def main():
13
+ uvicorn.run(
14
+ "drupal_rag_mcp.api_server:app",
15
+ host="127.0.0.1",
16
+ port=8000,
17
+ reload=False,
18
+ )
19
+
20
+
21
+
22
+ app = FastAPI()
23
+
24
+
25
+ embedding = HuggingFaceEmbeddings(
26
+ model_name="sentence-transformers/all-MiniLM-L6-v2"
27
+ )
28
+
29
+ vectorstore = Chroma(
30
+ persist_directory=DB_DIR,
31
+ embedding_function=embedding,
32
+ )
33
+
34
+ retriever = vectorstore.as_retriever(search_kwargs={"k": 8})
35
+
36
+ from drupal_rag_mcp.config import (
37
+ ALLOWED_COMMANDS,
38
+ CODE_EXTS,
39
+ DRUPAL_ROOT,
40
+ IGNORE_EXTS,
41
+ IGNORE_DIRS
42
+ )
43
+
44
+ # -------------------------
45
+ # Helpers
46
+ # -------------------------
47
+
48
+ def safe_path(relative_path: str) -> Path:
49
+ """
50
+ Prevents the API from reading/writing outside your Drupal project.
51
+ """
52
+ path = (DRUPAL_ROOT / relative_path).resolve()
53
+
54
+ if not str(path).startswith(str(DRUPAL_ROOT.resolve())):
55
+ raise HTTPException(status_code=400, detail="Unsafe path")
56
+
57
+ return path
58
+
59
+ def should_skip(path: Path) -> bool:
60
+ parts = set(path.relative_to(DRUPAL_ROOT).parts)
61
+ if parts & IGNORE_DIRS:
62
+ return True
63
+ return path.suffix.lower() in IGNORE_EXTS
64
+ # -------------------------
65
+ # Request models
66
+ # -------------------------
67
+
68
+ class SearchRequest(BaseModel):
69
+ query: str
70
+
71
+ class FindRoutesRequest(BaseModel):
72
+ query: str = ""
73
+ max_results: int = 50
74
+
75
+ class FileRequest(BaseModel):
76
+ path: str
77
+
78
+
79
+ class WriteFileRequest(BaseModel):
80
+ path: str
81
+ content: str
82
+
83
+
84
+ class CommandRequest(BaseModel):
85
+ command: list[str]
86
+
87
+ class ListFilesRequest(BaseModel):
88
+ directory: str = "."
89
+ max_files: int = 300
90
+ # -------------------------
91
+ # Tool 1: Search RAG
92
+ # -------------------------
93
+
94
+ @app.post("/search")
95
+ def search_docs(request: SearchRequest):
96
+ docs = retriever.invoke(request.query)
97
+
98
+ return {
99
+ "results": [
100
+ {
101
+ "source": doc.metadata.get("source"),
102
+ "type": doc.metadata.get("type"),
103
+ "content": doc.page_content,
104
+ }
105
+ for doc in docs
106
+ ]
107
+ }
108
+
109
+
110
+ # -------------------------
111
+ # Tool 2: Read file
112
+ # -------------------------
113
+
114
+ @app.post("/read-file")
115
+ def read_file(request: FileRequest):
116
+ path = safe_path(request.path)
117
+
118
+ if not path.exists():
119
+ raise HTTPException(status_code=404, detail="File not found")
120
+
121
+ if not path.is_file():
122
+ raise HTTPException(status_code=400, detail="Path is not a file")
123
+
124
+ return {
125
+ "path": request.path,
126
+ "content": path.read_text(encoding="utf-8"),
127
+ }
128
+
129
+
130
+ # -------------------------
131
+ # Tool 3: Write file
132
+ # -------------------------
133
+
134
+ @app.post("/write-file")
135
+ def write_file(request: WriteFileRequest):
136
+ path = safe_path(request.path)
137
+
138
+ path.parent.mkdir(parents=True, exist_ok=True)
139
+ path.write_text(request.content, encoding="utf-8")
140
+
141
+ return {
142
+ "status": "written",
143
+ "path": request.path,
144
+ }
145
+
146
+
147
+ # -------------------------
148
+ # Tool 4: Run safe command
149
+ # -------------------------
150
+
151
+ @app.post("/run-command")
152
+ def run_command(request: CommandRequest):
153
+ if not request.command:
154
+ raise HTTPException(status_code=400, detail="Empty command")
155
+
156
+ executable = request.command[0]
157
+
158
+ if executable not in ALLOWED_COMMANDS:
159
+ raise HTTPException(
160
+ status_code=403,
161
+ detail=f"Command not allowed: {executable}",
162
+ )
163
+
164
+ result = subprocess.run(
165
+ request.command,
166
+ cwd=DRUPAL_ROOT,
167
+ text=True,
168
+ capture_output=True,
169
+ timeout=60,
170
+ )
171
+
172
+ return {
173
+ "command": request.command,
174
+ "returncode": result.returncode,
175
+ "stdout": result.stdout,
176
+ "stderr": result.stderr,
177
+ }
178
+
179
+ # -------------------------
180
+ # Tool 5: List Project Files
181
+ # -------------------------
182
+
183
+ @app.post("/list-files")
184
+ def list_files(request: ListFilesRequest):
185
+ directory = safe_path(request.directory)
186
+
187
+ if not directory.exists():
188
+ raise HTTPException(status_code=404, detail="Directory not found")
189
+
190
+ if not directory.is_dir():
191
+ raise HTTPException(status_code=400, detail="Path is not a directory")
192
+
193
+ files: list[str] = []
194
+
195
+ for path in directory.rglob("*"):
196
+ if len(files) >= request.max_files:
197
+ break
198
+
199
+ if not path.is_file():
200
+ continue
201
+
202
+ if should_skip(path):
203
+ continue
204
+
205
+ if path.suffix.lower() not in CODE_EXTS:
206
+ continue
207
+
208
+ files.append(str(path.relative_to(DRUPAL_ROOT)))
209
+
210
+ return {
211
+ "root": str(DRUPAL_ROOT),
212
+ "directory": request.directory,
213
+ "count": len(files),
214
+ "files": files,
215
+ }
216
+
217
+ # -------------------------
218
+ # Tool 6: Find - Routes
219
+ # -------------------------
220
+ @app.post("/find-routes")
221
+ def find_routes(request: FindRoutesRequest):
222
+ routes = []
223
+
224
+ for path in DRUPAL_ROOT.rglob("*.routing.yml"):
225
+ if should_skip(path):
226
+ continue
227
+
228
+ try:
229
+ data = yaml.safe_load(path.read_text(encoding="utf-8"))
230
+ except Exception:
231
+ continue
232
+
233
+ if not isinstance(data, dict):
234
+ continue
235
+
236
+ for route_name, route_info in data.items():
237
+ if not isinstance(route_info, dict):
238
+ continue
239
+
240
+ route_path = route_info.get("path", "")
241
+ defaults = route_info.get("defaults", {})
242
+ requirements = route_info.get("requirements", {})
243
+
244
+ controller = defaults.get("_controller", "")
245
+ form = defaults.get("_form", "")
246
+ title = defaults.get("_title", "")
247
+ permission = requirements.get("_permission", "")
248
+
249
+ searchable = " ".join([
250
+ str(route_name),
251
+ str(route_path),
252
+ str(controller),
253
+ str(form),
254
+ str(title),
255
+ str(permission),
256
+ str(path.relative_to(DRUPAL_ROOT)),
257
+ ]).lower()
258
+
259
+ if request.query and request.query.lower() not in searchable:
260
+ continue
261
+
262
+ routes.append({
263
+ "route_name": route_name,
264
+ "path": route_path,
265
+ "controller": controller,
266
+ "form": form,
267
+ "title": title,
268
+ "permission": permission,
269
+ "source": str(path.relative_to(DRUPAL_ROOT)),
270
+ })
271
+
272
+ if len(routes) >= request.max_results:
273
+ break
274
+
275
+ return {
276
+ "query": request.query,
277
+ "count": len(routes),
278
+ "routes": routes,
279
+ }
@@ -0,0 +1,56 @@
1
+ from pathlib import Path
2
+ import os
3
+ import tomllib
4
+ from dotenv import load_dotenv
5
+
6
+ load_dotenv()
7
+
8
+ CONFIG_FILE = Path.home() / ".drupal-rag" / "config.toml"
9
+
10
+ DOC_EXTS = {".md", ".txt", ".rst"}
11
+
12
+ CODE_EXTS = {
13
+ ".php", ".module", ".inc", ".install",
14
+ ".theme", ".yml", ".twig", ".js", ".css"
15
+ }
16
+
17
+ IGNORE_EXTS = {
18
+ ".sqlite", ".gz", ".zip", ".tar",
19
+ ".jpg", ".jpeg", ".png", ".gif",
20
+ ".pdf", ".mp4", ".mov", ".sql"
21
+ }
22
+
23
+ IGNORE_DIRS = {
24
+ "vendor",
25
+ "node_modules",
26
+ ".git",
27
+ "web/sites/default/files",
28
+ }
29
+
30
+ ALLOWED_COMMANDS = {
31
+ "phpunit",
32
+ "composer",
33
+ "drush",
34
+ "vendor/bin/phpunit",
35
+ }
36
+
37
+ config = {}
38
+ if CONFIG_FILE.exists():
39
+ config = tomllib.loads(CONFIG_FILE.read_text())
40
+
41
+ drupal_root_value = os.getenv("DRUPAL_ROOT") or config.get("drupal_root")
42
+
43
+ if not drupal_root_value:
44
+ raise RuntimeError(
45
+ "DRUPAL_ROOT is not set. Set it in your MCP env config or in ~/.drupal-rag/config.toml"
46
+ )
47
+
48
+ DRUPAL_ROOT = Path(drupal_root_value).expanduser().resolve()
49
+
50
+ DB_DIR = Path(
51
+ os.getenv("DB_DIR")
52
+ or config.get("db_dir")
53
+ or "~/.drupal-rag/chroma"
54
+ ).expanduser().resolve()
55
+
56
+ RAG_API = os.getenv("RAG_API") or config.get("api_url") or "http://127.0.0.1:8000"
@@ -0,0 +1,131 @@
1
+ # --------------------------------------------------
2
+ # Import Libraries
3
+ # Document: Every file we load will be converted into a Document so LangChain can process it consistently
4
+ # RecursiveCharacterTextSplitter: Splits large files into smaller chunks before embedding.
5
+ # Chroma is our vector database
6
+ # HuggingFaceEmbeddings: Converts text into vectors without requiring OpenAI
7
+ # --------------------------------------------------
8
+ from langchain_core.documents import Document
9
+ from langchain_text_splitters import RecursiveCharacterTextSplitter
10
+ from langchain_chroma import Chroma
11
+ from langchain_community.embeddings import HuggingFaceEmbeddings
12
+
13
+ # --------------------------------------------------
14
+ # Import and use configurations
15
+ # --------------------------------------------------
16
+ from drupal_rag_mcp.config import (
17
+ DRUPAL_ROOT,
18
+ DB_DIR,
19
+ DOC_EXTS,
20
+ CODE_EXTS,
21
+ IGNORE_DIRS,
22
+ IGNORE_EXTS
23
+ )
24
+
25
+ # --------------------------------------------------
26
+ # Create embedding model: This model converts text into numerical vectors
27
+ # --------------------------------------------------
28
+ embedding = HuggingFaceEmbeddings(
29
+ model_name="sentence-transformers/all-MiniLM-L6-v2"
30
+ )
31
+
32
+ # --------------------------------------------------
33
+ # Determine if IGNORE_DIRS and IGNORE_EXTS should be skipped
34
+ # --------------------------------------------------
35
+ def should_skip(path):
36
+ return any(part in IGNORE_DIRS for part in path.parts)
37
+
38
+ def should_skip_file(path):
39
+ return path.suffix.lower() in IGNORE_EXTS
40
+
41
+ # --------------------------------------------------
42
+ # Load Drupal files into LangChain Documents: Converts files into Document objects
43
+ # --------------------------------------------------
44
+ def load_files():
45
+ docs = []
46
+ # Recursively search every file and directory
47
+ for path in DRUPAL_ROOT.rglob("*"):
48
+ if not path.is_file():
49
+ continue
50
+
51
+ if should_skip(path):
52
+ continue
53
+
54
+ if should_skip_file(path):
55
+ continue
56
+
57
+ if path.suffix in DOC_EXTS or path.suffix in CODE_EXTS:
58
+ try:
59
+ text = path.read_text(encoding="utf-8")
60
+ except UnicodeDecodeError:
61
+ continue
62
+ # Create a LangChain Document.
63
+ docs.append(
64
+ Document(
65
+ page_content=text,
66
+ metadata={
67
+ "source": str(path.relative_to(DRUPAL_ROOT)),
68
+ "extension": path.suffix,
69
+ "type": "doc" if path.suffix in DOC_EXTS else "code",
70
+ },
71
+ )
72
+ )
73
+
74
+ return docs
75
+
76
+
77
+
78
+ # --------------------------------------------------
79
+ # Split documents into chunks for easy retrieval later
80
+ # --------------------------------------------------
81
+ splitter = RecursiveCharacterTextSplitter(
82
+ chunk_size=1200,
83
+ chunk_overlap=200,
84
+ separators=[
85
+ "\n## ",
86
+ "\n### ",
87
+ "\nfunction ",
88
+ "\nclass ",
89
+ "\npublic function ",
90
+ "\n\n",
91
+ "\n",
92
+ " ",
93
+ "",
94
+ ],
95
+ )
96
+ # Split all documents into chunks.
97
+
98
+
99
+
100
+ def main():
101
+ documents = load_files()
102
+
103
+ splitter = RecursiveCharacterTextSplitter(
104
+ chunk_size=1200,
105
+ chunk_overlap=200,
106
+ separators=[
107
+ "\n## ",
108
+ "\n### ",
109
+ "\nfunction ",
110
+ "\nclass ",
111
+ "\npublic function ",
112
+ "\n\n",
113
+ "\n",
114
+ " ",
115
+ "",
116
+ ],
117
+ )
118
+
119
+ chunks = splitter.split_documents(documents)
120
+
121
+ Chroma.from_documents(
122
+ documents=chunks,
123
+ embedding=embedding,
124
+ persist_directory=str(DB_DIR),
125
+ )
126
+
127
+ print(f"Indexed {len(chunks)} chunks into {DB_DIR}")
128
+
129
+
130
+ if __name__ == "__main__":
131
+ main()
@@ -0,0 +1,82 @@
1
+ import requests
2
+ from mcp.server.fastmcp import FastMCP
3
+
4
+ mcp = FastMCP("drupal-rag")
5
+
6
+ RAG_API = "http://127.0.0.1:8000"
7
+
8
+
9
+ @mcp.tool()
10
+ def search_drupal_docs(query: str) -> dict:
11
+ """Search the local Drupal RAG knowledge base."""
12
+ response = requests.post(
13
+ f"{RAG_API}/search",
14
+ json={"query": query},
15
+ timeout=30,
16
+ )
17
+ response.raise_for_status()
18
+ return response.json()
19
+
20
+
21
+ @mcp.tool()
22
+ def read_drupal_file(path: str) -> dict:
23
+ """Read a file from the Drupal project using a relative path."""
24
+ response = requests.post(
25
+ f"{RAG_API}/read-file",
26
+ json={"path": path},
27
+ timeout=30,
28
+ )
29
+ response.raise_for_status()
30
+ return response.json()
31
+
32
+
33
+ @mcp.tool()
34
+ def write_drupal_file(path: str, content: str) -> dict:
35
+ """Write a file in the Drupal project using a relative path."""
36
+ response = requests.post(
37
+ f"{RAG_API}/write-file",
38
+ json={"path": path, "content": content},
39
+ timeout=30,
40
+ )
41
+ response.raise_for_status()
42
+ return response.json()
43
+
44
+
45
+ @mcp.tool()
46
+ def run_drupal_command(command: list[str]) -> dict:
47
+ """Run an allowed Drupal command such as drush, composer, or phpunit."""
48
+ response = requests.post(
49
+ f"{RAG_API}/run-command",
50
+ json={"command": command},
51
+ timeout=120,
52
+ )
53
+ response.raise_for_status()
54
+ return response.json()
55
+
56
+ @mcp.tool()
57
+ def list_project_files(directory: str = ".", max_files: int = 300) -> dict:
58
+ """List code and text files in the project. No embeddings or vector database are used."""
59
+ response = requests.post(
60
+ f"{RAG_API}/list-files",
61
+ json={"directory": directory, "max_files": max_files},
62
+ timeout=30,
63
+ )
64
+ response.raise_for_status()
65
+ return response.json()
66
+
67
+ @mcp.tool()
68
+ def find_routes(query: str = "", max_results: int = 50) -> dict:
69
+ """Find Drupal routes by route name, path, controller, form, title, permission, or routing file."""
70
+ response = requests.post(
71
+ f"{RAG_API}/find-routes",
72
+ json={"query": query, "max_results": max_results},
73
+ timeout=30,
74
+ )
75
+ response.raise_for_status()
76
+ return response.json()
77
+
78
+ def main():
79
+ mcp.run()
80
+
81
+ if __name__ == "__main__":
82
+ main()
@@ -0,0 +1,371 @@
1
+ Metadata-Version: 2.4
2
+ Name: drupal-rag-mcp
3
+ Version: 0.1.0
4
+ Summary: MCP server for Drupal RAG, file access, routes, and safe Drupal commands
5
+ Requires-Python: >=3.10
6
+ Description-Content-Type: text/markdown
7
+ License-File: LICENSE
8
+ Requires-Dist: mcp>=1.27.2
9
+ Requires-Dist: requests>=2.34.2
10
+ Requires-Dist: python-dotenv>=1.2.2
11
+ Requires-Dist: PyYAML>=6.0.3
12
+ Requires-Dist: psutil>=7.2.2
13
+ Requires-Dist: ipython>=9.14.1
14
+ Provides-Extra: rag
15
+ Requires-Dist: chromadb>=1.5.9; extra == "rag"
16
+ Requires-Dist: langchain>=1.3.2; extra == "rag"
17
+ Requires-Dist: langchain-chroma>=1.1.0; extra == "rag"
18
+ Requires-Dist: langchain-community>=0.4.2; extra == "rag"
19
+ Requires-Dist: langchain-core>=1.4.0; extra == "rag"
20
+ Requires-Dist: langchain-huggingface>=1.2.2; extra == "rag"
21
+ Requires-Dist: langchain-text-splitters>=1.1.2; extra == "rag"
22
+ Requires-Dist: sentence-transformers>=5.5.1; extra == "rag"
23
+ Requires-Dist: fastapi>=0.136.3; extra == "rag"
24
+ Requires-Dist: uvicorn>=0.48.0; extra == "rag"
25
+ Dynamic: license-file
26
+
27
+ # Drupal RAG MCP
28
+ <img src="media/screenshot.png" alt="Drupal Agent Screenshot" style="max-width: 150px;">
29
+
30
+ A Model Context Protocol (MCP) server that gives AI coding assistants direct access to Drupal projects through:
31
+
32
+ * Drupal codebase search (RAG)
33
+ * File reading
34
+ * File writing
35
+ * Drupal route discovery
36
+ * Safe Drupal command execution
37
+ * Project file listing
38
+
39
+ This server is designed for use with MCP-compatible clients such as:
40
+
41
+ * Claude Desktop
42
+ * Claude Code
43
+ * Cursor
44
+ * Windsurf
45
+ * Cline
46
+ * OpenAI Codex CLI
47
+ * Other MCP-compatible tools
48
+
49
+ ---
50
+
51
+ ## Features
52
+
53
+ ### Search Drupal Knowledge Base
54
+
55
+ Search indexed Drupal source code and documentation using semantic search.
56
+
57
+ Tool:
58
+
59
+ ```text
60
+ search_drupal_docs
61
+ ```
62
+
63
+ ### Read Drupal Files
64
+
65
+ Read any file within the configured Drupal project.
66
+
67
+ Tool:
68
+
69
+ ```text
70
+ read_drupal_file
71
+ ```
72
+
73
+ ### Write Drupal Files
74
+
75
+ Create or update files inside the Drupal project.
76
+
77
+ Tool:
78
+
79
+ ```text
80
+ write_drupal_file
81
+ ```
82
+
83
+ ### Run Drupal Commands
84
+
85
+ Execute approved Drupal development commands.
86
+
87
+ Supported commands:
88
+
89
+ * drush
90
+ * composer
91
+ * phpunit
92
+
93
+ Tool:
94
+
95
+ ```text
96
+ run_drupal_command
97
+ ```
98
+
99
+ ### List Project Files
100
+
101
+ Browse project source files without using embeddings.
102
+
103
+ Tool:
104
+
105
+ ```text
106
+ list_project_files
107
+ ```
108
+
109
+ ### Discover Drupal Routes
110
+
111
+ Search Drupal routes by:
112
+
113
+ * Route name
114
+ * Path
115
+ * Controller
116
+ * Form
117
+ * Permission
118
+ * Title
119
+
120
+ Tool:
121
+
122
+ ```text
123
+ find_routes
124
+ ```
125
+
126
+ ---
127
+
128
+ # Architecture
129
+
130
+ ```text
131
+ AI Client
132
+
133
+
134
+ Drupal MCP Server
135
+
136
+
137
+ Drupal RAG API
138
+
139
+ ├── Chroma Vector Database
140
+ ├── Drupal Source Code
141
+ └── Drupal Documentation
142
+ ```
143
+
144
+ The MCP server communicates with a local FastAPI service that performs indexing, search, file operations, and command execution.
145
+
146
+ ---
147
+
148
+ # Installation
149
+
150
+ Install from PyPI:
151
+
152
+ ```bash
153
+ pip install drupal-rag-mcp
154
+ ```
155
+
156
+ Or install with RAG dependencies:
157
+
158
+ ```bash
159
+ pip install "drupal-rag-mcp[rag]"
160
+ ```
161
+
162
+ ---
163
+
164
+ # Requirements
165
+
166
+ * Python 3.10+
167
+ * Drupal project
168
+ * Composer
169
+ * Drush (optional)
170
+ * ChromaDB
171
+
172
+ ---
173
+
174
+
175
+ # Build Your Drupal RAG
176
+ Create a .drupal-rag folder under your home directory
177
+ ```bash
178
+ mkdir ~/.drupal-rag
179
+ cd ~/.drupal-rag
180
+ touch config.toml
181
+ ```
182
+ Paste this settings
183
+ ```toml
184
+ drupal_root = "/path_to_drupal_project_directory"
185
+ db_dir = "~/.drupal-rag/chroma"
186
+ ```
187
+
188
+ Inside ~/.drupal-rag
189
+ ```bash
190
+ mkdir docs
191
+ ```
192
+
193
+ Inside docs add all your drupal documentation (PDF, MD, txt).
194
+ Start indexing your docs to create rag
195
+
196
+ ```bash
197
+ drupal-rag-index
198
+ ```
199
+
200
+ This will:
201
+
202
+ 1. Scan Drupal files
203
+ 2. Split documents into chunks
204
+ 3. Generate embeddings
205
+ 4. Store vectors in Chroma
206
+
207
+ ---
208
+
209
+ # Start the API Server
210
+
211
+ ```bash
212
+ drupal-rag-api
213
+ ```
214
+
215
+ The API provides:
216
+
217
+ ```text
218
+ POST /search
219
+ POST /read-file
220
+ POST /write-file
221
+ POST /run-command
222
+ POST /list-files
223
+ POST /find-routes
224
+ ```
225
+
226
+ ---
227
+
228
+ # Start the MCP Server
229
+
230
+ ```bash
231
+ drupal-rag-mcp
232
+ ```
233
+
234
+ The MCP server uses stdio transport and is intended to be launched by an MCP client.
235
+
236
+ ---
237
+
238
+ # MCP Client Configuration
239
+
240
+ ## Claude Desktop
241
+
242
+ ```json
243
+ {
244
+ "mcpServers": {
245
+ "drupal-rag": {
246
+ "command": "drupal-rag-mcp"
247
+ }
248
+ }
249
+ }
250
+ ```
251
+
252
+ ## Cursor
253
+
254
+ ```json
255
+ {
256
+ "mcpServers": {
257
+ "drupal-rag": {
258
+ "command": "drupal-rag-mcp"
259
+ }
260
+ }
261
+ }
262
+ ```
263
+
264
+ ## VSCODE
265
+
266
+ ### Installing and Running Ollama on Mac
267
+ ```bash
268
+ brew install ollama
269
+ brew services start ollama
270
+ brew services stop ollama
271
+ ```
272
+
273
+ ### Run and Download an AI Model
274
+ ```bash
275
+ ollama pull qwen3
276
+ ollama run qwen3
277
+ ```
278
+
279
+ ### Set Up Continue
280
+ 1. Add Extension Continue
281
+ 2. Configure Continue config.yaml
282
+ ```yml
283
+ name: Local Config
284
+ version: 1.0.0
285
+ schema: v1
286
+
287
+ models:
288
+ - name: Qwen3
289
+ provider: ollama
290
+ model: qwen3
291
+ ```
292
+
293
+ 3. Add MCP server in Continue
294
+
295
+ ```bash
296
+ which drupal-rag-mcp
297
+ ```
298
+ Example output: `/opt/anaconda3/envs/drupal_rag/bin/drupal-rag-mcp`
299
+
300
+ Configure Continue mcp-server.yaml
301
+ ```yml
302
+ name: Drupal RAG MCP
303
+ version: 0.0.1
304
+ schema: v1
305
+
306
+ mcpServers:
307
+ - name: drupal-rag
308
+ command: /opt/anaconda3/envs/drupal_rag/bin/drupal-rag-mcp
309
+ args: []
310
+ env:
311
+ DRUPAL_ROOT: /path_to_your_drupal
312
+ ```
313
+
314
+
315
+ ## Codex CLI
316
+
317
+ ```toml
318
+ [mcp_servers.drupal-rag]
319
+ command = "drupal-rag-mcp"
320
+ args = []
321
+ ```
322
+
323
+ ---
324
+
325
+ # Example Prompts
326
+
327
+ ```text
328
+ Find the route responsible for user login.
329
+ ```
330
+
331
+ ```text
332
+ Search Drupal documentation for cache tags.
333
+ ```
334
+
335
+ ```text
336
+ Show me all custom routes under /admin.
337
+ ```
338
+
339
+ ```text
340
+ Read web/modules/custom/example/src/Form/ExampleForm.php
341
+ ```
342
+
343
+ ```text
344
+ Run drush cr
345
+ ```
346
+
347
+ ---
348
+
349
+ # Security
350
+
351
+ The server includes several safeguards:
352
+
353
+ * Path traversal protection
354
+ * Drupal root restriction
355
+ * Command allow-list
356
+ * Ignored directories and file types
357
+ * Local-only API access
358
+
359
+ Only approved commands can be executed.
360
+
361
+ Review the source code before enabling write access in production environments.
362
+
363
+ ---
364
+
365
+ # License
366
+
367
+ MIT License
368
+
369
+ Copyright (c) 2026 John Paul Mariano
370
+
371
+ See the LICENSE file for full license text.
@@ -0,0 +1,11 @@
1
+ drupal_rag_mcp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ drupal_rag_mcp/api_server.py,sha256=3kQFRgbjsxgg9RV8duajQ6H6Dsb5k2tH4NisDSTir1Q,6750
3
+ drupal_rag_mcp/config.py,sha256=oc7DzrdQNqCPET8mgq-_0cVTxMSm9OmMNNReoX2oOCo,1201
4
+ drupal_rag_mcp/index.py,sha256=O3pGzxjj1qqjz1h4Coqx6XExWxgKnvoyBk-uh0Tc2HU,3821
5
+ drupal_rag_mcp/mcp_server.py,sha256=KLIMtVLQgZotwmonKvOeTQHv2XYhuHkGL9rgq4jKfNg,2218
6
+ drupal_rag_mcp-0.1.0.dist-info/licenses/LICENSE,sha256=jj6Zey70W4se0XC-iAe2-0PWCO6vXKFRKEZc-bDmod0,1073
7
+ drupal_rag_mcp-0.1.0.dist-info/METADATA,sha256=cNepwptAyvDCzf9b5pQYrem6MoPDQ5SIYzkIgGcilZU,5724
8
+ drupal_rag_mcp-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
9
+ drupal_rag_mcp-0.1.0.dist-info/entry_points.txt,sha256=7yq7PMXDo_fouNXYwnt6MlTy-dCo2bPvYANbEBPGH7Q,159
10
+ drupal_rag_mcp-0.1.0.dist-info/top_level.txt,sha256=oT9RQCaIOn7pLaTuL9tQt0vtw9DzluAiXaU9eZKCH0I,15
11
+ drupal_rag_mcp-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,4 @@
1
+ [console_scripts]
2
+ drupal-rag-api = drupal_rag_mcp.api_server:main
3
+ drupal-rag-index = drupal_rag_mcp.index:main
4
+ drupal-rag-mcp = drupal_rag_mcp.mcp_server:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 John Paul Mariano
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ drupal_rag_mcp