codegraphcontext 0.1.4__tar.gz → 0.1.6__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.4/src/codegraphcontext.egg-info → codegraphcontext-0.1.6}/PKG-INFO +7 -1
  2. {codegraphcontext-0.1.4 → codegraphcontext-0.1.6}/README.md +6 -0
  3. {codegraphcontext-0.1.4 → codegraphcontext-0.1.6}/pyproject.toml +1 -1
  4. codegraphcontext-0.1.6/src/codegraphcontext/cli/main.py +93 -0
  5. {codegraphcontext-0.1.4 → codegraphcontext-0.1.6}/src/codegraphcontext/cli/setup_wizard.py +4 -2
  6. {codegraphcontext-0.1.4 → codegraphcontext-0.1.6}/src/codegraphcontext/core/jobs.py +19 -0
  7. codegraphcontext-0.1.6/src/codegraphcontext/core/watcher.py +141 -0
  8. {codegraphcontext-0.1.4 → codegraphcontext-0.1.6}/src/codegraphcontext/prompts.py +1 -0
  9. {codegraphcontext-0.1.4 → codegraphcontext-0.1.6}/src/codegraphcontext/server.py +162 -26
  10. {codegraphcontext-0.1.4 → codegraphcontext-0.1.6}/src/codegraphcontext/tools/code_finder.py +52 -1
  11. codegraphcontext-0.1.6/src/codegraphcontext/tools/graph_builder.py +1018 -0
  12. {codegraphcontext-0.1.4 → codegraphcontext-0.1.6/src/codegraphcontext.egg-info}/PKG-INFO +7 -1
  13. codegraphcontext-0.1.4/src/codegraphcontext/cli/main.py +0 -66
  14. codegraphcontext-0.1.4/src/codegraphcontext/core/watcher.py +0 -100
  15. codegraphcontext-0.1.4/src/codegraphcontext/tools/graph_builder.py +0 -608
  16. {codegraphcontext-0.1.4 → codegraphcontext-0.1.6}/LICENSE +0 -0
  17. {codegraphcontext-0.1.4 → codegraphcontext-0.1.6}/setup.cfg +0 -0
  18. {codegraphcontext-0.1.4 → codegraphcontext-0.1.6}/src/codegraphcontext/__init__.py +0 -0
  19. {codegraphcontext-0.1.4 → codegraphcontext-0.1.6}/src/codegraphcontext/__main__.py +0 -0
  20. {codegraphcontext-0.1.4 → codegraphcontext-0.1.6}/src/codegraphcontext/cli/__init__.py +0 -0
  21. {codegraphcontext-0.1.4 → codegraphcontext-0.1.6}/src/codegraphcontext/core/__init__.py +0 -0
  22. {codegraphcontext-0.1.4 → codegraphcontext-0.1.6}/src/codegraphcontext/core/database.py +0 -0
  23. {codegraphcontext-0.1.4 → codegraphcontext-0.1.6}/src/codegraphcontext/tools/__init__.py +0 -0
  24. {codegraphcontext-0.1.4 → codegraphcontext-0.1.6}/src/codegraphcontext/tools/import_extractor.py +0 -0
  25. {codegraphcontext-0.1.4 → codegraphcontext-0.1.6}/src/codegraphcontext/tools/system.py +0 -0
  26. {codegraphcontext-0.1.4 → codegraphcontext-0.1.6}/src/codegraphcontext.egg-info/SOURCES.txt +0 -0
  27. {codegraphcontext-0.1.4 → codegraphcontext-0.1.6}/src/codegraphcontext.egg-info/dependency_links.txt +0 -0
  28. {codegraphcontext-0.1.4 → codegraphcontext-0.1.6}/src/codegraphcontext.egg-info/entry_points.txt +0 -0
  29. {codegraphcontext-0.1.4 → codegraphcontext-0.1.6}/src/codegraphcontext.egg-info/requires.txt +0 -0
  30. {codegraphcontext-0.1.4 → codegraphcontext-0.1.6}/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.4
3
+ Version: 0.1.6
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
@@ -123,6 +123,12 @@ Once the server is running, you can interact with it through your AI assistant u
123
123
  OR
124
124
  - "Keep the code graph updated for the project I'm working on at `~/dev/main-app`."
125
125
 
126
+ When you ask to watch a directory, the system performs two actions at once:
127
+ 1. It kicks off a full scan to index all the code in that directory. This process runs in the background, and you'll receive a `job_id` to track its progress.
128
+ 2. It begins watching the directory for any file changes to keep the graph updated in real-time.
129
+
130
+ This means you can start by simply telling the system to watch a directory, and it will handle both the initial indexing and the continuous updates automatically.
131
+
126
132
  ### Querying and Understanding Code
127
133
 
128
134
  - **Finding where code is defined:**
@@ -72,6 +72,12 @@ Once the server is running, you can interact with it through your AI assistant u
72
72
  OR
73
73
  - "Keep the code graph updated for the project I'm working on at `~/dev/main-app`."
74
74
 
75
+ When you ask to watch a directory, the system performs two actions at once:
76
+ 1. It kicks off a full scan to index all the code in that directory. This process runs in the background, and you'll receive a `job_id` to track its progress.
77
+ 2. It begins watching the directory for any file changes to keep the graph updated in real-time.
78
+
79
+ This means you can start by simply telling the system to watch a directory, and it will handle both the initial indexing and the continuous updates automatically.
80
+
75
81
  ### Querying and Understanding Code
76
82
 
77
83
  - **Finding where code is defined:**
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "codegraphcontext"
3
- version = "0.1.4"
3
+ version = "0.1.6"
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"
@@ -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)",
@@ -5,6 +5,8 @@ from datetime import datetime, timedelta
5
5
  from dataclasses import dataclass, asdict
6
6
  from enum import Enum
7
7
  from typing import Any, Dict, List, Optional
8
+ from pathlib import Path
9
+
8
10
 
9
11
  class JobStatus(Enum):
10
12
  """Job status enumeration"""
@@ -90,6 +92,23 @@ class JobManager:
90
92
  with self.lock:
91
93
  return list(self.jobs.values())
92
94
 
95
+ def find_active_job_by_path(self, path: str) -> Optional[JobInfo]:
96
+ """Finds the most recent, active job for a given path."""
97
+ with self.lock:
98
+ path_obj = Path(path).resolve()
99
+
100
+ matching_jobs = sorted(
101
+ [job for job in self.jobs.values() if job.path and Path(job.path).resolve() == path_obj],
102
+ key=lambda j: j.start_time,
103
+ reverse=True
104
+ )
105
+
106
+ for job in matching_jobs:
107
+ if job.status in [JobStatus.PENDING, JobStatus.RUNNING]:
108
+ return job
109
+
110
+ return None
111
+
93
112
  def cleanup_old_jobs(self, max_age_hours: int = 24):
94
113
  """Clean up jobs older than specified hours"""
95
114
  cutoff_time = datetime.now() - timedelta(hours=max_age_hours)
@@ -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
 
@@ -60,7 +59,7 @@ class MCPServer:
60
59
  self.code_finder = CodeFinder(self.db_manager)
61
60
  self.import_extractor = ImportExtractor()
62
61
 
63
- self.code_watcher = CodeWatcher(self.graph_builder)
62
+ self.code_watcher = CodeWatcher(self.graph_builder, self.job_manager)
64
63
 
65
64
  self._init_tools()
66
65
 
@@ -69,7 +68,7 @@ class MCPServer:
69
68
  self.tools = {
70
69
  "add_code_to_graph": {
71
70
  "name": "add_code_to_graph",
72
- "description": "Add code from a local folder to the graph. Returns a job ID for background processing.",
71
+ "description": "Performs a one-time scan of a local folder to add its code to the graph. Ideal for indexing libraries, dependencies, or projects not being actively modified. Returns a job ID for background processing.",
73
72
  "inputSchema": {
74
73
  "type": "object",
75
74
  "properties": {
@@ -117,7 +116,7 @@ class MCPServer:
117
116
  },
118
117
  "watch_directory": {
119
118
  "name": "watch_directory",
120
- "description": "Start watching a directory for code changes and automatically update the graph.",
119
+ "description": "Performs an initial scan of a directory and then continuously monitors it for changes, automatically keeping the graph up-to-date. Ideal for projects under active development. Returns a job ID for the initial scan.",
121
120
  "inputSchema": {
122
121
  "type": "object",
123
122
  "properties": { "path": {"type": "string", "description": "Path to directory to watch"} },
@@ -166,11 +165,50 @@ class MCPServer:
166
165
  "properties": {},
167
166
  "additionalProperties": False
168
167
  }
168
+ },
169
+ "calculate_cyclomatic_complexity": {
170
+ "name": "calculate_cyclomatic_complexity",
171
+ "description": "Calculate the cyclomatic complexity of a specific function to measure its complexity.",
172
+ "inputSchema": {
173
+ "type": "object",
174
+ "properties": {
175
+ "function_name": {"type": "string", "description": "The name of the function to analyze."},
176
+ "file_path": {"type": "string", "description": "Optional: The full path to the file containing the function for a more specific query."}
177
+ },
178
+ "required": ["function_name"]
179
+ }
180
+ },
181
+ "find_most_complex_functions": {
182
+ "name": "find_most_complex_functions",
183
+ "description": "Find the most complex functions in the codebase based on cyclomatic complexity.",
184
+ "inputSchema": {
185
+ "type": "object",
186
+ "properties": {
187
+ "limit": {"type": "integer", "description": "The maximum number of complex functions to return.", "default": 10}
188
+ }
189
+ }
190
+ },
191
+ "list_indexed_repositories": {
192
+ "name": "list_indexed_repositories",
193
+ "description": "List all indexed repositories.",
194
+ "inputSchema": {
195
+ "type": "object",
196
+ "properties": {}
197
+ }
198
+ },
199
+ "delete_repository": {
200
+ "name": "delete_repository",
201
+ "description": "Delete an indexed repository from the graph.",
202
+ "inputSchema": {
203
+ "type": "object",
204
+ "properties": {
205
+ "repo_path": {"type": "string", "description": "The path of the repository to delete."}
206
+ },
207
+ "required": ["repo_path"]
208
+ }
169
209
  }
170
210
  # Other tools like list_imports, add_package_to_graph can be added here following the same pattern
171
- }
172
-
173
-
211
+ }
174
212
 
175
213
  def get_database_status(self) -> dict:
176
214
  """Get current database connection status"""
@@ -184,24 +222,30 @@ class MCPServer:
184
222
  module = importlib.import_module(package_name)
185
223
 
186
224
  if hasattr(module, '__file__') and module.__file__:
187
- module_file = module.__file__
225
+ module_file = Path(module.__file__)
188
226
  debug_log(f"Module file: {module_file}")
189
-
190
- if module_file.endswith('__init__.py'):
191
- 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)
192
234
  else:
193
- package_path = os.path.dirname(module_file)
194
-
195
- 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}")
196
239
  return package_path
197
-
240
+
198
241
  elif hasattr(module, '__path__'):
242
+ # This handles namespace packages or packages without __init__.py
199
243
  if isinstance(module.__path__, list) and module.__path__:
200
- package_path = module.__path__[0]
244
+ package_path = str(Path(module.__path__[0]))
201
245
  debug_log(f"Package path from __path__: {package_path}")
202
246
  return package_path
203
247
  else:
204
- package_path = str(module.__path__)
248
+ package_path = str(Path(str(module.__path__)))
205
249
  debug_log(f"Package path from __path__ (str): {package_path}")
206
250
  return package_path
207
251
 
@@ -276,6 +320,70 @@ class MCPServer:
276
320
  debug_log(f"Error finding dead code: {str(e)}")
277
321
  return {"error": f"Failed to find dead code: {str(e)}"}
278
322
 
323
+ def calculate_cyclomatic_complexity_tool(self, **args) -> Dict[str, Any]:
324
+ """Tool to calculate cyclomatic complexity for a given function."""
325
+ function_name = args.get("function_name")
326
+ file_path = args.get("file_path")
327
+
328
+ try:
329
+ debug_log(f"Calculating cyclomatic complexity for function: {function_name}")
330
+ results = self.code_finder.get_cyclomatic_complexity(function_name, file_path)
331
+
332
+ response = {
333
+ "success": True,
334
+ "function_name": function_name,
335
+ "results": results
336
+ }
337
+ if file_path:
338
+ response["file_path"] = file_path
339
+
340
+ return response
341
+ except Exception as e:
342
+ debug_log(f"Error calculating cyclomatic complexity: {str(e)}")
343
+ return {"error": f"Failed to calculate cyclomatic complexity: {str(e)}"}
344
+
345
+ def find_most_complex_functions_tool(self, **args) -> Dict[str, Any]:
346
+ """Tool to find the most complex functions."""
347
+ limit = args.get("limit", 10)
348
+ try:
349
+ debug_log(f"Finding the top {limit} most complex functions.")
350
+ results = self.code_finder.find_most_complex_functions(limit)
351
+ return {
352
+ "success": True,
353
+ "limit": limit,
354
+ "results": results
355
+ }
356
+ except Exception as e:
357
+ debug_log(f"Error finding most complex functions: {str(e)}")
358
+ return {"error": f"Failed to find most complex functions: {str(e)}"}
359
+
360
+ def list_indexed_repositories_tool(self, **args) -> Dict[str, Any]:
361
+ """Tool to list indexed repositories."""
362
+ try:
363
+ debug_log("Listing indexed repositories.")
364
+ results = self.code_finder.list_indexed_repositories()
365
+ return {
366
+ "success": True,
367
+ "repositories": results
368
+ }
369
+ except Exception as e:
370
+ debug_log(f"Error listing indexed repositories: {str(e)}")
371
+ return {"error": f"Failed to list indexed repositories: {str(e)}"}
372
+
373
+ def delete_repository_tool(self, **args) -> Dict[str, Any]:
374
+ """Tool to delete a repository from the graph."""
375
+ repo_path = args.get("repo_path")
376
+ try:
377
+ debug_log(f"Deleting repository: {repo_path}")
378
+ self.graph_builder.delete_repository_from_graph(repo_path)
379
+ return {
380
+ "success": True,
381
+ "message": f"Repository '{repo_path}' deleted successfully."
382
+ }
383
+ except Exception as e:
384
+ debug_log(f"Error deleting repository: {str(e)}")
385
+ return {"error": f"Failed to delete repository: {str(e)}"}
386
+
279
387
  def watch_directory_tool(self, **args) -> Dict[str, Any]:
280
388
  """Tool to start watching a directory."""
281
389
  path = args.get("path")
@@ -283,17 +391,23 @@ class MCPServer:
283
391
  return {"error": f"Invalid path provided: {path}. Must be a directory."}
284
392
 
285
393
  try:
286
- initial_scan_result = self.add_code_to_graph_tool(path=path, is_dependency=False)
287
- if "error" in initial_scan_result:
288
- return initial_scan_result
394
+ # First, ensure the code is added/scanned
395
+ scan_job_result = self.add_code_to_graph_tool(path=path, is_dependency=False)
396
+ if "error" in scan_job_result:
397
+ return scan_job_result
289
398
 
290
- self.code_watcher.watch_directory(path)
399
+ # Now, start the watcher
400
+ watch_result = self.code_watcher.watch_directory(path)
291
401
 
292
- return {
402
+ # Combine results
403
+ final_result = {
293
404
  "success": True,
294
- "message": f"Initial scan started (Job ID: {initial_scan_result.get('job_id')}). Now watching for live changes in {path}.",
295
- "instructions": "Changes to .py files in this directory will now be automatically updated in the graph."
405
+ "message": f"Initial scan started for {path}. Now watching for live changes.",
406
+ "job_id": scan_job_result.get("job_id"),
407
+ "details": watch_result
296
408
  }
409
+ return final_result
410
+
297
411
  except Exception as e:
298
412
  logger.error(f"Failed to start watching directory {path}: {e}")
299
413
  return {"error": f"Failed to start watching directory: {str(e)}"}
@@ -362,6 +476,15 @@ class MCPServer:
362
476
 
363
477
  if not path_obj.exists():
364
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
+ }
365
488
 
366
489
  total_files, estimated_time = self.graph_builder.estimate_processing_time(path_obj)
367
490
 
@@ -395,6 +518,15 @@ class MCPServer:
395
518
  is_dependency = args.get("is_dependency", True)
396
519
 
397
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
+
398
530
  package_path = self.get_local_package_path(package_name)
399
531
 
400
532
  if not package_path:
@@ -555,7 +687,11 @@ class MCPServer:
555
687
  "execute_cypher_query": self.execute_cypher_query_tool,
556
688
  "add_code_to_graph": self.add_code_to_graph_tool,
557
689
  "check_job_status": self.check_job_status_tool,
558
- "list_jobs": self.list_jobs_tool
690
+ "list_jobs": self.list_jobs_tool,
691
+ "calculate_cyclomatic_complexity": self.calculate_cyclomatic_complexity_tool,
692
+ "find_most_complex_functions": self.find_most_complex_functions_tool,
693
+ "list_indexed_repositories": self.list_indexed_repositories_tool,
694
+ "delete_repository": self.delete_repository_tool
559
695
  }
560
696
  handler = tool_map.get(tool_name)
561
697
  if handler:
@@ -627,4 +763,4 @@ class MCPServer:
627
763
  def shutdown(self):
628
764
  logger.info("Shutting down server...")
629
765
  self.code_watcher.stop()
630
- self.db_manager.close_driver()
766
+ self.db_manager.close_driver()