codegraphcontext 0.1.7__tar.gz → 0.1.8__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 (27) hide show
  1. {codegraphcontext-0.1.7/src/codegraphcontext.egg-info → codegraphcontext-0.1.8}/PKG-INFO +40 -4
  2. {codegraphcontext-0.1.7 → codegraphcontext-0.1.8}/README.md +39 -3
  3. {codegraphcontext-0.1.7 → codegraphcontext-0.1.8}/pyproject.toml +1 -1
  4. {codegraphcontext-0.1.7 → codegraphcontext-0.1.8}/src/codegraphcontext/server.py +17 -22
  5. {codegraphcontext-0.1.7 → codegraphcontext-0.1.8}/src/codegraphcontext/tools/code_finder.py +145 -13
  6. {codegraphcontext-0.1.7 → codegraphcontext-0.1.8}/src/codegraphcontext/tools/graph_builder.py +124 -21
  7. {codegraphcontext-0.1.7 → codegraphcontext-0.1.8}/src/codegraphcontext/tools/import_extractor.py +13 -0
  8. {codegraphcontext-0.1.7 → codegraphcontext-0.1.8/src/codegraphcontext.egg-info}/PKG-INFO +40 -4
  9. {codegraphcontext-0.1.7 → codegraphcontext-0.1.8}/LICENSE +0 -0
  10. {codegraphcontext-0.1.7 → codegraphcontext-0.1.8}/setup.cfg +0 -0
  11. {codegraphcontext-0.1.7 → codegraphcontext-0.1.8}/src/codegraphcontext/__init__.py +0 -0
  12. {codegraphcontext-0.1.7 → codegraphcontext-0.1.8}/src/codegraphcontext/__main__.py +0 -0
  13. {codegraphcontext-0.1.7 → codegraphcontext-0.1.8}/src/codegraphcontext/cli/__init__.py +0 -0
  14. {codegraphcontext-0.1.7 → codegraphcontext-0.1.8}/src/codegraphcontext/cli/main.py +0 -0
  15. {codegraphcontext-0.1.7 → codegraphcontext-0.1.8}/src/codegraphcontext/cli/setup_wizard.py +0 -0
  16. {codegraphcontext-0.1.7 → codegraphcontext-0.1.8}/src/codegraphcontext/core/__init__.py +0 -0
  17. {codegraphcontext-0.1.7 → codegraphcontext-0.1.8}/src/codegraphcontext/core/database.py +0 -0
  18. {codegraphcontext-0.1.7 → codegraphcontext-0.1.8}/src/codegraphcontext/core/jobs.py +0 -0
  19. {codegraphcontext-0.1.7 → codegraphcontext-0.1.8}/src/codegraphcontext/core/watcher.py +0 -0
  20. {codegraphcontext-0.1.7 → codegraphcontext-0.1.8}/src/codegraphcontext/prompts.py +0 -0
  21. {codegraphcontext-0.1.7 → codegraphcontext-0.1.8}/src/codegraphcontext/tools/__init__.py +0 -0
  22. {codegraphcontext-0.1.7 → codegraphcontext-0.1.8}/src/codegraphcontext/tools/system.py +0 -0
  23. {codegraphcontext-0.1.7 → codegraphcontext-0.1.8}/src/codegraphcontext.egg-info/SOURCES.txt +0 -0
  24. {codegraphcontext-0.1.7 → codegraphcontext-0.1.8}/src/codegraphcontext.egg-info/dependency_links.txt +0 -0
  25. {codegraphcontext-0.1.7 → codegraphcontext-0.1.8}/src/codegraphcontext.egg-info/entry_points.txt +0 -0
  26. {codegraphcontext-0.1.7 → codegraphcontext-0.1.8}/src/codegraphcontext.egg-info/requires.txt +0 -0
  27. {codegraphcontext-0.1.7 → codegraphcontext-0.1.8}/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.7
3
+ Version: 0.1.8
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
@@ -53,9 +53,13 @@ Dynamic: license-file
53
53
  # CodeGraphContext
54
54
  [![Build Status](https://github.com/Shashankss1205/CodeGraphContext/actions/workflows/test.yml/badge.svg)](https://github.com/Shashankss1205/CodeGraphContext/actions/workflows/test.yml)
55
55
 
56
-
57
56
  An MCP server that indexes local code into a graph database to provide context to AI assistants.
58
57
 
58
+ ## Project Details
59
+ - **Version:** 0.1.8
60
+ - **Authors:** Shashank Shekhar Singh <shashankshekharsingh1205@gmail.com>
61
+ - **License:** MIT License (See [LICENSE](LICENSE) for details)
62
+
59
63
  ## Features
60
64
 
61
65
  - **Code Indexing:** Analyzes Python code and builds a knowledge graph of its components.
@@ -63,12 +67,23 @@ An MCP server that indexes local code into a graph database to provide context t
63
67
  - **Live Updates:** Watches local files for changes and automatically updates the graph.
64
68
  - **Interactive Setup:** A user-friendly command-line wizard for easy setup.
65
69
 
70
+ ## Dependencies
71
+
72
+ - `neo4j>=5.15.0`
73
+ - `watchdog>=3.0.0`
74
+ - `requests>=2.31.0`
75
+ - `stdlibs>=2023.11.18`
76
+ - `typer[all]>=0.9.0`
77
+ - `rich>=13.7.0`
78
+ - `inquirerpy>=0.3.4`
79
+ - `python-dotenv>=1.0.0`
80
+
66
81
  ## Getting Started
67
82
 
68
83
  1. **Install:** `pip install codegraphcontext`
69
84
  2. **Setup:** `cgc setup`
70
85
  3. **Start:** `cgc start`
71
- 4. **Index Code:** `cgc tool add-code-to-graph '{"path": "/path/to/your/project"}'`
86
+ 4. **Index Code:** `cgc tool add-code-to-graph '{"path": "/path/to/your/project"}'` (Under active development)
72
87
 
73
88
  ## MCP Client Configuration
74
89
 
@@ -98,7 +113,11 @@ Add the following to your MCP client's configuration:
98
113
  "analyze_code_relationships",
99
114
  "watch_directory",
100
115
  "find_dead_code",
101
- "execute_cypher_query"
116
+ "execute_cypher_query",
117
+ "calculate_cyclomatic_complexity",
118
+ "find_most_complex_functions",
119
+ "list_indexed_repositories",
120
+ "delete_repository"
102
121
  ],
103
122
  "disabled": false
104
123
  },
@@ -149,5 +168,22 @@ Once the server is running, you can interact with it through your AI assistant u
149
168
  - "Which files import the `requests` library?"
150
169
  - "Find all implementations of the `render` method."
151
170
 
171
+ - **Advanced Call Chain and Dependency Tracking (Spanning Hundreds of Files):**
172
+ The CodeGraphContext excels at tracing complex execution flows and dependencies across vast codebases. Leveraging the power of graph databases, it can identify direct and indirect callers and callees, even when a function is called through multiple layers of abstraction or across numerous files. This is invaluable for:
173
+ - **Impact Analysis:** Understand the full ripple effect of a change to a core function.
174
+ - **Debugging:** Trace the path of execution from an entry point to a specific bug.
175
+ - **Code Comprehension:** Grasp how different parts of a large system interact.
176
+
177
+ - "Show me the full call chain from the `main` function to `process_data`."
178
+ - "Find all functions that directly or indirectly call `validate_input`."
179
+ - "What are all the functions that `initialize_system` eventually calls?"
180
+ - "Trace the dependencies of the `DatabaseManager` module."
181
+
152
182
  - **Code Quality and Maintenance:**
153
183
  - "Is there any dead or unused code in this project?"
184
+ - "Calculate the cyclomatic complexity of the `process_data` function in `src/utils.py`."
185
+ - "Find the 5 most complex functions in the codebase."
186
+
187
+ - **Repository Management:**
188
+ - "List all currently indexed repositories."
189
+ - "Delete the indexed repository at `/path/to/old-project`."
@@ -1,9 +1,13 @@
1
1
  # CodeGraphContext
2
2
  [![Build Status](https://github.com/Shashankss1205/CodeGraphContext/actions/workflows/test.yml/badge.svg)](https://github.com/Shashankss1205/CodeGraphContext/actions/workflows/test.yml)
3
3
 
4
-
5
4
  An MCP server that indexes local code into a graph database to provide context to AI assistants.
6
5
 
6
+ ## Project Details
7
+ - **Version:** 0.1.8
8
+ - **Authors:** Shashank Shekhar Singh <shashankshekharsingh1205@gmail.com>
9
+ - **License:** MIT License (See [LICENSE](LICENSE) for details)
10
+
7
11
  ## Features
8
12
 
9
13
  - **Code Indexing:** Analyzes Python code and builds a knowledge graph of its components.
@@ -11,12 +15,23 @@ An MCP server that indexes local code into a graph database to provide context t
11
15
  - **Live Updates:** Watches local files for changes and automatically updates the graph.
12
16
  - **Interactive Setup:** A user-friendly command-line wizard for easy setup.
13
17
 
18
+ ## Dependencies
19
+
20
+ - `neo4j>=5.15.0`
21
+ - `watchdog>=3.0.0`
22
+ - `requests>=2.31.0`
23
+ - `stdlibs>=2023.11.18`
24
+ - `typer[all]>=0.9.0`
25
+ - `rich>=13.7.0`
26
+ - `inquirerpy>=0.3.4`
27
+ - `python-dotenv>=1.0.0`
28
+
14
29
  ## Getting Started
15
30
 
16
31
  1. **Install:** `pip install codegraphcontext`
17
32
  2. **Setup:** `cgc setup`
18
33
  3. **Start:** `cgc start`
19
- 4. **Index Code:** `cgc tool add-code-to-graph '{"path": "/path/to/your/project"}'`
34
+ 4. **Index Code:** `cgc tool add-code-to-graph '{"path": "/path/to/your/project"}'` (Under active development)
20
35
 
21
36
  ## MCP Client Configuration
22
37
 
@@ -46,7 +61,11 @@ Add the following to your MCP client's configuration:
46
61
  "analyze_code_relationships",
47
62
  "watch_directory",
48
63
  "find_dead_code",
49
- "execute_cypher_query"
64
+ "execute_cypher_query",
65
+ "calculate_cyclomatic_complexity",
66
+ "find_most_complex_functions",
67
+ "list_indexed_repositories",
68
+ "delete_repository"
50
69
  ],
51
70
  "disabled": false
52
71
  },
@@ -97,5 +116,22 @@ Once the server is running, you can interact with it through your AI assistant u
97
116
  - "Which files import the `requests` library?"
98
117
  - "Find all implementations of the `render` method."
99
118
 
119
+ - **Advanced Call Chain and Dependency Tracking (Spanning Hundreds of Files):**
120
+ The CodeGraphContext excels at tracing complex execution flows and dependencies across vast codebases. Leveraging the power of graph databases, it can identify direct and indirect callers and callees, even when a function is called through multiple layers of abstraction or across numerous files. This is invaluable for:
121
+ - **Impact Analysis:** Understand the full ripple effect of a change to a core function.
122
+ - **Debugging:** Trace the path of execution from an entry point to a specific bug.
123
+ - **Code Comprehension:** Grasp how different parts of a large system interact.
124
+
125
+ - "Show me the full call chain from the `main` function to `process_data`."
126
+ - "Find all functions that directly or indirectly call `validate_input`."
127
+ - "What are all the functions that `initialize_system` eventually calls?"
128
+ - "Trace the dependencies of the `DatabaseManager` module."
129
+
100
130
  - **Code Quality and Maintenance:**
101
131
  - "Is there any dead or unused code in this project?"
132
+ - "Calculate the cyclomatic complexity of the `process_data` function in `src/utils.py`."
133
+ - "Find the 5 most complex functions in the codebase."
134
+
135
+ - **Repository Management:**
136
+ - "List all currently indexed repositories."
137
+ - "Delete the indexed repository at `/path/to/old-project`."
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "codegraphcontext"
3
- version = "0.1.7"
3
+ version = "0.1.8"
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"
@@ -103,11 +103,11 @@ class MCPServer:
103
103
  },
104
104
  "analyze_code_relationships": {
105
105
  "name": "analyze_code_relationships",
106
- "description": "Analyze code relationships like 'who calls this function' or 'class hierarchy'.",
106
+ "description": "Analyze code relationships like 'who calls this function' or 'class hierarchy'. Supported query types include: find_callers, find_callees, find_all_callers, find_all_callees, find_importers, who_modifies, class_hierarchy, overrides, dead_code, call_chain, module_deps, variable_scope, find_complexity, find_functions_by_argument, find_functions_by_decorator.",
107
107
  "inputSchema": {
108
108
  "type": "object",
109
109
  "properties": {
110
- "query_type": {"type": "string", "description": "Type of relationship query to run."},
110
+ "query_type": {"type": "string", "description": "Type of relationship query to run.", "enum": ["find_callers", "find_callees", "find_all_callers", "find_all_callees", "find_importers", "who_modifies", "class_hierarchy", "overrides", "dead_code", "call_chain", "module_deps", "variable_scope", "find_complexity", "find_functions_by_argument", "find_functions_by_decorator"]},
111
111
  "target": {"type": "string", "description": "The function, class, or module to analyze."},
112
112
  "context": {"type": "string", "description": "Optional: specific file path for precise results."}
113
113
  },
@@ -159,11 +159,12 @@ class MCPServer:
159
159
  },
160
160
  "find_dead_code": {
161
161
  "name": "find_dead_code",
162
- "description": "Find potentially unused functions (dead code) across the entire indexed codebase.",
162
+ "description": "Find potentially unused functions (dead code) across the entire indexed codebase, optionally excluding functions with specific decorators.",
163
163
  "inputSchema": {
164
164
  "type": "object",
165
- "properties": {},
166
- "additionalProperties": False
165
+ "properties": {
166
+ "exclude_decorated_with": {"type": "array", "items": {"type": "string"}, "description": "Optional: A list of decorator names (e.g., '@app.route') to exclude from dead code detection.", "default": []}
167
+ }
167
168
  }
168
169
  },
169
170
  "calculate_cyclomatic_complexity": {
@@ -207,7 +208,6 @@ class MCPServer:
207
208
  "required": ["repo_path"]
208
209
  }
209
210
  }
210
- # Other tools like list_imports, add_package_to_graph can be added here following the same pattern
211
211
  }
212
212
 
213
213
  def get_database_status(self) -> dict:
@@ -304,12 +304,12 @@ class MCPServer:
304
304
  "details": str(e)
305
305
  }
306
306
 
307
- def find_dead_code_tool(self) -> Dict[str, Any]:
307
+ def find_dead_code_tool(self, **args) -> Dict[str, Any]:
308
308
  """Tool to find potentially dead code across the entire project."""
309
+ exclude_decorated_with = args.get("exclude_decorated_with", [])
309
310
  try:
310
311
  debug_log("Finding dead code.")
311
- # The target argument from the old tool is not needed.
312
- results = self.code_finder.find_dead_code()
312
+ results = self.code_finder.find_dead_code(exclude_decorated_with=exclude_decorated_with)
313
313
 
314
314
  return {
315
315
  "success": True,
@@ -446,17 +446,11 @@ class MCPServer:
446
446
  else:
447
447
  return {"error": f"Path {path} does not exist"}
448
448
 
449
- if language == 'python':
450
- # Get the list of stdlib modules for the current Python version
451
- stdlib_modules = set(stdlibs.module_names)
452
- # stdlib_modules = {
453
- # 'os', 'sys', 'json', 'time', 'datetime', 'math', 'random', 're', 'collections',
454
- # 'itertools', 'functools', 'operator', 'pathlib', 'urllib', 'http', 'logging',
455
- # 'threading', 'multiprocessing', 'asyncio', 'typing', 'dataclasses', 'enum',
456
- # 'abc', 'io', 'csv', 'sqlite3', 'pickle', 'base64', 'hashlib', 'hmac', 'secrets',
457
- # 'unittest', 'doctest', 'pdb', 'profile', 'cProfile', 'timeit'
458
- # }
459
- all_imports = all_imports - stdlib_modules
449
+ # Removed standard library filtering as per user request.
450
+ # if language == 'python':
451
+ # # Get the list of stdlib modules for the current Python version
452
+ # stdlib_modules = set(stdlibs.module_names)
453
+ # all_imports = all_imports - stdlib_modules
460
454
 
461
455
  return {
462
456
  "imports": sorted(list(all_imports)), "language": language,
@@ -642,8 +636,9 @@ class MCPServer:
642
636
  return {
643
637
  "error": "Both 'query_type' and 'target' are required",
644
638
  "supported_query_types": [
645
- "who_calls", "what_calls", "who_imports", "who_modifies",
646
- "class_hierarchy", "overrides", "dead_code"
639
+ "find_callers", "find_callees", "find_importers", "who_modifies",
640
+ "class_hierarchy", "overrides", "dead_code", "call_chain",
641
+ "module_deps", "variable_scope", "find_complexity"
647
642
  ]
648
643
  }
649
644
 
@@ -2,6 +2,7 @@
2
2
  import logging
3
3
  import re
4
4
  from typing import Any, Dict, List
5
+ from pathlib import Path
5
6
 
6
7
  from ..core.database import DatabaseManager
7
8
 
@@ -116,6 +117,56 @@ class CodeFinder:
116
117
 
117
118
  return results
118
119
 
120
+ def find_functions_by_argument(self, argument_name: str, file_path: str = None) -> List[Dict]:
121
+ """Find functions that take a specific argument name."""
122
+ with self.driver.session() as session:
123
+ if file_path:
124
+ query = """
125
+ MATCH (f:Function)-[:HAS_PARAMETER]->(p:Parameter)
126
+ WHERE p.name = $argument_name AND f.file_path = $file_path
127
+ RETURN f.name AS function_name, f.file_path AS file_path, f.line_number AS line_number,
128
+ f.docstring AS docstring, f.is_dependency AS is_dependency
129
+ ORDER BY f.is_dependency ASC, f.file_path, f.line_number
130
+ LIMIT 20
131
+ """
132
+ result = session.run(query, argument_name=argument_name, file_path=file_path)
133
+ else:
134
+ query = """
135
+ MATCH (f:Function)-[:HAS_PARAMETER]->(p:Parameter)
136
+ WHERE p.name = $argument_name
137
+ RETURN f.name AS function_name, f.file_path AS file_path, f.line_number AS line_number,
138
+ f.docstring AS docstring, f.is_dependency AS is_dependency
139
+ ORDER BY f.is_dependency ASC, f.file_path, f.line_number
140
+ LIMIT 20
141
+ """
142
+ result = session.run(query, argument_name=argument_name)
143
+ return [dict(record) for record in result]
144
+
145
+ def find_functions_by_decorator(self, decorator_name: str, file_path: str = None) -> List[Dict]:
146
+ """Find functions that have a specific decorator applied to them."""
147
+ with self.driver.session() as session:
148
+ if file_path:
149
+ query = """
150
+ MATCH (f:Function)
151
+ WHERE f.file_path = $file_path AND $decorator_name IN f.decorators
152
+ RETURN f.name AS function_name, f.file_path AS file_path, f.line_number AS line_number,
153
+ f.docstring AS docstring, f.is_dependency AS is_dependency, f.decorators AS decorators
154
+ ORDER BY f.is_dependency ASC, f.file_path, f.line_number
155
+ LIMIT 20
156
+ """
157
+ result = session.run(query, decorator_name=decorator_name, file_path=file_path)
158
+ else:
159
+ query = """
160
+ MATCH (f:Function)
161
+ WHERE $decorator_name IN f.decorators
162
+ RETURN f.name AS function_name, f.file_path AS file_path, f.line_number AS line_number,
163
+ f.docstring AS docstring, f.is_dependency AS is_dependency, f.decorators AS decorators
164
+ ORDER BY f.is_dependency ASC, f.file_path, f.line_number
165
+ LIMIT 20
166
+ """
167
+ result = session.run(query, decorator_name=decorator_name)
168
+ return [dict(record) for record in result]
169
+
119
170
  def who_calls_function(self, function_name: str, file_path: str = None) -> List[Dict]:
120
171
  """Find what functions call a specific function using CALLS relationships with improved matching"""
121
172
  with self.driver.session() as session:
@@ -230,7 +281,7 @@ class CodeFinder:
230
281
  with self.driver.session() as session:
231
282
  result = session.run("""
232
283
  MATCH (file:File)-[imp:IMPORTS]->(module:Module)
233
- WHERE module.name CONTAINS $module_name OR module.name = $module_name
284
+ WHERE module.name = $module_name OR module.full_import_name CONTAINS $module_name
234
285
  OPTIONAL MATCH (repo:Repository)-[:CONTAINS]->(file)
235
286
  RETURN DISTINCT
236
287
  file.name as file_name,
@@ -344,8 +395,11 @@ class CodeFinder:
344
395
 
345
396
  return [dict(record) for record in result]
346
397
 
347
- def find_dead_code(self) -> Dict[str, Any]:
348
- """Find potentially unused functions (not called by other functions in the project)"""
398
+ def find_dead_code(self, exclude_decorated_with: List[str] = None) -> Dict[str, Any]:
399
+ """Find potentially unused functions (not called by other functions in the project), optionally excluding those with specific decorators."""
400
+ if exclude_decorated_with is None:
401
+ exclude_decorated_with = []
402
+
349
403
  with self.driver.session() as session:
350
404
  result = session.run("""
351
405
  MATCH (func:Function)
@@ -353,6 +407,7 @@ class CodeFinder:
353
407
  AND NOT func.name IN ['main', '__init__', '__main__', 'setup', 'run', '__new__', '__del__']
354
408
  AND NOT func.name STARTS WITH '_test'
355
409
  AND NOT func.name STARTS WITH 'test_'
410
+ AND ALL(decorator_name IN $exclude_decorated_with WHERE NOT decorator_name IN func.decorators)
356
411
  WITH func
357
412
  OPTIONAL MATCH (caller:Function)-[:CALLS]->(func)
358
413
  WHERE caller.is_dependency = false
@@ -368,37 +423,84 @@ class CodeFinder:
368
423
  file.name as file_name
369
424
  ORDER BY func.file_path, func.line_number
370
425
  LIMIT 50
371
- """)
426
+ """, exclude_decorated_with=exclude_decorated_with)
372
427
 
373
428
  return {
374
429
  "potentially_unused_functions": [dict(record) for record in result],
375
430
  "note": "These functions might be unused, but could be entry points, callbacks, or called dynamically"
376
431
  }
377
432
 
433
+ def find_all_callers(self, function_name: str, file_path: str = None) -> List[Dict]:
434
+ """Find all direct and indirect callers of a specific function."""
435
+ with self.driver.session() as session:
436
+ if file_path:
437
+ # Find functions within the specified file_path that call the target function
438
+ query = """
439
+ MATCH (f:Function)-[:CALLS*]->(target:Function {name: $function_name})
440
+ WHERE f.file_path = $file_path
441
+ RETURN DISTINCT f.name AS caller_name, f.file_path AS caller_file_path, f.line_number AS caller_line_number, f.is_dependency AS caller_is_dependency
442
+ ORDER BY f.is_dependency ASC, f.file_path, f.line_number
443
+ LIMIT 50
444
+ """
445
+ result = session.run(query, function_name=function_name, file_path=file_path)
446
+ else:
447
+ # If no file_path (context) is provided, find all callers of the function by name
448
+ query = """
449
+ MATCH (f:Function)-[:CALLS*]->(target:Function {name: $function_name})
450
+ RETURN DISTINCT f.name AS caller_name, f.file_path AS caller_file_path, f.line_number AS caller_line_number, f.is_dependency AS caller_is_dependency
451
+ ORDER BY f.is_dependency ASC, f.file_path, f.line_number
452
+ LIMIT 50
453
+ """
454
+ result = session.run(query, function_name=function_name)
455
+ return [dict(record) for record in result]
456
+
457
+ def find_all_callees(self, function_name: str, file_path: str = None) -> List[Dict]:
458
+ """Find all direct and indirect callees of a specific function."""
459
+ with self.driver.session() as session:
460
+ if file_path:
461
+ query = """
462
+ MATCH (caller:Function {name: $function_name, file_path: $file_path})
463
+ MATCH (caller)-[:CALLS*]->(f:Function)
464
+ RETURN DISTINCT f.name AS callee_name, f.file_path AS callee_file_path, f.line_number AS callee_line_number, f.is_dependency AS callee_is_dependency
465
+ ORDER BY f.is_dependency ASC, f.file_path, f.line_number
466
+ LIMIT 50
467
+ """
468
+ result = session.run(query, function_name=function_name, file_path=file_path)
469
+ else:
470
+ query = """
471
+ MATCH (caller:Function {name: $function_name})
472
+ MATCH (caller)-[:CALLS*]->(f:Function)
473
+ RETURN DISTINCT f.name AS callee_name, f.file_path AS callee_file_path, f.line_number AS callee_line_number, f.is_dependency AS callee_is_dependency
474
+ ORDER BY f.is_dependency ASC, f.file_path, f.line_number
475
+ LIMIT 50
476
+ """
477
+ result = session.run(query, function_name=function_name)
478
+ return [dict(record) for record in result]
479
+
378
480
  def find_function_call_chain(self, start_function: str, end_function: str, max_depth: int = 5) -> List[Dict]:
379
481
  """Find call chains between two functions"""
380
482
  with self.driver.session() as session:
381
- result = session.run("""
483
+ result = session.run(f"""
382
484
  MATCH path = shortestPath(
383
- (start:Function {name: $start_function})-[:CALLS*1..$max_depth]->(end:Function {name: $end_function})
485
+ (start:Function {{name: $start_function}})-[:CALLS*1..{max_depth}]->(end:Function {{name: $end_function}})
384
486
  )
385
487
  WITH path, nodes(path) as func_nodes, relationships(path) as call_rels
386
488
  RETURN
387
- [node in func_nodes | {
489
+ [node in func_nodes | {{
388
490
  name: node.name,
389
491
  file_path: node.file_path,
390
492
  line_number: node.line_number,
391
493
  is_dependency: node.is_dependency
392
- }] as function_chain,
393
- [rel in call_rels | {
494
+ }}] as function_chain,
495
+ [rel in call_rels | {{
394
496
  call_line: rel.line_number,
395
497
  args: rel.args,
396
498
  full_call_name: rel.full_call_name
397
- }] as call_details,
499
+ }}] as call_details,
398
500
  length(path) as chain_length
399
501
  ORDER BY chain_length ASC
400
502
  LIMIT 10
401
- """, start_function=start_function, end_function=end_function, max_depth=max_depth)
503
+ """, start_function=start_function, end_function=end_function)
402
504
 
403
505
  return [dict(record) for record in result]
404
506
 
@@ -494,6 +596,20 @@ class CodeFinder:
494
596
  "summary": f"Found {len(results)} files that import '{target}'"
495
597
  }
496
598
 
599
+ elif query_type == "find_functions_by_argument":
600
+ results = self.find_functions_by_argument(target, context)
601
+ return {
602
+ "query_type": "find_functions_by_argument", "target": target, "context": context, "results": results,
603
+ "summary": f"Found {len(results)} functions that take '{target}' as an argument"
604
+ }
605
+
606
+ elif query_type == "find_functions_by_decorator":
607
+ results = self.find_functions_by_decorator(target, context)
608
+ return {
609
+ "query_type": "find_functions_by_decorator", "target": target, "context": context, "results": results,
610
+ "summary": f"Found {len(results)} functions decorated with '{target}'"
611
+ }
612
+
497
613
  elif query_type in ["who_modifies", "modifies", "mutations", "changes", "variable_usage"]:
498
614
  results = self.who_modifies_variable(target)
499
615
  return {
@@ -530,13 +646,29 @@ class CodeFinder:
530
646
  "summary": f"Found the top {len(results)} most complex functions"
531
647
  }
532
648
 
649
+ elif query_type == "find_all_callers":
650
+ results = self.find_all_callers(target, context)
651
+ return {
652
+ "query_type": "find_all_callers", "target": target, "context": context, "results": results,
653
+ "summary": f"Found {len(results)} direct and indirect callers of '{target}'"
654
+ }
655
+
656
+ elif query_type == "find_all_callees":
657
+ results = self.find_all_callees(target, context)
658
+ return {
659
+ "query_type": "find_all_callees", "target": target, "context": context, "results": results,
660
+ "summary": f"Found {len(results)} direct and indirect callees of '{target}'"
661
+ }
662
+
533
663
  elif query_type in ["call_chain", "path", "chain"]:
534
664
  if '->' in target:
535
665
  start_func, end_func = target.split('->', 1)
536
- results = self.find_function_call_chain(start_func.strip(), end_func.strip())
666
+ # max_depth can be passed as context, default to 5 if not provided or invalid
667
+ max_depth = int(context) if context and context.isdigit() else 5
668
+ results = self.find_function_call_chain(start_func.strip(), end_func.strip(), max_depth)
537
669
  return {
538
670
  "query_type": "call_chain", "target": target, "results": results,
539
- "summary": f"Found {len(results)} call chains from '{start_func.strip()}' to '{end_func.strip()}'"
671
+ "summary": f"Found {len(results)} call chains from '{start_func.strip()}' to '{end_func.strip()}' (max depth: {max_depth})"
540
672
  }
541
673
  else:
542
674
  return {
@@ -244,7 +244,9 @@ class CodeVisitor(ast.NodeVisitor):
244
244
  "args": [arg.arg for arg in node.args.args], "source": ast.unparse(node),
245
245
  "context": self.current_context, "class_context": self.current_class,
246
246
  "is_dependency": self.is_dependency, "docstring": ast.get_docstring(node),
247
- "decorators": [ast.unparse(d) for d in node.decorator_list]}
247
+ "decorators": [ast.unparse(d) for d in node.decorator_list],
248
+ "source_code": ast.unparse(node)} # Add source_code here
249
+ self.functions.append(func_data)
248
250
  self.functions.append(func_data)
249
251
  self._push_context(node.name, "function", node.lineno)
250
252
  # This will trigger visit_Assign and visit_Call for nodes inside the function
@@ -343,7 +345,8 @@ class CodeVisitor(ast.NodeVisitor):
343
345
  """Visit import statements"""
344
346
  for name in node.names:
345
347
  import_data = {
346
- "name": name.name,
348
+ "name": name.name.split('.')[0], # Store the top-level package name
349
+ "full_import_name": name.name, # Store the full import name
347
350
  "line_number": node.lineno,
348
351
  "alias": name.asname,
349
352
  "context": self.current_context,
@@ -361,14 +364,20 @@ class CodeVisitor(ast.NodeVisitor):
361
364
 
362
365
  for alias in node.names:
363
366
  # If node.module is None, it's an import like `from . import name`
367
+ # Determine the base module name for the 'name' property
364
368
  if node.module:
365
- full_name = f"{prefix}{node.module}.{alias.name}"
369
+ # For 'from .module import name', base_module is 'module'
370
+ # For 'from package.module import name', base_module is 'package'
371
+ base_module = node.module.split('.')[0]
372
+ full_import_name = f"{prefix}{node.module}.{alias.name}"
366
373
  else:
367
- # The full name is just the prefix and the imported name
368
- full_name = f"{prefix}{alias.name}"
374
+ # For 'from . import name', base_module is 'name'
375
+ base_module = alias.name
376
+ full_import_name = f"{prefix}{alias.name}"
369
377
 
370
378
  import_data = {
371
- "name": full_name,
379
+ "name": base_module, # Store the top-level module name
380
+ "full_import_name": full_import_name, # Store the full import path
372
381
  "line_number": node.lineno,
373
382
  "alias": alias.asname,
374
383
  "context": self.current_context,
@@ -445,29 +454,29 @@ class CodeVisitor(ast.NodeVisitor):
445
454
  inferred_obj_type = None
446
455
  if isinstance(node.func, ast.Attribute):
447
456
  base_obj_node = node.func.value
448
-
457
+
449
458
  if isinstance(base_obj_node, ast.Name):
450
459
  obj_name = base_obj_node.id
451
460
  if obj_name == 'self':
452
461
  # If the base is 'self', find the type of the attribute on the current class
453
462
  inferred_obj_type = self.class_symbol_table.get(node.func.attr)
454
463
  if not inferred_obj_type: # Fallback for method calls directly on self
455
- inferred_obj_type = self.current_class
464
+ inferred_obj_type = self.current_class
456
465
  else:
457
466
  inferred_obj_type = (self.local_symbol_table.get(obj_name) or
458
- self.class_symbol_table.get(obj_name) or
459
- self.module_symbol_table.get(obj_name))
467
+ self.class_symbol_table.get(obj_name) or
468
+ self.module_symbol_table.get(obj_name))
460
469
  # If it's not a variable, it might be a direct call on a Class name.
461
470
  if not inferred_obj_type and obj_name in self.imports_map:
462
471
  inferred_obj_type = obj_name
463
472
 
464
473
  elif isinstance(base_obj_node, ast.Call):
465
474
  inferred_obj_type = self._resolve_type_from_call(base_obj_node)
466
-
475
+
467
476
  elif isinstance(base_obj_node, ast.Attribute): # e.g., self.job_manager
468
477
  # This handles nested attributes
469
478
  # The goal is to find the type of `self.job_manager`, which is 'JobManager'
470
-
479
+
471
480
  # Resolve the base of the chain, e.g., get 'self' from 'self.job_manager'
472
481
  base = base_obj_node
473
482
  while isinstance(base, ast.Attribute):
@@ -477,11 +486,66 @@ class CodeVisitor(ast.NodeVisitor):
477
486
  # In self.X.Y... The attribute we care about is the first one, X
478
487
  attr_name = base_obj_node.attr
479
488
  inferred_obj_type = self.class_symbol_table.get(attr_name)
480
-
489
+
481
490
  elif isinstance(node.func, ast.Name):
482
491
  inferred_obj_type = (self.local_symbol_table.get(call_name) or
483
- self.class_symbol_table.get(call_name) or
484
- self.module_symbol_table.get(call_name))
492
+ self.class_symbol_table.get(call_name) or
493
+ self.module_symbol_table.get(call_name))
494
+
495
+ # there are no CALLS relationships originating from P2pkhAddress.to_address in the graph. This is the root cause of the find_all_callees tool reporting 0
496
+ # results.
497
+
498
+ # The problem is not with the find_all_callees query itself, but with the GraphBuilder's ability to correctly identify and create CALLS relationships for methods like
499
+ # P2pkhAddress.to_address.
500
+
501
+ # Specifically, the GraphBuilder._create_function_calls method is likely not correctly processing calls made within methods of a class, especially when those calls are to:
502
+ # 1. self.method(): Internal method calls.
503
+ # 2. Functions imported from other modules (e.g., h_to_b, get_network).
504
+ # 3. Functions from external libraries (e.g., hashlib.sha256, b58encode).
505
+
506
+ # The GraphBuilder.CodeVisitor.visit_Call method is responsible for identifying function calls. It needs to be improved to handle these cases.
507
+
508
+ # Plan:
509
+
510
+ # 1. Enhance `CodeVisitor.visit_Call` in `src/codegraphcontext/tools/graph_builder.py`:
511
+ # * Internal Method Calls (`self.method()`): When node.func is an ast.Attribute and node.func.value.id is self, the call_name should be node.func.attr, and the resolved_path should
512
+ # be the file_path of the current class.
513
+ # * Imported Functions: The _create_function_calls method already has some logic for resolving imported functions using imports_map. I need to ensure this logic is robust and
514
+ # correctly applied within visit_Call to set inferred_obj_type or resolved_path accurately.
515
+ # * External Library Functions: For now, we might not be able to fully resolve calls to external library functions unless those libraries are also indexed. However, we should at
516
+ # least capture the full_call_name and call_name for these.
517
+ # inferred_obj_type = None
518
+ # if isinstance(node.func, ast.Attribute):
519
+ # base_obj_node = node.func.value
520
+
521
+ # if isinstance(base_obj_node, ast.Name):
522
+ # obj_name = base_obj_node.id
523
+ # if obj_name == 'self':
524
+ # # If the base is 'self', the call is to a method of the current class
525
+ # inferred_obj_type = self.current_class
526
+ # else:
527
+ # # Try to resolve the type of the object from symbol tables
528
+ # inferred_obj_type = (self.local_symbol_table.get(obj_name) or
529
+ # self.class_symbol_table.get(obj_name) or
530
+ # self.module_symbol_table.get(obj_name))
531
+ # # If not found in symbol tables, check if it's a class name from imports
532
+ # if not inferred_obj_type and obj_name in self.imports_map:
533
+ # inferred_obj_type = obj_name
534
+
535
+ # elif isinstance(base_obj_node, ast.Call):
536
+ # inferred_obj_type = self._resolve_type_from_call(base_obj_node)
537
+
538
+ # elif isinstance(base_obj_node, ast.Attribute): # e.g., self.job_manager.method()
539
+ # # Recursively resolve the type of the base attribute
540
+ # inferred_obj_type = self._resolve_attribute_base_type(base_obj_node)
541
+
542
+ # elif isinstance(node.func, ast.Name):
543
+ # # If it's a direct function call, try to infer its type from symbol tables or imports
544
+ # inferred_obj_type = (self.local_symbol_table.get(call_name) or
545
+ # self.class_symbol_table.get(call_name) or
546
+ # self.module_symbol_table.get(call_name))
547
+ # if not inferred_obj_type and call_name in self.imports_map:
548
+ # inferred_obj_type = call_name
485
549
 
486
550
  if call_name and call_name not in __builtins__:
487
551
  call_data = {
@@ -621,12 +685,36 @@ class GraphBuilder:
621
685
  MERGE (f)-[:CONTAINS]->(n)
622
686
  """
623
687
  session.run(query, file_path=file_path_str, name=item['name'], line_number=item['line_number'], props=item)
688
+
689
+ # If it's a function, create parameter nodes and relationships and calculate complexity
690
+ if label == 'Function':
691
+ # Calculate cyclomatic complexity
692
+ try:
693
+ func_tree = ast.parse(item['source_code'])
694
+ complexity_visitor = CyclomaticComplexityVisitor()
695
+ complexity_visitor.visit(func_tree)
696
+ item['cyclomatic_complexity'] = complexity_visitor.complexity
697
+ except Exception as e:
698
+ logger.warning(f"Could not calculate cyclomatic complexity for {item['name']} in {file_path_str}: {e}")
699
+ item['cyclomatic_complexity'] = 1 # Default to 1 on error
700
+
701
+ for arg_name in item.get('args', []):
702
+ session.run("""
703
+ MATCH (fn:Function {name: $func_name, file_path: $file_path, line_number: $line_number})
704
+ MERGE (p:Parameter {name: $arg_name, file_path: $file_path, function_line_number: $line_number})
705
+ MERGE (fn)-[:HAS_PARAMETER]->(p)
706
+ """, func_name=item['name'], file_path=file_path_str, line_number=item['line_number'], arg_name=arg_name)
624
707
 
625
708
  for imp in file_data['imports']:
626
- session.run("""
627
- MATCH (f:File {path: $file_path})
628
- MERGE (m:Module {name: $name})
629
- SET m.alias = $alias
709
+ set_clauses = ["m.alias = $alias"]
710
+ if 'full_import_name' in imp:
711
+ set_clauses.append("m.full_import_name = $full_import_name")
712
+ set_clause_str = ", ".join(set_clauses)
713
+
714
+ session.run(f"""
715
+ MATCH (f:File {{path: $file_path}})
716
+ MERGE (m:Module {{name: $name}})
717
+ SET {set_clause_str}
630
718
  MERGE (f)-[:IMPORTS]->(m)
631
719
  """, file_path=file_path_str, **imp)
632
720
 
@@ -663,9 +751,20 @@ class GraphBuilder:
663
751
 
664
752
  for var in file_data.get('variables', []):
665
753
  context = var.get('context')
754
+ class_context = var.get('class_context')
666
755
  parent_line = var.get('parent_line')
667
756
 
668
- if context and parent_line:
757
+ if class_context:
758
+ session.run("""
759
+ MATCH (c:Class {name: $class_name, file_path: $file_path})
760
+ MATCH (v:Variable {name: $var_name, file_path: $file_path, line_number: $var_line})
761
+ MERGE (c)-[:CONTAINS]->(v)
762
+ """,
763
+ class_name=class_context,
764
+ file_path=file_path,
765
+ var_name=var['name'],
766
+ var_line=var['line_number'])
767
+ elif context and parent_line:
669
768
  parent_label = "Function"
670
769
  parent_node_data = None
671
770
 
@@ -749,7 +848,11 @@ class GraphBuilder:
749
848
 
750
849
  # Fallback if no path could be resolved by any of the above rules
751
850
  if not resolved_path:
752
- resolved_path = caller_file_path
851
+ # If the called name is in the imports map, use its path
852
+ if called_name in imports_map and imports_map[called_name]:
853
+ resolved_path = imports_map[called_name][0] # Take the first path for now
854
+ else:
855
+ resolved_path = caller_file_path
753
856
 
754
857
  caller_context = call.get('context')
755
858
  inferred_type = call.get('inferred_obj_type')
@@ -6,6 +6,18 @@ from pathlib import Path
6
6
  from typing import Set
7
7
 
8
8
  import stdlibs
9
+ import os
10
+ from datetime import datetime
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ def debug_log(message):
15
+ """Write debug message to a file"""
16
+ debug_file = os.path.expanduser("~/mcp_debug.log")
17
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
18
+ with open(debug_file, "a") as f:
19
+ f.write(f"[{timestamp}] {message}\n")
20
+ f.flush()
9
21
 
10
22
  logger = logging.getLogger(__name__)
11
23
 
@@ -32,6 +44,7 @@ class ImportExtractor:
32
44
  imports.add(node.module.split('.')[0]) # Get top-level package
33
45
  except Exception as e:
34
46
  logger.warning(f"Error parsing or reading {file_path}: {e}")
47
+ debug_log(f"Raw imports extracted from {file_path}: {imports}") # Add this line
35
48
  return imports
36
49
 
37
50
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codegraphcontext
3
- Version: 0.1.7
3
+ Version: 0.1.8
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
@@ -53,9 +53,13 @@ Dynamic: license-file
53
53
  # CodeGraphContext
54
54
  [![Build Status](https://github.com/Shashankss1205/CodeGraphContext/actions/workflows/test.yml/badge.svg)](https://github.com/Shashankss1205/CodeGraphContext/actions/workflows/test.yml)
55
55
 
56
-
57
56
  An MCP server that indexes local code into a graph database to provide context to AI assistants.
58
57
 
58
+ ## Project Details
59
+ - **Version:** 0.1.8
60
+ - **Authors:** Shashank Shekhar Singh <shashankshekharsingh1205@gmail.com>
61
+ - **License:** MIT License (See [LICENSE](LICENSE) for details)
62
+
59
63
  ## Features
60
64
 
61
65
  - **Code Indexing:** Analyzes Python code and builds a knowledge graph of its components.
@@ -63,12 +67,23 @@ An MCP server that indexes local code into a graph database to provide context t
63
67
  - **Live Updates:** Watches local files for changes and automatically updates the graph.
64
68
  - **Interactive Setup:** A user-friendly command-line wizard for easy setup.
65
69
 
70
+ ## Dependencies
71
+
72
+ - `neo4j>=5.15.0`
73
+ - `watchdog>=3.0.0`
74
+ - `requests>=2.31.0`
75
+ - `stdlibs>=2023.11.18`
76
+ - `typer[all]>=0.9.0`
77
+ - `rich>=13.7.0`
78
+ - `inquirerpy>=0.3.4`
79
+ - `python-dotenv>=1.0.0`
80
+
66
81
  ## Getting Started
67
82
 
68
83
  1. **Install:** `pip install codegraphcontext`
69
84
  2. **Setup:** `cgc setup`
70
85
  3. **Start:** `cgc start`
71
- 4. **Index Code:** `cgc tool add-code-to-graph '{"path": "/path/to/your/project"}'`
86
+ 4. **Index Code:** `cgc tool add-code-to-graph '{"path": "/path/to/your/project"}'` (Under active development)
72
87
 
73
88
  ## MCP Client Configuration
74
89
 
@@ -98,7 +113,11 @@ Add the following to your MCP client's configuration:
98
113
  "analyze_code_relationships",
99
114
  "watch_directory",
100
115
  "find_dead_code",
101
- "execute_cypher_query"
116
+ "execute_cypher_query",
117
+ "calculate_cyclomatic_complexity",
118
+ "find_most_complex_functions",
119
+ "list_indexed_repositories",
120
+ "delete_repository"
102
121
  ],
103
122
  "disabled": false
104
123
  },
@@ -149,5 +168,22 @@ Once the server is running, you can interact with it through your AI assistant u
149
168
  - "Which files import the `requests` library?"
150
169
  - "Find all implementations of the `render` method."
151
170
 
171
+ - **Advanced Call Chain and Dependency Tracking (Spanning Hundreds of Files):**
172
+ The CodeGraphContext excels at tracing complex execution flows and dependencies across vast codebases. Leveraging the power of graph databases, it can identify direct and indirect callers and callees, even when a function is called through multiple layers of abstraction or across numerous files. This is invaluable for:
173
+ - **Impact Analysis:** Understand the full ripple effect of a change to a core function.
174
+ - **Debugging:** Trace the path of execution from an entry point to a specific bug.
175
+ - **Code Comprehension:** Grasp how different parts of a large system interact.
176
+
177
+ - "Show me the full call chain from the `main` function to `process_data`."
178
+ - "Find all functions that directly or indirectly call `validate_input`."
179
+ - "What are all the functions that `initialize_system` eventually calls?"
180
+ - "Trace the dependencies of the `DatabaseManager` module."
181
+
152
182
  - **Code Quality and Maintenance:**
153
183
  - "Is there any dead or unused code in this project?"
184
+ - "Calculate the cyclomatic complexity of the `process_data` function in `src/utils.py`."
185
+ - "Find the 5 most complex functions in the codebase."
186
+
187
+ - **Repository Management:**
188
+ - "List all currently indexed repositories."
189
+ - "Delete the indexed repository at `/path/to/old-project`."