codegraphcontext 0.1.5__tar.gz → 0.1.7__tar.gz

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.
Files changed (30) hide show
  1. {codegraphcontext-0.1.5/src/codegraphcontext.egg-info → codegraphcontext-0.1.7}/PKG-INFO +4 -1
  2. {codegraphcontext-0.1.5 → codegraphcontext-0.1.7}/README.md +3 -1
  3. {codegraphcontext-0.1.5 → codegraphcontext-0.1.7}/pyproject.toml +2 -1
  4. codegraphcontext-0.1.7/src/codegraphcontext/cli/main.py +93 -0
  5. {codegraphcontext-0.1.5 → codegraphcontext-0.1.7}/src/codegraphcontext/cli/setup_wizard.py +139 -16
  6. codegraphcontext-0.1.7/src/codegraphcontext/core/watcher.py +141 -0
  7. {codegraphcontext-0.1.5 → codegraphcontext-0.1.7}/src/codegraphcontext/prompts.py +1 -0
  8. {codegraphcontext-0.1.5 → codegraphcontext-0.1.7}/src/codegraphcontext/server.py +37 -16
  9. codegraphcontext-0.1.7/src/codegraphcontext/tools/graph_builder.py +1018 -0
  10. {codegraphcontext-0.1.5 → codegraphcontext-0.1.7}/src/codegraphcontext/tools/import_extractor.py +15 -13
  11. {codegraphcontext-0.1.5 → codegraphcontext-0.1.7/src/codegraphcontext.egg-info}/PKG-INFO +4 -1
  12. {codegraphcontext-0.1.5 → codegraphcontext-0.1.7}/src/codegraphcontext.egg-info/requires.txt +1 -0
  13. codegraphcontext-0.1.5/src/codegraphcontext/cli/main.py +0 -66
  14. codegraphcontext-0.1.5/src/codegraphcontext/core/watcher.py +0 -119
  15. codegraphcontext-0.1.5/src/codegraphcontext/tools/graph_builder.py +0 -732
  16. {codegraphcontext-0.1.5 → codegraphcontext-0.1.7}/LICENSE +0 -0
  17. {codegraphcontext-0.1.5 → codegraphcontext-0.1.7}/setup.cfg +0 -0
  18. {codegraphcontext-0.1.5 → codegraphcontext-0.1.7}/src/codegraphcontext/__init__.py +0 -0
  19. {codegraphcontext-0.1.5 → codegraphcontext-0.1.7}/src/codegraphcontext/__main__.py +0 -0
  20. {codegraphcontext-0.1.5 → codegraphcontext-0.1.7}/src/codegraphcontext/cli/__init__.py +0 -0
  21. {codegraphcontext-0.1.5 → codegraphcontext-0.1.7}/src/codegraphcontext/core/__init__.py +0 -0
  22. {codegraphcontext-0.1.5 → codegraphcontext-0.1.7}/src/codegraphcontext/core/database.py +0 -0
  23. {codegraphcontext-0.1.5 → codegraphcontext-0.1.7}/src/codegraphcontext/core/jobs.py +0 -0
  24. {codegraphcontext-0.1.5 → codegraphcontext-0.1.7}/src/codegraphcontext/tools/__init__.py +0 -0
  25. {codegraphcontext-0.1.5 → codegraphcontext-0.1.7}/src/codegraphcontext/tools/code_finder.py +0 -0
  26. {codegraphcontext-0.1.5 → codegraphcontext-0.1.7}/src/codegraphcontext/tools/system.py +0 -0
  27. {codegraphcontext-0.1.5 → codegraphcontext-0.1.7}/src/codegraphcontext.egg-info/SOURCES.txt +0 -0
  28. {codegraphcontext-0.1.5 → codegraphcontext-0.1.7}/src/codegraphcontext.egg-info/dependency_links.txt +0 -0
  29. {codegraphcontext-0.1.5 → codegraphcontext-0.1.7}/src/codegraphcontext.egg-info/entry_points.txt +0 -0
  30. {codegraphcontext-0.1.5 → codegraphcontext-0.1.7}/src/codegraphcontext.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codegraphcontext
3
- Version: 0.1.5
3
+ Version: 0.1.7
4
4
  Summary: An MCP server that indexes local code into a graph database to provide context to AI assistants.
5
5
  Author-email: Shashank Shekhar Singh <shashankshekharsingh1205@gmail.com>
6
6
  License: MIT License
@@ -47,9 +47,12 @@ Requires-Dist: python-dotenv>=1.0.0
47
47
  Provides-Extra: dev
48
48
  Requires-Dist: pytest>=7.4.0; extra == "dev"
49
49
  Requires-Dist: black>=23.11.0; extra == "dev"
50
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
50
51
  Dynamic: license-file
51
52
 
52
53
  # CodeGraphContext
54
+ [![Build Status](https://github.com/Shashankss1205/CodeGraphContext/actions/workflows/test.yml/badge.svg)](https://github.com/Shashankss1205/CodeGraphContext/actions/workflows/test.yml)
55
+
53
56
 
54
57
  An MCP server that indexes local code into a graph database to provide context to AI assistants.
55
58
 
@@ -1,4 +1,6 @@
1
1
  # CodeGraphContext
2
+ [![Build Status](https://github.com/Shashankss1205/CodeGraphContext/actions/workflows/test.yml/badge.svg)](https://github.com/Shashankss1205/CodeGraphContext/actions/workflows/test.yml)
3
+
2
4
 
3
5
  An MCP server that indexes local code into a graph database to provide context to AI assistants.
4
6
 
@@ -96,4 +98,4 @@ Once the server is running, you can interact with it through your AI assistant u
96
98
  - "Find all implementations of the `render` method."
97
99
 
98
100
  - **Code Quality and Maintenance:**
99
- - "Is there any dead or unused code in this project?"
101
+ - "Is there any dead or unused code in this project?"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "codegraphcontext"
3
- version = "0.1.5"
3
+ version = "0.1.7"
4
4
  description = "An MCP server that indexes local code into a graph database to provide context to AI assistants."
5
5
  authors = [{ name = "Shashank Shekhar Singh", email = "shashankshekharsingh1205@gmail.com" }]
6
6
  readme = "README.md"
@@ -36,6 +36,7 @@ cgc = "codegraphcontext.cli.main:app"
36
36
  dev = [
37
37
  "pytest>=7.4.0",
38
38
  "black>=23.11.0",
39
+ "pytest-asyncio>=0.21.0",
39
40
  ]
40
41
 
41
42
  [tool.setuptools]
@@ -0,0 +1,93 @@
1
+ # src/codegraphcontext/cli/main.py
2
+ import typer
3
+ from rich.console import Console
4
+ import asyncio
5
+ import logging
6
+ from codegraphcontext.server import MCPServer
7
+ from .setup_wizard import run_setup_wizard
8
+
9
+ # Set the log level for the noisy neo4j logger to WARNING
10
+ logging.getLogger("neo4j").setLevel(logging.WARNING) # <-- ADD THIS LINE
11
+
12
+ app = typer.Typer(
13
+ name="cgc",
14
+ help="CodeGraphContext: An MCP server for AI-powered code analysis.",
15
+ add_completion=False,
16
+ )
17
+ console = Console()
18
+
19
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(name)s - %(message)s')
20
+
21
+ @app.command()
22
+ def setup():
23
+ """
24
+ Run the interactive setup wizard to configure the server and database.
25
+ """
26
+ run_setup_wizard()
27
+
28
+ @app.command()
29
+ def start():
30
+ """
31
+ Start the CodeGraphContext MCP server.
32
+ """
33
+ console.print("[bold green]Starting CodeGraphContext Server...[/bold green]")
34
+ server = None
35
+ loop = asyncio.new_event_loop()
36
+ asyncio.set_event_loop(loop)
37
+ try:
38
+ server = MCPServer(loop=loop)
39
+ loop.run_until_complete(server.run())
40
+ except ValueError as e:
41
+ console.print(f"[bold red]Configuration Error:[/bold red] {e}")
42
+ console.print("Please run `cgc setup` to configure the server.")
43
+ except KeyboardInterrupt:
44
+ console.print("\n[bold yellow]Server stopped by user.[/bold yellow]")
45
+ finally:
46
+ if server:
47
+ server.shutdown()
48
+ loop.close()
49
+
50
+
51
+ @app.command()
52
+ def tool(
53
+ name: str = typer.Argument(..., help="The name of the tool to call."),
54
+ args: str = typer.Argument("{}", help="A JSON string of arguments for the tool."),
55
+ ):
56
+ """
57
+ Directly call a CodeGraphContext tool.
58
+ Note: This command instantiates a new, independent MCP server instance for each call.
59
+ Therefore, it does not share state (like job IDs) with a server started via `cgc start`.
60
+
61
+ This command can be used for:\n
62
+ - `add_code_to_graph`: Index a new project or directory. Args: `path` (str), `is_dependency` (bool, optional)\n
63
+ - `add_package_to_graph`: Add a Python package to the graph. Args: `package_name` (str), `is_dependency` (bool, optional)\n
64
+ - `find_code`: Search for code snippets. Args: `query` (str)\n
65
+ - `analyze_code_relationships`: Analyze code relationships (e.g., callers, callees). Args: `query_type` (str), `target` (str), `context` (str, optional)\n
66
+ - `watch_directory`: Start watching a directory for changes. Args: `path` (str)\n
67
+ - `execute_cypher_query`: Run direct Cypher queries. Args: `cypher_query` (str)\n
68
+ - `list_imports`: List imports from files. Args: `path` (str), `language` (str, optional), `recursive` (bool, optional)\n
69
+ - `find_dead_code`: Find potentially unused functions. Args: None\n
70
+ - `calculate_cyclomatic_complexity`: Calculate function complexity. Args: `function_name` (str), `file_path` (str, optional)\n
71
+ - `find_most_complex_functions`: Find the most complex functions. Args: `limit` (int, optional)\n
72
+ - `list_indexed_repositories`: List indexed repositories. Args: None\n
73
+ - `delete_repository`: Delete an indexed repository. Args: `repo_path` (str)
74
+ """
75
+
76
+ # This is a placeholder for a more advanced tool caller that would
77
+ # connect to the running server via a different mechanism (e.g., a socket).
78
+ # For now, it's a conceptual part of the CLI.
79
+ console.print(f"Calling tool [bold cyan]{name}[/bold cyan] with args: {args}")
80
+ console.print("[yellow]Note: This is a placeholder for direct tool invocation.[/yellow]")
81
+
82
+
83
+ @app.command(name="help")
84
+ def show_help():
85
+ """
86
+ Show a list of available commands and their descriptions.
87
+ """
88
+ console.print("[bold]CodeGraphContext CLI Commands:[/bold]")
89
+ for command in app.registered_commands:
90
+ # Get the first line of the docstring as a short description
91
+ description = command.help.split('\n')[0].strip() if command.help else "No description."
92
+ console.print(f" [bold cyan]{command.name}[/bold cyan]: {description}")
93
+ console.print("\nFor more information on a specific command, run: [bold]cgc <command> --help[/bold]")
@@ -38,7 +38,9 @@ def _generate_mcp_json(creds):
38
38
  "list_imports", "add_code_to_graph", "add_package_to_graph",
39
39
  "check_job_status", "list_jobs", "find_code",
40
40
  "analyze_code_relationships", "watch_directory",
41
- "find_dead_code", "execute_cypher_query"
41
+ "find_dead_code", "execute_cypher_query",
42
+ "calculate_cyclomatic_complexity", "find_most_complex_functions",
43
+ "list_indexed_repositories", "delete_repository"
42
44
  ],
43
45
  "disabled": False
44
46
  },
@@ -99,7 +101,7 @@ def run_setup_wizard():
99
101
  questions = [
100
102
  {
101
103
  "type": "list",
102
- "message": "Where is your Neo4j database located?",
104
+ "message": "Where is your Neo4j database located? We can help you get one, if you don't have.",
103
105
  "choices": [
104
106
  "Local (Recommended: I'll help you run it on this machine)",
105
107
  "Hosted (Connect to a remote database like AuraDB)",
@@ -227,20 +229,141 @@ def setup_local_db():
227
229
  setup_local_binary()
228
230
 
229
231
  def setup_docker():
230
- """Creates Docker files and runs docker-compose."""
231
- console.print("This will create a `docker-compose.yml` and `.env` file in your current directory.")
232
- # Here you would write the file contents
233
- console.print("[green]docker-compose.yml and .env created.[/green]")
234
- console.print("Please set your NEO4J_PASSWORD in the .env file.")
235
- confirm_q = [{"type": "confirm", "message": "Ready to launch Docker containers?", "name": "proceed"}]
236
- if prompt(confirm_q).get("proceed"):
237
- try:
238
- # Using our run_command to handle the subprocess call
239
- docker_process = run_command(["docker", "compose", "up", "-d"], console, check=True)
240
- if docker_process:
241
- console.print("[bold green]Docker containers started successfully![/bold green]")
242
- except Exception as e:
243
- console.print(f"[bold red]Failed to start Docker containers:[/bold red] {e}")
232
+ """Creates Docker files and runs docker-compose for Neo4j."""
233
+ console.print("\n[bold cyan]Setting up Neo4j with Docker...[/bold cyan]")
234
+
235
+ # Prompt for password first
236
+ console.print("Please set a secure password for your Neo4j database:")
237
+ password_questions = [
238
+ {"type": "password", "message": "Enter Neo4j password:", "name": "password"},
239
+ {"type": "password", "message": "Confirm password:", "name": "password_confirm"},
240
+ ]
241
+
242
+ while True:
243
+ passwords = prompt(password_questions)
244
+ if not passwords:
245
+ return # User cancelled
246
+
247
+ password = passwords.get("password", "")
248
+ if password and password == passwords.get("password_confirm"):
249
+ break
250
+ console.print("[red]Passwords do not match or are empty. Please try again.[/red]")
251
+
252
+ # Create data directories
253
+ neo4j_dir = Path.cwd() / "neo4j_data"
254
+ for subdir in ["data", "logs", "conf", "plugins"]:
255
+ (neo4j_dir / subdir).mkdir(parents=True, exist_ok=True)
256
+
257
+ # Fixed docker-compose.yml content
258
+ docker_compose_content = f"""
259
+ services:
260
+ neo4j:
261
+ image: neo4j:5.21
262
+ container_name: neo4j-cgc
263
+ restart: unless-stopped
264
+ ports:
265
+ - "7474:7474"
266
+ - "7687:7687"
267
+ environment:
268
+ - NEO4J_AUTH=neo4j/12345678
269
+ - NEO4J_ACCEPT_LICENSE_AGREEMENT=yes
270
+ volumes:
271
+ - neo4j_data:/data
272
+ - neo4j_logs:/logs
273
+
274
+ volumes:
275
+ neo4j_data:
276
+ neo4j_logs:
277
+ """
278
+
279
+ # Write docker-compose.yml
280
+ compose_file = Path.cwd() / "docker-compose.yml"
281
+ with open(compose_file, "w") as f:
282
+ f.write(docker_compose_content)
283
+
284
+ console.print("[green]✅ docker-compose.yml created with secure password.[/green]")
285
+
286
+ # Check if Docker is running
287
+ docker_check = run_command(["docker", "--version"], console, check=False)
288
+ if not docker_check:
289
+ console.print("[red]❌ Docker is not installed or not running. Please install Docker first.[/red]")
290
+ return
291
+
292
+ # Check if docker-compose is available
293
+ compose_check = run_command(["docker", "compose", "version"], console, check=False)
294
+ if not compose_check:
295
+ console.print("[red]❌ Docker Compose is not available. Please install Docker Compose.[/red]")
296
+ return
297
+
298
+ confirm_q = [{"type": "confirm", "message": "Ready to launch Neo4j in Docker?", "name": "proceed", "default": True}]
299
+ if not prompt(confirm_q).get("proceed"):
300
+ return
301
+
302
+ try:
303
+ # Pull the image first
304
+ console.print("[cyan]Pulling Neo4j Docker image...[/cyan]")
305
+ pull_process = run_command(["docker", "pull", "neo4j:5.21"], console, check=True)
306
+ if not pull_process:
307
+ console.print("[yellow]⚠️ Could not pull image, but continuing anyway...[/yellow]")
308
+
309
+ # Start containers
310
+ console.print("[cyan]Starting Neo4j container...[/cyan]")
311
+ docker_process = run_command(["docker", "compose", "up", "-d"], console, check=True)
312
+
313
+ if docker_process:
314
+ console.print("[bold green]🚀 Neo4j Docker container started successfully![/bold green]")
315
+
316
+ # Wait for Neo4j to be ready
317
+ console.print("[cyan]Waiting for Neo4j to be ready (this may take 30-60 seconds)...[/cyan]")
318
+
319
+ # Try to connect for up to 2 minutes
320
+ max_attempts = 24 # 24 * 5 seconds = 2 minutes
321
+ for attempt in range(max_attempts):
322
+ time.sleep(5)
323
+
324
+ # Check if container is still running
325
+ status_check = run_command(["docker", "compose", "ps", "-q", "neo4j"], console, check=False)
326
+ if not status_check or not status_check.stdout.strip():
327
+ console.print("[red]❌ Neo4j container stopped unexpectedly. Check logs with: docker compose logs neo4j[/red]")
328
+ return
329
+
330
+ # Try to connect
331
+ health_check = run_command([
332
+ "docker", "exec", "neo4j-cgc", "cypher-shell",
333
+ "-u", "neo4j", "-p", password,
334
+ "RETURN 'Connection successful' as status"
335
+ ], console, check=False)
336
+
337
+ if health_check and health_check.returncode == 0:
338
+ console.print("[bold green]✅ Neo4j is ready and accepting connections![/bold green]")
339
+ break
340
+
341
+ if attempt < max_attempts - 1:
342
+ console.print(f"[yellow]Still waiting... (attempt {attempt + 1}/{max_attempts})[/yellow]")
343
+ else:
344
+ console.print("[red]❌ Neo4j did not become ready within 2 minutes. Check logs with: docker compose logs neo4j[/red]")
345
+ return
346
+
347
+ # Generate MCP configuration
348
+ creds = {
349
+ "uri": "neo4j://localhost:7687", # Use neo4j:// protocol for Neo4j 5.x
350
+ "username": "neo4j",
351
+ "password": password
352
+ }
353
+ _generate_mcp_json(creds)
354
+
355
+ console.print("\n[bold green]🎉 Setup complete![/bold green]")
356
+ console.print("Neo4j is running at:")
357
+ console.print(" • Web interface: http://localhost:7474")
358
+ console.print(" • Bolt connection: neo4j://localhost:7687")
359
+ console.print("\n[cyan]Useful commands:[/cyan]")
360
+ console.print(" • Stop: docker compose down")
361
+ console.print(" • Restart: docker compose restart")
362
+ console.print(" • View logs: docker compose logs neo4j")
363
+
364
+ except Exception as e:
365
+ console.print(f"[bold red]❌ Failed to start Neo4j Docker container:[/bold red] {e}")
366
+ console.print("[cyan]Try checking the logs with: docker compose logs neo4j[/cyan]")
244
367
 
245
368
  def setup_local_binary():
246
369
  """Automates the installation and configuration of Neo4j on Ubuntu/Debian."""
@@ -0,0 +1,141 @@
1
+ # src/codegraphcontext/core/watcher.py
2
+ import logging
3
+ import threading
4
+ from pathlib import Path
5
+ import typing
6
+ from watchdog.observers import Observer
7
+ from watchdog.events import FileSystemEventHandler
8
+
9
+ if typing.TYPE_CHECKING:
10
+ from codegraphcontext.tools.graph_builder import GraphBuilder
11
+ from codegraphcontext.core.jobs import JobManager
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ # --- NEW, MORE POWERFUL EVENT HANDLER ---
17
+ class RepositoryEventHandler(FileSystemEventHandler):
18
+ """
19
+ An event handler that manages the state for a single repository.
20
+ It performs an initial scan and uses cached data for efficient updates.
21
+ """
22
+ def __init__(self, graph_builder: "GraphBuilder", repo_path: Path, debounce_interval=2.0):
23
+ super().__init__()
24
+ self.graph_builder = graph_builder
25
+ self.repo_path = repo_path
26
+ self.debounce_interval = debounce_interval
27
+ self.timers = {}
28
+
29
+ # --- STATE CACHING ---
30
+ self.all_file_data = []
31
+ self.imports_map = {}
32
+
33
+ # Perform the initial scan and linking when created
34
+ self._initial_scan()
35
+
36
+ def _initial_scan(self):
37
+ """Scans the entire repository and builds the initial graph."""
38
+ logger.info(f"Performing initial scan for watcher: {self.repo_path}")
39
+ all_files = list(self.repo_path.rglob("*.py"))
40
+
41
+ # 1. Pre-scan for the global import map
42
+ self.imports_map = self.graph_builder._pre_scan_for_imports(all_files)
43
+
44
+ # 2. Parse all files and cache their data
45
+ for f in all_files:
46
+ parsed_data = self.graph_builder.parse_python_file(self.repo_path, f, self.imports_map)
47
+ if "error" not in parsed_data:
48
+ self.all_file_data.append(parsed_data)
49
+
50
+ # 3. Perform the initial linking of the entire graph
51
+ self.graph_builder._create_all_function_calls(self.all_file_data, self.imports_map)
52
+ logger.info(f"Initial scan and graph linking complete for: {self.repo_path}")
53
+
54
+ def _debounce(self, event_path, action):
55
+ """Schedules an action after a debounce interval to avoid rapid firing."""
56
+ if event_path in self.timers:
57
+ self.timers[event_path].cancel()
58
+ timer = threading.Timer(self.debounce_interval, action)
59
+ timer.start()
60
+ self.timers[event_path] = timer
61
+
62
+ def _handle_modification(self, event_path_str):
63
+ """Orchestrates the complete and correct update cycle for a modified file."""
64
+ logger.info(f"File change detected, starting full update for: {event_path_str}")
65
+ modified_path = Path(event_path_str)
66
+
67
+ # 1. Use the helper to update the file's nodes and get new data
68
+ # NOTE: You will need to modify `update_file_in_graph` to accept `repo_path`
69
+ # and `imports_map` as arguments and to return the new parsed data.
70
+ new_file_data = self.graph_builder.update_file_in_graph(
71
+ modified_path, self.repo_path, self.imports_map
72
+ )
73
+
74
+ if not new_file_data:
75
+ logger.error(f"Update failed for {event_path_str}, skipping re-link.")
76
+ return
77
+
78
+ # 2. Update the cache
79
+ self.all_file_data = [d for d in self.all_file_data if d.get("file_path") != event_path_str]
80
+ if not new_file_data.get("deleted"):
81
+ self.all_file_data.append(new_file_data)
82
+
83
+ # 3. CRITICAL: Re-link the entire graph using the updated cache
84
+ logger.info("Re-linking the call graph with updated information...")
85
+ self.graph_builder._create_all_function_calls(self.all_file_data, self.imports_map)
86
+ logger.info(f"Graph update for {event_path_str} complete! ✅")
87
+
88
+ def on_created(self, event):
89
+ if not event.is_directory and event.src_path.endswith('.py'):
90
+ self._debounce(event.src_path, lambda: self._handle_modification(event.src_path))
91
+
92
+ def on_modified(self, event):
93
+ if not event.is_directory and event.src_path.endswith('.py'):
94
+ self._debounce(event.src_path, lambda: self._handle_modification(event.src_path))
95
+
96
+ def on_deleted(self, event):
97
+ if not event.is_directory and event.src_path.endswith('.py'):
98
+ # Deletion is handled inside _handle_modification
99
+ self._debounce(event.src_path, lambda: self._handle_modification(event.src_path))
100
+
101
+ def on_moved(self, event):
102
+ if not event.is_directory and event.src_path.endswith('.py'):
103
+ # A move is a deletion from the old path and a creation at the new path
104
+ self._debounce(event.src_path, lambda: self._handle_modification(event.src_path))
105
+ self._debounce(event.dest_path, lambda: self._handle_modification(event.dest_path))
106
+
107
+
108
+ class CodeWatcher:
109
+ """Manages file system watching in a background thread."""
110
+ def __init__(self, graph_builder: "GraphBuilder", job_manager= "JobManager"):
111
+ self.graph_builder = graph_builder
112
+ self.observer = Observer()
113
+ self.watched_paths = set()
114
+
115
+ def watch_directory(self, path: str):
116
+ path_obj = Path(path).resolve()
117
+ path_str = str(path_obj)
118
+
119
+ if path_str in self.watched_paths:
120
+ logger.info(f"Path already being watched: {path_str}")
121
+ return {"message": f"Path already being watched: {path_str}"}
122
+
123
+ # --- Create a NEW, DEDICATED handler for this path ---
124
+ event_handler = RepositoryEventHandler(self.graph_builder, path_obj)
125
+
126
+ self.observer.schedule(event_handler, path_str, recursive=True)
127
+ self.watched_paths.add(path_str)
128
+ logger.info(f"Started watching for code changes in: {path_str}")
129
+
130
+ return {"message": f"Started watching {path_str}. Initial scan is in progress."}
131
+
132
+ def start(self):
133
+ if not self.observer.is_alive():
134
+ self.observer.start()
135
+ logger.info("Code watcher observer thread started.")
136
+
137
+ def stop(self):
138
+ if self.observer.is_alive():
139
+ self.observer.stop()
140
+ self.observer.join()
141
+ logger.info("Code watcher observer thread stopped.")
@@ -5,6 +5,7 @@ LLM_SYSTEM_PROMPT = """# AI Pair Programmer Instructions
5
5
  ## 1. Your Role and Goal
6
6
 
7
7
  You are an expert AI pair programmer. Your primary goal is to help a developer understand, write, and refactor code within their **local project**. Your defining feature is your connection to a local Model Context Protocol (MCP) server, which gives you real-time, accurate information about the codebase.
8
+ **Always prioritize using this MCP tools when they can simplify or enhance your workflow compared to guessing.**
8
9
 
9
10
  ## 2. Your Core Principles
10
11
 
@@ -11,7 +11,6 @@ from datetime import datetime
11
11
  from pathlib import Path
12
12
  from neo4j.exceptions import CypherSyntaxError
13
13
  from dataclasses import asdict
14
- from codegraphcontext.core.database import DatabaseManager
15
14
 
16
15
  from typing import Any, Dict, Coroutine, Optional
17
16
 
@@ -126,7 +125,7 @@ class MCPServer:
126
125
  },
127
126
  "execute_cypher_query": {
128
127
  "name": "execute_cypher_query",
129
- "description": "Fallback tool to run a direct, read-only Cypher query against the code graph.",
128
+ "description": "Fallback tool to run a direct, read-only Cypher query against the code graph. Use this for complex questions not covered by other tools. The graph contains nodes representing code structures and relationships between them. **Schema Overview:**\n- **Nodes:** `Repository`, `File`, `Module`, `Class`, `Function`.\n- **Properties:** Nodes have properties like `name`, `path`, `cyclomatic_complexity` (on Function nodes), and `code`.\n- **Relationships:** `CONTAINS` (e.g., File-[:CONTAINS]->Function), `CALLS` (Function-[:CALLS]->Function or File-[:CALLS]->Function), `IMPORTS` (File-[:IMPORTS]->Module), `INHERITS` (Class-[:INHERITS]->Class).",
130
129
  "inputSchema": {
131
130
  "type": "object",
132
131
  "properties": { "cypher_query": {"type": "string", "description": "The read-only Cypher query to execute."} },
@@ -209,9 +208,7 @@ class MCPServer:
209
208
  }
210
209
  }
211
210
  # Other tools like list_imports, add_package_to_graph can be added here following the same pattern
212
- }
213
-
214
-
211
+ }
215
212
 
216
213
  def get_database_status(self) -> dict:
217
214
  """Get current database connection status"""
@@ -225,24 +222,30 @@ class MCPServer:
225
222
  module = importlib.import_module(package_name)
226
223
 
227
224
  if hasattr(module, '__file__') and module.__file__:
228
- module_file = module.__file__
225
+ module_file = Path(module.__file__)
229
226
  debug_log(f"Module file: {module_file}")
230
-
231
- if module_file.endswith('__init__.py'):
232
- package_path = os.path.dirname(module_file)
227
+
228
+ if module_file.name == '__init__.py':
229
+ # It's a package (directory)
230
+ package_path = str(module_file.parent)
231
+ elif package_name in stdlibs.module_names:
232
+ # It's a single-file standard library module (e.g., os.py, io.py)
233
+ package_path = str(module_file)
233
234
  else:
234
- package_path = os.path.dirname(module_file)
235
-
236
- debug_log(f"Package path: {package_path}")
235
+ # Default to parent directory for other single-file modules
236
+ package_path = str(module_file.parent)
237
+
238
+ debug_log(f"Determined package path: {package_path}")
237
239
  return package_path
238
-
240
+
239
241
  elif hasattr(module, '__path__'):
242
+ # This handles namespace packages or packages without __init__.py
240
243
  if isinstance(module.__path__, list) and module.__path__:
241
- package_path = module.__path__[0]
244
+ package_path = str(Path(module.__path__[0]))
242
245
  debug_log(f"Package path from __path__: {package_path}")
243
246
  return package_path
244
247
  else:
245
- package_path = str(module.__path__)
248
+ package_path = str(Path(str(module.__path__)))
246
249
  debug_log(f"Package path from __path__ (str): {package_path}")
247
250
  return package_path
248
251
 
@@ -473,6 +476,15 @@ class MCPServer:
473
476
 
474
477
  if not path_obj.exists():
475
478
  return {"error": f"Path {path} does not exist"}
479
+
480
+ # Check if the repository is already indexed
481
+ indexed_repos = self.list_indexed_repositories_tool().get("repositories", [])
482
+ for repo in indexed_repos:
483
+ if Path(repo["path"]).resolve() == path_obj:
484
+ return {
485
+ "success": False,
486
+ "message": f"Repository '{path}' is already indexed."
487
+ }
476
488
 
477
489
  total_files, estimated_time = self.graph_builder.estimate_processing_time(path_obj)
478
490
 
@@ -506,6 +518,15 @@ class MCPServer:
506
518
  is_dependency = args.get("is_dependency", True)
507
519
 
508
520
  try:
521
+ # Check if the package is already indexed
522
+ indexed_repos = self.list_indexed_repositories_tool().get("repositories", [])
523
+ for repo in indexed_repos:
524
+ if repo.get("is_dependency") and (repo.get("name") == package_name or repo.get("name") == f"{package_name}.py"):
525
+ return {
526
+ "success": False,
527
+ "message": f"Package '{package_name}' is already indexed."
528
+ }
529
+
509
530
  package_path = self.get_local_package_path(package_name)
510
531
 
511
532
  if not package_path:
@@ -742,4 +763,4 @@ class MCPServer:
742
763
  def shutdown(self):
743
764
  logger.info("Shutting down server...")
744
765
  self.code_watcher.stop()
745
- self.db_manager.close_driver()
766
+ self.db_manager.close_driver()