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.
- {codegraphcontext-0.1.4/src/codegraphcontext.egg-info → codegraphcontext-0.1.6}/PKG-INFO +7 -1
- {codegraphcontext-0.1.4 → codegraphcontext-0.1.6}/README.md +6 -0
- {codegraphcontext-0.1.4 → codegraphcontext-0.1.6}/pyproject.toml +1 -1
- codegraphcontext-0.1.6/src/codegraphcontext/cli/main.py +93 -0
- {codegraphcontext-0.1.4 → codegraphcontext-0.1.6}/src/codegraphcontext/cli/setup_wizard.py +4 -2
- {codegraphcontext-0.1.4 → codegraphcontext-0.1.6}/src/codegraphcontext/core/jobs.py +19 -0
- codegraphcontext-0.1.6/src/codegraphcontext/core/watcher.py +141 -0
- {codegraphcontext-0.1.4 → codegraphcontext-0.1.6}/src/codegraphcontext/prompts.py +1 -0
- {codegraphcontext-0.1.4 → codegraphcontext-0.1.6}/src/codegraphcontext/server.py +162 -26
- {codegraphcontext-0.1.4 → codegraphcontext-0.1.6}/src/codegraphcontext/tools/code_finder.py +52 -1
- codegraphcontext-0.1.6/src/codegraphcontext/tools/graph_builder.py +1018 -0
- {codegraphcontext-0.1.4 → codegraphcontext-0.1.6/src/codegraphcontext.egg-info}/PKG-INFO +7 -1
- codegraphcontext-0.1.4/src/codegraphcontext/cli/main.py +0 -66
- codegraphcontext-0.1.4/src/codegraphcontext/core/watcher.py +0 -100
- codegraphcontext-0.1.4/src/codegraphcontext/tools/graph_builder.py +0 -608
- {codegraphcontext-0.1.4 → codegraphcontext-0.1.6}/LICENSE +0 -0
- {codegraphcontext-0.1.4 → codegraphcontext-0.1.6}/setup.cfg +0 -0
- {codegraphcontext-0.1.4 → codegraphcontext-0.1.6}/src/codegraphcontext/__init__.py +0 -0
- {codegraphcontext-0.1.4 → codegraphcontext-0.1.6}/src/codegraphcontext/__main__.py +0 -0
- {codegraphcontext-0.1.4 → codegraphcontext-0.1.6}/src/codegraphcontext/cli/__init__.py +0 -0
- {codegraphcontext-0.1.4 → codegraphcontext-0.1.6}/src/codegraphcontext/core/__init__.py +0 -0
- {codegraphcontext-0.1.4 → codegraphcontext-0.1.6}/src/codegraphcontext/core/database.py +0 -0
- {codegraphcontext-0.1.4 → codegraphcontext-0.1.6}/src/codegraphcontext/tools/__init__.py +0 -0
- {codegraphcontext-0.1.4 → codegraphcontext-0.1.6}/src/codegraphcontext/tools/import_extractor.py +0 -0
- {codegraphcontext-0.1.4 → codegraphcontext-0.1.6}/src/codegraphcontext/tools/system.py +0 -0
- {codegraphcontext-0.1.4 → codegraphcontext-0.1.6}/src/codegraphcontext.egg-info/SOURCES.txt +0 -0
- {codegraphcontext-0.1.4 → codegraphcontext-0.1.6}/src/codegraphcontext.egg-info/dependency_links.txt +0 -0
- {codegraphcontext-0.1.4 → codegraphcontext-0.1.6}/src/codegraphcontext.egg-info/entry_points.txt +0 -0
- {codegraphcontext-0.1.4 → codegraphcontext-0.1.6}/src/codegraphcontext.egg-info/requires.txt +0 -0
- {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.
|
|
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.
|
|
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": "
|
|
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": "
|
|
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.
|
|
191
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
399
|
+
# Now, start the watcher
|
|
400
|
+
watch_result = self.code_watcher.watch_directory(path)
|
|
291
401
|
|
|
292
|
-
|
|
402
|
+
# Combine results
|
|
403
|
+
final_result = {
|
|
293
404
|
"success": True,
|
|
294
|
-
"message": f"Initial scan started
|
|
295
|
-
"
|
|
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()
|