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.
- codegraph/__init__.py +12 -0
- codegraph/__main__.py +6 -0
- codegraph/cli.py +911 -0
- codegraph/codegraph.py +618 -0
- codegraph/context/__init__.py +216 -0
- codegraph/db/__init__.py +11 -0
- codegraph/db/connection.py +163 -0
- codegraph/db/queries.py +752 -0
- codegraph/db/schema.sql +151 -0
- codegraph/directory.py +160 -0
- codegraph/errors.py +101 -0
- codegraph/extraction/__init__.py +956 -0
- codegraph/extraction/languages/__init__.py +64 -0
- codegraph/extraction/languages/base.py +132 -0
- codegraph/extraction/languages/c_cfg.py +53 -0
- codegraph/extraction/languages/cpp_cfg.py +60 -0
- codegraph/extraction/languages/dart_cfg.py +53 -0
- codegraph/extraction/languages/go_cfg.py +70 -0
- codegraph/extraction/languages/java_cfg.py +62 -0
- codegraph/extraction/languages/javascript_cfg.py +84 -0
- codegraph/extraction/languages/kotlin_cfg.py +52 -0
- codegraph/extraction/languages/lua_cfg.py +65 -0
- codegraph/extraction/languages/python_cfg.py +75 -0
- codegraph/extraction/languages/ruby_cfg.py +48 -0
- codegraph/extraction/languages/rust_cfg.py +68 -0
- codegraph/extraction/languages/scala_cfg.py +59 -0
- codegraph/extraction/languages/swift_cfg.py +64 -0
- codegraph/extraction/languages/typescript_cfg.py +89 -0
- codegraph/extraction/tree_sitter_extractor.py +689 -0
- codegraph/graph/__init__.py +685 -0
- codegraph/installer/__init__.py +6 -0
- codegraph/mcp/__init__.py +668 -0
- codegraph/project_config.py +191 -0
- codegraph/resolution/__init__.py +337 -0
- codegraph/search/__init__.py +653 -0
- codegraph/sync/__init__.py +204 -0
- codegraph/types.py +334 -0
- codegraph/ui/__init__.py +11 -0
- codegraph/utils.py +218 -0
- codegraph_py-1.0.0.dist-info/METADATA +238 -0
- codegraph_py-1.0.0.dist-info/RECORD +44 -0
- codegraph_py-1.0.0.dist-info/WHEEL +5 -0
- codegraph_py-1.0.0.dist-info/entry_points.txt +2 -0
- 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
|
+
}
|