codegraph-py 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. codegraph/__init__.py +12 -0
  2. codegraph/__main__.py +6 -0
  3. codegraph/cli.py +911 -0
  4. codegraph/codegraph.py +618 -0
  5. codegraph/context/__init__.py +216 -0
  6. codegraph/db/__init__.py +11 -0
  7. codegraph/db/connection.py +163 -0
  8. codegraph/db/queries.py +752 -0
  9. codegraph/db/schema.sql +151 -0
  10. codegraph/directory.py +160 -0
  11. codegraph/errors.py +101 -0
  12. codegraph/extraction/__init__.py +956 -0
  13. codegraph/extraction/languages/__init__.py +64 -0
  14. codegraph/extraction/languages/base.py +132 -0
  15. codegraph/extraction/languages/c_cfg.py +53 -0
  16. codegraph/extraction/languages/cpp_cfg.py +60 -0
  17. codegraph/extraction/languages/dart_cfg.py +53 -0
  18. codegraph/extraction/languages/go_cfg.py +70 -0
  19. codegraph/extraction/languages/java_cfg.py +62 -0
  20. codegraph/extraction/languages/javascript_cfg.py +84 -0
  21. codegraph/extraction/languages/kotlin_cfg.py +52 -0
  22. codegraph/extraction/languages/lua_cfg.py +65 -0
  23. codegraph/extraction/languages/python_cfg.py +75 -0
  24. codegraph/extraction/languages/ruby_cfg.py +48 -0
  25. codegraph/extraction/languages/rust_cfg.py +68 -0
  26. codegraph/extraction/languages/scala_cfg.py +59 -0
  27. codegraph/extraction/languages/swift_cfg.py +64 -0
  28. codegraph/extraction/languages/typescript_cfg.py +89 -0
  29. codegraph/extraction/tree_sitter_extractor.py +689 -0
  30. codegraph/graph/__init__.py +685 -0
  31. codegraph/installer/__init__.py +6 -0
  32. codegraph/mcp/__init__.py +668 -0
  33. codegraph/project_config.py +191 -0
  34. codegraph/resolution/__init__.py +337 -0
  35. codegraph/search/__init__.py +653 -0
  36. codegraph/sync/__init__.py +204 -0
  37. codegraph/types.py +334 -0
  38. codegraph/ui/__init__.py +11 -0
  39. codegraph/utils.py +218 -0
  40. codegraph_py-1.0.0.dist-info/METADATA +238 -0
  41. codegraph_py-1.0.0.dist-info/RECORD +44 -0
  42. codegraph_py-1.0.0.dist-info/WHEEL +5 -0
  43. codegraph_py-1.0.0.dist-info/entry_points.txt +2 -0
  44. codegraph_py-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,668 @@
1
+ """
2
+ CodeGraph MCP Server
3
+
4
+ Model Context Protocol server that exposes CodeGraph functionality
5
+ as tools for AI assistants, with optional file watching for auto-reindex.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import os
12
+ import sys
13
+ import time
14
+ import threading
15
+ from typing import Any, Dict, List, Optional, Callable
16
+
17
+ from codegraph.codegraph import CodeGraph
18
+ from codegraph.types import SearchOptions
19
+ from codegraph.sync import FileWatcher, PendingFile
20
+
21
+
22
+ class MCPServer:
23
+ """
24
+ MCP Server that provides CodeGraph tools to AI assistants.
25
+
26
+ Communicates via stdio using JSON-RPC 2.0 over newline-delimited messages.
27
+ Optionally watches the project for file changes and auto-syncs.
28
+ """
29
+
30
+ def __init__(self, project_root: str, watch: bool = False):
31
+ self._project_root = project_root
32
+ self._codegraph: Optional[CodeGraph] = None
33
+ self._running = False
34
+ self._request_id = 0
35
+ self._watch = watch
36
+ self._watcher: Optional[FileWatcher] = None
37
+ self._last_sync_info: Dict = {}
38
+ self._sync_lock = threading.Lock()
39
+
40
+ def _get_cg(self) -> CodeGraph:
41
+ """Get or open the CodeGraph instance."""
42
+ if self._codegraph is None:
43
+ self._codegraph = CodeGraph.open_sync(self._project_root)
44
+ return self._codegraph
45
+
46
+ def _on_file_change(self, pending: List[PendingFile]) -> None:
47
+ """Callback when files change — auto-sync."""
48
+ with self._sync_lock:
49
+ try:
50
+ cg = self._get_cg()
51
+ result = cg.sync()
52
+ if result.files_added > 0 or result.files_modified > 0 or result.files_removed > 0:
53
+ # Re-resolve references after sync
54
+ ref_count = cg._queries.get_unresolved_refs_count()
55
+ if ref_count > 0:
56
+ cg._resolver.initialize()
57
+ cg._resolve_references()
58
+ self._last_sync_info = {
59
+ 'added': result.files_added,
60
+ 'modified': result.files_modified,
61
+ 'removed': result.files_removed,
62
+ 'timestamp': time.time(),
63
+ }
64
+ except Exception:
65
+ pass # Silent auto-sync failures
66
+
67
+ async def start(self) -> None:
68
+ """Start the MCP server (stdio-based)."""
69
+ self._running = True
70
+
71
+ # Start file watcher if enabled
72
+ if self._watch:
73
+ try:
74
+ cg = self._get_cg()
75
+ self._watcher = cg.watch(on_change=self._on_file_change)
76
+ except Exception:
77
+ pass # File watching is optional
78
+
79
+ # Notify stderr that server is ready (for host process)
80
+ sys.stderr.write('CodeGraph MCP server started\n')
81
+ sys.stderr.flush()
82
+
83
+ # Read and process JSON-RPC messages from stdin
84
+ while self._running:
85
+ try:
86
+ line = sys.stdin.readline()
87
+ if not line:
88
+ break
89
+
90
+ message = json.loads(line.strip())
91
+ await self._handle_message(message)
92
+ except EOFError:
93
+ break
94
+ except json.JSONDecodeError:
95
+ continue
96
+ except Exception as e:
97
+ self._send_error(-32700, f'Parse error: {str(e)}')
98
+
99
+ def stop(self) -> None:
100
+ """Stop the MCP server."""
101
+ self._running = False
102
+ if self._watcher:
103
+ try:
104
+ self._watcher.stop()
105
+ except Exception:
106
+ pass
107
+ if self._codegraph:
108
+ try:
109
+ self._codegraph.close()
110
+ except Exception:
111
+ pass
112
+
113
+ async def _handle_message(self, message: Dict) -> None:
114
+ """Handle a JSON-RPC message."""
115
+ method = message.get('method', '')
116
+ params = message.get('params', {})
117
+ msg_id = message.get('id')
118
+
119
+ # Handle JSON-RPC methods
120
+ if method == 'initialize':
121
+ self._send_response(msg_id, {
122
+ 'protocolVersion': '2024-11-05',
123
+ 'capabilities': {
124
+ 'tools': {},
125
+ },
126
+ 'serverInfo': {
127
+ 'name': 'codegraph',
128
+ 'version': '1.0.0',
129
+ },
130
+ })
131
+ elif method == 'tools/list':
132
+ self._send_response(msg_id, {'tools': self._get_tool_definitions()})
133
+ elif method == 'tools/call':
134
+ await self._handle_tool_call(msg_id, params)
135
+ elif method == 'notifications/initialized':
136
+ pass # Acknowledge
137
+ elif method == 'shutdown':
138
+ self._send_response(msg_id, None)
139
+ self.stop()
140
+
141
+ def _get_tool_definitions(self) -> List[Dict]:
142
+ """Get MCP tool definitions."""
143
+ return [
144
+ {
145
+ 'name': 'codegraph_explore',
146
+ 'description': 'Explore an area of the codebase: returns relevant symbols source grouped by file, plus call paths among them. Primary tool — call this first before other codegraph tools.',
147
+ 'inputSchema': {
148
+ 'type': 'object',
149
+ 'properties': {
150
+ 'query': {
151
+ 'type': 'string',
152
+ 'description': 'Natural language query or symbol names to explore',
153
+ },
154
+ 'maxFiles': {
155
+ 'type': 'number',
156
+ 'description': 'Maximum number of files to include source from',
157
+ 'default': 12,
158
+ },
159
+ 'projectPath': {
160
+ 'type': 'string',
161
+ 'description': 'Alternative project path for cross-project queries',
162
+ },
163
+ },
164
+ 'required': ['query'],
165
+ },
166
+ },
167
+ {
168
+ 'name': 'codegraph_node',
169
+ 'description': 'Get source code for a symbol or file. Returns location, signature, and source content. For a file path, returns the full file content. For a symbol name, returns the definition with its surrounding context.',
170
+ 'inputSchema': {
171
+ 'type': 'object',
172
+ 'properties': {
173
+ 'name': {
174
+ 'type': 'string',
175
+ 'description': 'Symbol name or file path to look up',
176
+ },
177
+ 'includeCode': {
178
+ 'type': 'boolean',
179
+ 'description': 'Whether to include source code in response',
180
+ 'default': True,
181
+ },
182
+ 'projectPath': {
183
+ 'type': 'string',
184
+ 'description': 'Alternative project path',
185
+ },
186
+ },
187
+ 'required': ['name'],
188
+ },
189
+ },
190
+ {
191
+ 'name': 'codegraph_search',
192
+ 'description': 'Quick symbol search by name. Returns matching symbol locations (file + line number). For detailed source context, use codegraph_explore or codegraph_node.',
193
+ 'inputSchema': {
194
+ 'type': 'object',
195
+ 'properties': {
196
+ 'query': {
197
+ 'type': 'string',
198
+ 'description': 'Symbol name or pattern to search for',
199
+ },
200
+ 'limit': {
201
+ 'type': 'number',
202
+ 'description': 'Maximum number of results',
203
+ 'default': 10,
204
+ },
205
+ 'projectPath': {
206
+ 'type': 'string',
207
+ 'description': 'Alternative project path',
208
+ },
209
+ },
210
+ 'required': ['query'],
211
+ },
212
+ },
213
+ {
214
+ 'name': 'codegraph_callers',
215
+ 'description': 'List functions that call a given symbol. Shows the call chain — what invokes the function or method.',
216
+ 'inputSchema': {
217
+ 'type': 'object',
218
+ 'properties': {
219
+ 'symbol': {
220
+ 'type': 'string',
221
+ 'description': 'Symbol name to find callers for',
222
+ },
223
+ 'depth': {
224
+ 'type': 'number',
225
+ 'description': 'How deep to traverse the call chain',
226
+ 'default': 1,
227
+ },
228
+ 'projectPath': {
229
+ 'type': 'string',
230
+ 'description': 'Alternative project path',
231
+ },
232
+ },
233
+ 'required': ['symbol'],
234
+ },
235
+ },
236
+ {
237
+ 'name': 'codegraph_callees',
238
+ 'description': 'List functions a symbol calls. Shows what the function or method invokes.',
239
+ 'inputSchema': {
240
+ 'type': 'object',
241
+ 'properties': {
242
+ 'symbol': {
243
+ 'type': 'string',
244
+ 'description': 'Symbol name to find callees for',
245
+ },
246
+ 'depth': {
247
+ 'type': 'number',
248
+ 'description': 'How deep to traverse the call chain',
249
+ 'default': 1,
250
+ },
251
+ 'projectPath': {
252
+ 'type': 'string',
253
+ 'description': 'Alternative project path',
254
+ },
255
+ },
256
+ 'required': ['symbol'],
257
+ },
258
+ },
259
+ {
260
+ 'name': 'codegraph_impact',
261
+ 'description': 'Analyze what code is affected by changing a symbol. Shows the blast radius — all symbols that depend on the given symbol, directly or transitively.',
262
+ 'inputSchema': {
263
+ 'type': 'object',
264
+ 'properties': {
265
+ 'symbol': {
266
+ 'type': 'string',
267
+ 'description': 'Symbol name to analyze impact for',
268
+ },
269
+ 'depth': {
270
+ 'type': 'number',
271
+ 'description': 'How deep to traverse for impact analysis',
272
+ 'default': 3,
273
+ },
274
+ 'projectPath': {
275
+ 'type': 'string',
276
+ 'description': 'Alternative project path',
277
+ },
278
+ },
279
+ 'required': ['symbol'],
280
+ },
281
+ },
282
+ {
283
+ 'name': 'codegraph_status',
284
+ 'description': 'Get index health. Returns files/nodes/edges counts, pending changes, and whether re-index is recommended.',
285
+ 'inputSchema': {
286
+ 'type': 'object',
287
+ 'properties': {
288
+ 'projectPath': {
289
+ 'type': 'string',
290
+ 'description': 'Project path',
291
+ },
292
+ },
293
+ },
294
+ },
295
+ {
296
+ 'name': 'codegraph_files',
297
+ 'description': 'List indexed files in the project tree. Optionally filter by path prefix or file extension.',
298
+ 'inputSchema': {
299
+ 'type': 'object',
300
+ 'properties': {
301
+ 'path': {
302
+ 'type': 'string',
303
+ 'description': 'Optional path prefix to filter by',
304
+ },
305
+ 'extensions': {
306
+ 'type': 'array',
307
+ 'items': {'type': 'string'},
308
+ 'description': 'Optional file extensions to filter by (e.g. [".py", ".ts"])',
309
+ },
310
+ 'projectPath': {
311
+ 'type': 'string',
312
+ 'description': 'Alternative project path',
313
+ },
314
+ },
315
+ },
316
+ },
317
+ ]
318
+
319
+ async def _handle_tool_call(self, msg_id: int, params: Dict) -> None:
320
+ """Handle a tools/call request."""
321
+ tool_name = params.get('name', '')
322
+ arguments = params.get('arguments', {})
323
+
324
+ handler = ToolHandler(self)
325
+ try:
326
+ result = await handler.execute(tool_name, arguments)
327
+ self._send_response(msg_id, result)
328
+ except Exception as e:
329
+ self._send_error(-32603, f'Internal error: {str(e)}', msg_id)
330
+
331
+ def _send_response(self, msg_id: int, result: Any) -> None:
332
+ """Send a JSON-RPC response."""
333
+ response = {
334
+ 'jsonrpc': '2.0',
335
+ 'id': msg_id,
336
+ 'result': result,
337
+ }
338
+ sys.stdout.write(json.dumps(response) + '\n')
339
+ sys.stdout.flush()
340
+
341
+ def _send_error(self, code: int, message: str,
342
+ msg_id: Optional[int] = None) -> None:
343
+ """Send a JSON-RPC error response."""
344
+ response = {
345
+ 'jsonrpc': '2.0',
346
+ 'id': msg_id,
347
+ 'error': {
348
+ 'code': code,
349
+ 'message': message,
350
+ },
351
+ }
352
+ sys.stdout.write(json.dumps(response) + '\n')
353
+ sys.stdout.flush()
354
+
355
+ def _open_cg(self, project_path: Optional[str] = None) -> CodeGraph:
356
+ """Open a CodeGraph instance, lazily."""
357
+ root = project_path or self._project_root
358
+ if self._codegraph and self._codegraph.get_project_root() == root:
359
+ return self._codegraph
360
+ self._codegraph = CodeGraph.open_sync(root)
361
+ return self._codegraph
362
+
363
+
364
+ class ToolHandler:
365
+ """Handles MCP tool execution."""
366
+
367
+ def __init__(self, server: MCPServer):
368
+ self._server = server
369
+
370
+ async def execute(self, tool_name: str, arguments: Dict) -> Dict:
371
+ """Execute a tool and return MCP-formatted result."""
372
+ project_path = arguments.get('projectPath')
373
+ cg = self._server._open_cg(project_path)
374
+
375
+ handlers = {
376
+ 'codegraph_explore': self._handle_explore,
377
+ 'codegraph_node': self._handle_node,
378
+ 'codegraph_search': self._handle_search,
379
+ 'codegraph_callers': self._handle_callers,
380
+ 'codegraph_callees': self._handle_callees,
381
+ 'codegraph_impact': self._handle_impact,
382
+ 'codegraph_status': self._handle_status,
383
+ 'codegraph_files': self._handle_files,
384
+ }
385
+
386
+ handler = handlers.get(tool_name)
387
+ if not handler:
388
+ return {
389
+ 'content': [{'type': 'text', 'text': f'Unknown tool: {tool_name}'}],
390
+ 'isError': True,
391
+ }
392
+
393
+ return await handler(cg, arguments)
394
+
395
+ async def _handle_explore(self, cg: CodeGraph, args: Dict) -> Dict:
396
+ """Handle codegraph_explore tool."""
397
+ query = args.get('query', '')
398
+ if not query:
399
+ return {
400
+ 'content': [{'type': 'text', 'text': 'Query is required'}],
401
+ 'isError': True,
402
+ }
403
+
404
+ max_files = args.get('maxFiles', 12)
405
+
406
+ # Search for relevant nodes
407
+ results = cg.search_nodes(query, SearchOptions(limit=max_files))
408
+
409
+ if not results:
410
+ return {
411
+ 'content': [{
412
+ 'type': 'text',
413
+ 'text': f'No results found for "{query}". The project may not be indexed yet — run `codegraph index` first, or try a different query.',
414
+ }],
415
+ }
416
+
417
+ # Build response grouped by file
418
+ from collections import defaultdict
419
+ files: Dict[str, List] = defaultdict(list)
420
+
421
+ for result in results:
422
+ node = result.node
423
+ files[node.file_path].append(node)
424
+
425
+ output_parts = []
426
+ for filepath, nodes in files.items():
427
+ output_parts.append(f'\n### {filepath}\n')
428
+ for node in nodes:
429
+ sig = f' {node.signature}' if node.signature else ''
430
+ output_parts.append(
431
+ f'- `{node.kind}` **{node.name}**{sig} (line {node.start_line})'
432
+ )
433
+
434
+ return {
435
+ 'content': [{'type': 'text', 'text': ''.join(output_parts)}],
436
+ }
437
+
438
+ async def _handle_node(self, cg: CodeGraph, args: Dict) -> Dict:
439
+ """Handle codegraph_node tool."""
440
+ name = args.get('name', '')
441
+ include_code = args.get('includeCode', True)
442
+
443
+ if not name:
444
+ return {
445
+ 'content': [{'type': 'text', 'text': 'Name is required'}],
446
+ 'isError': True,
447
+ }
448
+
449
+ # Try as file path first
450
+ nodes = cg.get_nodes_by_file(name)
451
+ if not nodes:
452
+ # Try as symbol name
453
+ nodes = cg.get_nodes_by_name(name)
454
+
455
+ if not nodes:
456
+ return {
457
+ 'content': [{'type': 'text', 'text': f'No symbol found: {name}'}],
458
+ }
459
+
460
+ output_parts = []
461
+ for node in nodes[:5]:
462
+ output_parts.append(
463
+ f'**{node.kind}**: `{node.name}`\n'
464
+ f'- File: {node.file_path}:{node.start_line}\n'
465
+ )
466
+ if node.signature:
467
+ output_parts.append(f'- Signature: `{node.signature}`\n')
468
+ if node.docstring:
469
+ output_parts.append(f'- Doc: {node.docstring[:200]}\n')
470
+
471
+ # Read source code if requested
472
+ if include_code:
473
+ try:
474
+ filepath = os.path.join(cg.get_project_root(), node.file_path)
475
+ with open(filepath, 'r', encoding='utf-8', errors='replace') as f:
476
+ lines = f.readlines()
477
+ code = ''.join(lines[node.start_line - 1:node.end_line])
478
+ output_parts.append(f'```\n{code}\n```\n')
479
+ except Exception:
480
+ pass
481
+
482
+ return {
483
+ 'content': [{'type': 'text', 'text': ''.join(output_parts)}],
484
+ }
485
+
486
+ async def _handle_search(self, cg: CodeGraph, args: Dict) -> Dict:
487
+ """Handle codegraph_search tool."""
488
+ query = args.get('query', '')
489
+ limit = args.get('limit', 10)
490
+
491
+ if not query:
492
+ return {
493
+ 'content': [{'type': 'text', 'text': 'Query is required'}],
494
+ 'isError': True,
495
+ }
496
+
497
+ results = cg.search_nodes(query, SearchOptions(limit=limit))
498
+
499
+ if not results:
500
+ return {
501
+ 'content': [{'type': 'text', 'text': f'No results for "{query}"'}],
502
+ }
503
+
504
+ output = f'Search results for "{query}":\n\n'
505
+ for r in results:
506
+ node = r.node
507
+ output += f' {node.kind:12} {node.name} ({node.file_path}:{node.start_line})\n'
508
+ if node.signature:
509
+ output += f' {"":12} {node.signature}\n'
510
+
511
+ return {
512
+ 'content': [{'type': 'text', 'text': output}],
513
+ }
514
+
515
+ async def _handle_callers(self, cg: CodeGraph, args: Dict) -> Dict:
516
+ """Handle codegraph_callers tool."""
517
+ symbol = args.get('symbol', '')
518
+ depth = args.get('depth', 1)
519
+
520
+ nodes = cg.get_nodes_by_name(symbol)
521
+ if not nodes:
522
+ return {
523
+ 'content': [{'type': 'text', 'text': f'Symbol not found: {symbol}'}],
524
+ }
525
+
526
+ callers = cg.get_callers(nodes[0].id, max_depth=depth)
527
+
528
+ if not callers:
529
+ return {
530
+ 'content': [{'type': 'text', 'text': f'No callers found for {symbol}'}],
531
+ }
532
+
533
+ output = f'Callers of {symbol}:\n\n'
534
+ for caller_node, edge in callers:
535
+ output += f' {caller_node.kind:12} {caller_node.name} ({caller_node.file_path}:{caller_node.start_line})\n'
536
+
537
+ return {
538
+ 'content': [{'type': 'text', 'text': output}],
539
+ }
540
+
541
+ async def _handle_callees(self, cg: CodeGraph, args: Dict) -> Dict:
542
+ """Handle codegraph_callees tool."""
543
+ symbol = args.get('symbol', '')
544
+ depth = args.get('depth', 1)
545
+
546
+ nodes = cg.get_nodes_by_name(symbol)
547
+ if not nodes:
548
+ return {
549
+ 'content': [{'type': 'text', 'text': f'Symbol not found: {symbol}'}],
550
+ }
551
+
552
+ callees = cg.get_callees(nodes[0].id, max_depth=depth)
553
+
554
+ if not callees:
555
+ return {
556
+ 'content': [{'type': 'text', 'text': f'No callees found for {symbol}'}],
557
+ }
558
+
559
+ output = f'Callees of {symbol}:\n\n'
560
+ for callee_node, edge in callees:
561
+ output += f' {callee_node.kind:12} {callee_node.name} ({callee_node.file_path}:{callee_node.start_line})\n'
562
+
563
+ return {
564
+ 'content': [{'type': 'text', 'text': output}],
565
+ }
566
+
567
+ async def _handle_impact(self, cg: CodeGraph, args: Dict) -> Dict:
568
+ """Handle codegraph_impact tool."""
569
+ symbol = args.get('symbol', '')
570
+ depth = args.get('depth', 3)
571
+
572
+ nodes = cg.get_nodes_by_name(symbol)
573
+ if not nodes:
574
+ return {
575
+ 'content': [{'type': 'text', 'text': f'Symbol not found: {symbol}'}],
576
+ }
577
+
578
+ subgraph = cg.get_impact_radius(nodes[0].id, max_depth=depth)
579
+
580
+ if not subgraph.nodes:
581
+ return {
582
+ 'content': [{'type': 'text', 'text': f'No impact detected for {symbol}'}],
583
+ }
584
+
585
+ files = set(n.file_path for n in subgraph.nodes.values())
586
+ output = (
587
+ f'Impact analysis for {symbol}:\n'
588
+ f' {len(subgraph.nodes)} symbols affected\n'
589
+ f' {len(subgraph.edges)} relationships\n'
590
+ f' {len(files)} files involved\n\n'
591
+ f'Affected symbols:\n'
592
+ )
593
+
594
+ for node in subgraph.nodes.values():
595
+ if node.id != nodes[0].id:
596
+ output += f' {node.kind:12} {node.name} ({node.file_path}:{node.start_line})\n'
597
+
598
+ return {
599
+ 'content': [{'type': 'text', 'text': output}],
600
+ }
601
+
602
+ async def _handle_status(self, cg: CodeGraph, args: Dict) -> Dict:
603
+ """Handle codegraph_status tool."""
604
+ stats = cg.get_stats()
605
+ changes = cg.get_changed_files()
606
+ build_info = cg.get_index_build_info()
607
+
608
+ pending_count = (
609
+ len(changes.get('added', []))
610
+ + len(changes.get('modified', []))
611
+ + len(changes.get('removed', []))
612
+ )
613
+
614
+ output = (
615
+ f'CodeGraph Status for {cg.get_project_root()}:\n\n'
616
+ f'**Index Statistics:**\n'
617
+ f' Files: {stats.file_count}\n'
618
+ f' Nodes: {stats.node_count}\n'
619
+ f' Edges: {stats.edge_count}\n'
620
+ f' DB Size: {stats.db_size_bytes / 1024 / 1024:.2f} MB\n'
621
+ f' Backend: sqlite3 (built-in, full WAL)\n\n'
622
+ )
623
+
624
+ if pending_count > 0:
625
+ output += '**Pending Changes:**\n'
626
+ if changes.get('added'):
627
+ output += f' Added: {len(changes["added"])} files\n'
628
+ if changes.get('modified'):
629
+ output += f' Modified: {len(changes["modified"])} files\n'
630
+ if changes.get('removed'):
631
+ output += f' Removed: {len(changes["removed"])} files\n'
632
+ else:
633
+ output += '**Index is up to date**\n'
634
+
635
+ return {
636
+ 'content': [{'type': 'text', 'text': output}],
637
+ }
638
+
639
+ async def _handle_files(self, cg: CodeGraph, args: Dict) -> Dict:
640
+ """Handle codegraph_files tool."""
641
+ path_filter = args.get('path', '')
642
+ extensions = args.get('extensions')
643
+
644
+ files = cg.get_nodes_by_file('') # This doesn't work - need file iteration
645
+ # Use the QueryBuilder to get all file paths instead
646
+ all_files = cg._queries.get_all_file_paths()
647
+
648
+ # Apply filters
649
+ if path_filter:
650
+ all_files = [f for f in all_files if f.startswith(path_filter)]
651
+ if extensions:
652
+ all_files = [f for f in all_files
653
+ if any(f.endswith(ext) for ext in extensions)]
654
+
655
+ if not all_files:
656
+ return {
657
+ 'content': [{'type': 'text', 'text': 'No files found'}],
658
+ }
659
+
660
+ output = f'Files ({len(all_files)} total):\n\n'
661
+ for f in all_files[:100]:
662
+ output += f' {f}\n'
663
+ if len(all_files) > 100:
664
+ output += f' ... and {len(all_files) - 100} more\n'
665
+
666
+ return {
667
+ 'content': [{'type': 'text', 'text': output}],
668
+ }