cortexcode 0.1.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.
@@ -0,0 +1,455 @@
1
+ """MCP Server — Model Context Protocol server for AI agent integration.
2
+
3
+ Provides tools for AI agents to query the CortexCode index directly.
4
+ Supports: symbol lookup, file context, call graph traversal, diff context.
5
+
6
+ Usage:
7
+ cortexcode mcp # Start MCP server on stdin/stdout
8
+ cortexcode mcp --port 8080 # Start on HTTP port
9
+ """
10
+
11
+ import json
12
+ import sys
13
+ from pathlib import Path
14
+ from typing import Any
15
+
16
+
17
+ def create_mcp_response(id: Any, result: Any) -> dict:
18
+ """Create a JSON-RPC 2.0 response."""
19
+ return {"jsonrpc": "2.0", "id": id, "result": result}
20
+
21
+
22
+ def create_mcp_error(id: Any, code: int, message: str) -> dict:
23
+ """Create a JSON-RPC 2.0 error response."""
24
+ return {"jsonrpc": "2.0", "id": id, "error": {"code": code, "message": message}}
25
+
26
+
27
+ def load_index(index_path: Path) -> dict | None:
28
+ """Load index from disk."""
29
+ try:
30
+ if index_path.exists():
31
+ return json.loads(index_path.read_text(encoding="utf-8"))
32
+ except (json.JSONDecodeError, OSError):
33
+ pass
34
+ return None
35
+
36
+
37
+ def auto_index_project(root_path: Path) -> bool:
38
+ """Auto-index the project if no index exists."""
39
+ from cortexcode.indexer import CodeIndexer
40
+
41
+ index_path = root_path / ".cortexcode" / "index.json"
42
+ if index_path.exists():
43
+ return True
44
+
45
+ try:
46
+ print(f"CortexCode: Auto-indexing {root_path}...", file=sys.stderr)
47
+ indexer = CodeIndexer()
48
+ indexer.index_directory(root_path)
49
+
50
+ output_dir = root_path / ".cortexcode"
51
+ output_dir.mkdir(parents=True, exist_ok=True)
52
+ indexer.save_index(output_dir / "index.json")
53
+ print(f"CortexCode: Index created successfully", file=sys.stderr)
54
+ return True
55
+ except Exception as e:
56
+ print(f"CortexCode: Auto-index failed: {e}", file=sys.stderr)
57
+ return False
58
+
59
+
60
+ class CortexCodeMCPServer:
61
+ """MCP server that exposes CortexCode index as tools."""
62
+
63
+ def __init__(self, index_path: Path | None = None):
64
+ self.index_path = index_path or Path(".cortexcode/index.json")
65
+ self.index: dict | None = None
66
+ self._reload_index()
67
+
68
+ def _reload_index(self):
69
+ """Reload the index from disk, auto-index if needed."""
70
+ self.index = load_index(self.index_path)
71
+
72
+ # If no index, try to auto-index
73
+ if not self.index and self.index_path.parent.exists():
74
+ root = self.index_path.parent.parent
75
+ if auto_index_project(root):
76
+ self.index = load_index(self.index_path)
77
+
78
+ def handle_request(self, request: dict) -> dict:
79
+ """Handle a JSON-RPC 2.0 request."""
80
+ method = request.get("method", "")
81
+ params = request.get("params", {})
82
+ req_id = request.get("id")
83
+
84
+ if method == "initialize":
85
+ return create_mcp_response(req_id, {
86
+ "protocolVersion": "2024-11-05",
87
+ "capabilities": {
88
+ "tools": {"listChanged": False},
89
+ },
90
+ "serverInfo": {
91
+ "name": "cortexcode",
92
+ "version": "0.1.0",
93
+ },
94
+ })
95
+
96
+ elif method == "notifications/initialized":
97
+ return None # No response needed for notifications
98
+
99
+ elif method == "tools/list":
100
+ return create_mcp_response(req_id, {
101
+ "tools": self._get_tools(),
102
+ })
103
+
104
+ elif method == "tools/call":
105
+ tool_name = params.get("name", "")
106
+ tool_args = params.get("arguments", {})
107
+ return self._call_tool(req_id, tool_name, tool_args)
108
+
109
+ elif method == "ping":
110
+ return create_mcp_response(req_id, {})
111
+
112
+ else:
113
+ return create_mcp_error(req_id, -32601, f"Method not found: {method}")
114
+
115
+ def _get_tools(self) -> list[dict]:
116
+ """Return list of available tools."""
117
+ return [
118
+ {
119
+ "name": "cortexcode_search",
120
+ "description": "USE THIS when user asks to find, locate, search for a function, class, method, or any code symbol. Also use when you need to know where something is defined. Returns type, file, line, params, and calls.",
121
+ "inputSchema": {
122
+ "type": "object",
123
+ "properties": {
124
+ "query": {"type": "string", "description": "What to search for - function name, class name, or any code symbol"},
125
+ "type": {"type": "string", "description": "Filter by type: function, class, method, interface", "enum": ["function", "class", "method", "interface"]},
126
+ "limit": {"type": "integer", "description": "Max results (default 10)", "default": 10},
127
+ },
128
+ "required": ["query"],
129
+ },
130
+ },
131
+ {
132
+ "name": "cortexcode_context",
133
+ "description": "USE THIS when you need to understand how a function/class works, see its implementation, or see what it calls/who calls it. Also use when user asks 'how does X work' or 'show me the code for X'.",
134
+ "inputSchema": {
135
+ "type": "object",
136
+ "properties": {
137
+ "query": {"type": "string", "description": "Symbol name to get context for, or 'file:symbol' for specific file"},
138
+ "num_results": {"type": "integer", "description": "Number of results", "default": 5},
139
+ },
140
+ "required": ["query"],
141
+ },
142
+ },
143
+ {
144
+ "name": "cortexcode_file_symbols",
145
+ "description": "USE THIS when you need to see all functions/classes in a specific file, or understand what a file exports/defines.",
146
+ "inputSchema": {
147
+ "type": "object",
148
+ "properties": {
149
+ "file_path": {"type": "string", "description": "Relative file path (e.g. 'src/auth.ts')"},
150
+ },
151
+ "required": ["file_path"],
152
+ },
153
+ },
154
+ {
155
+ "name": "cortexcode_call_graph",
156
+ "description": "USE THIS when you need to trace what functions call what, or find all callers of a function. Also use when user asks 'what uses this function' or 'where is this called'.",
157
+ "inputSchema": {
158
+ "type": "object",
159
+ "properties": {
160
+ "symbol": {"type": "string", "description": "Symbol name to trace"},
161
+ "depth": {"type": "integer", "description": "Depth of traversal (default 1)", "default": 1},
162
+ },
163
+ "required": ["symbol"],
164
+ },
165
+ },
166
+ {
167
+ "name": "cortexcode_diff",
168
+ "description": "USE THIS when you need to find what code changed, or see modified functions. Also use when user asks 'what changed' or 'what was modified recently'.",
169
+ "inputSchema": {
170
+ "type": "object",
171
+ "properties": {
172
+ "ref": {"type": "string", "description": "Git ref to compare against (default HEAD)", "default": "HEAD"},
173
+ },
174
+ },
175
+ },
176
+ {
177
+ "name": "cortexcode_stats",
178
+ "description": "USE THIS when user asks about project size, file count, language distribution, or wants to know 'how big is the project'.",
179
+ "inputSchema": {
180
+ "type": "object",
181
+ "properties": {},
182
+ },
183
+ },
184
+ {
185
+ "name": "cortexcode_deadcode",
186
+ "description": "USE THIS when user asks to find unused, unreachable, or dead code. Also use when user asks 'what functions are not used' or 'show me dead code'.",
187
+ "inputSchema": {
188
+ "type": "object",
189
+ "properties": {
190
+ "limit": {"type": "integer", "description": "Max results (default 20)", "default": 20},
191
+ },
192
+ },
193
+ },
194
+ {
195
+ "name": "cortexcode_complexity",
196
+ "description": "USE THIS when user asks about code complexity, most complex functions, or which functions are hard to maintain. Also use when user asks 'what is the most complex code' or 'show me complex functions'.",
197
+ "inputSchema": {
198
+ "type": "object",
199
+ "properties": {
200
+ "limit": {"type": "integer", "description": "Max results (default 20)", "default": 20},
201
+ },
202
+ },
203
+ },
204
+ {
205
+ "name": "cortexcode_impact",
206
+ "description": "USE THIS when user asks about change impact, what would break if they modify something, or which functions depend on a symbol. Also use when user asks 'what will break if I change X' or 'show me what uses this function'.",
207
+ "inputSchema": {
208
+ "type": "object",
209
+ "properties": {
210
+ "symbol": {"type": "string", "description": "Symbol name to analyze impact for"},
211
+ },
212
+ "required": ["symbol"],
213
+ },
214
+ },
215
+ {
216
+ "name": "cortexcode_file_deps",
217
+ "description": "USE THIS when you need to find what files import from what, or trace file dependencies. Also use when user asks 'what does this file depend on'.",
218
+ "inputSchema": {
219
+ "type": "object",
220
+ "properties": {
221
+ "file_path": {"type": "string", "description": "File to check dependencies for (optional, returns all if omitted)"},
222
+ },
223
+ },
224
+ },
225
+ ]
226
+
227
+ def _call_tool(self, req_id: Any, tool_name: str, args: dict) -> dict:
228
+ """Execute a tool call."""
229
+ self._reload_index()
230
+
231
+ if not self.index:
232
+ return create_mcp_response(req_id, {
233
+ "content": [{"type": "text", "text": "Error: No index found. Run 'cortexcode index' first."}],
234
+ "isError": True,
235
+ })
236
+
237
+ try:
238
+ if tool_name == "cortexcode_search":
239
+ result = self._tool_search(args)
240
+ elif tool_name == "cortexcode_context":
241
+ result = self._tool_context(args)
242
+ elif tool_name == "cortexcode_file_symbols":
243
+ result = self._tool_file_symbols(args)
244
+ elif tool_name == "cortexcode_call_graph":
245
+ result = self._tool_call_graph(args)
246
+ elif tool_name == "cortexcode_diff":
247
+ result = self._tool_diff(args)
248
+ elif tool_name == "cortexcode_stats":
249
+ result = self._tool_stats(args)
250
+ elif tool_name == "cortexcode_deadcode":
251
+ result = self._tool_deadcode(args)
252
+ elif tool_name == "cortexcode_complexity":
253
+ result = self._tool_complexity(args)
254
+ elif tool_name == "cortexcode_impact":
255
+ result = self._tool_impact(args)
256
+ elif tool_name == "cortexcode_file_deps":
257
+ result = self._tool_file_deps(args)
258
+ else:
259
+ return create_mcp_error(req_id, -32602, f"Unknown tool: {tool_name}")
260
+
261
+ return create_mcp_response(req_id, {
262
+ "content": [{"type": "text", "text": json.dumps(result, indent=2)}],
263
+ })
264
+ except Exception as e:
265
+ return create_mcp_response(req_id, {
266
+ "content": [{"type": "text", "text": f"Error: {str(e)}"}],
267
+ "isError": True,
268
+ })
269
+
270
+ def _tool_search(self, args: dict) -> list[dict]:
271
+ """Search symbols by name."""
272
+ query = args.get("query", "").lower()
273
+ sym_type = args.get("type")
274
+ limit = args.get("limit", 10)
275
+
276
+ files = self.index.get("files", {})
277
+ results = []
278
+
279
+ for rel_path, file_data in files.items():
280
+ if not isinstance(file_data, dict):
281
+ continue
282
+ for sym in file_data.get("symbols", []):
283
+ name = sym.get("name", "").lower()
284
+ if query in name:
285
+ if sym_type and sym.get("type") != sym_type:
286
+ continue
287
+ results.append({
288
+ "name": sym.get("name"),
289
+ "type": sym.get("type"),
290
+ "file": rel_path,
291
+ "line": sym.get("line"),
292
+ "params": sym.get("params", []),
293
+ "doc": sym.get("doc"),
294
+ })
295
+
296
+ return results[:limit]
297
+
298
+ def _tool_context(self, args: dict) -> dict:
299
+ """Get context for a symbol."""
300
+ from cortexcode.context import get_context
301
+ return get_context(self.index_path, args.get("query"), args.get("num_results", 5))
302
+
303
+ def _tool_file_symbols(self, args: dict) -> dict:
304
+ """List all symbols in a file."""
305
+ file_path = args.get("file_path", "")
306
+ files = self.index.get("files", {})
307
+
308
+ for rel_path, file_data in files.items():
309
+ if file_path.lower() in rel_path.lower():
310
+ if isinstance(file_data, dict):
311
+ return {
312
+ "file": rel_path,
313
+ "symbols": file_data.get("symbols", []),
314
+ "imports": file_data.get("imports", []),
315
+ "exports": file_data.get("exports", []),
316
+ }
317
+
318
+ return {"error": f"File not found: {file_path}"}
319
+
320
+ def _tool_call_graph(self, args: dict) -> dict:
321
+ """Get call graph for a symbol."""
322
+ symbol = args.get("symbol", "")
323
+ depth = args.get("depth", 1)
324
+ call_graph = self.index.get("call_graph", {})
325
+
326
+ calls = call_graph.get(symbol, [])
327
+ callers = [name for name, c in call_graph.items() if symbol in c]
328
+
329
+ result = {
330
+ "symbol": symbol,
331
+ "calls": calls,
332
+ "called_by": callers[:20],
333
+ }
334
+
335
+ # Depth > 1: also get calls of callees
336
+ if depth > 1 and calls:
337
+ result["transitive_calls"] = {}
338
+ for callee in calls[:10]:
339
+ sub_calls = call_graph.get(callee, [])
340
+ if sub_calls:
341
+ result["transitive_calls"][callee] = sub_calls
342
+
343
+ return result
344
+
345
+ def _tool_diff(self, args: dict) -> dict:
346
+ """Get diff context."""
347
+ from cortexcode.git_diff import get_diff_context
348
+ return get_diff_context(self.index_path, args.get("ref", "HEAD"))
349
+
350
+ def _tool_stats(self, args: dict) -> dict:
351
+ """Get project stats."""
352
+ files = self.index.get("files", {})
353
+ call_graph = self.index.get("call_graph", {})
354
+
355
+ total_symbols = 0
356
+ type_counts = {}
357
+ for file_data in files.values():
358
+ if isinstance(file_data, dict):
359
+ for sym in file_data.get("symbols", []):
360
+ total_symbols += 1
361
+ t = sym.get("type", "unknown")
362
+ type_counts[t] = type_counts.get(t, 0) + 1
363
+
364
+ non_empty_calls = sum(1 for v in call_graph.values() if v)
365
+
366
+ return {
367
+ "files": len(files),
368
+ "symbols": total_symbols,
369
+ "symbol_types": type_counts,
370
+ "languages": self.index.get("languages", []),
371
+ "call_graph_entries": len(call_graph),
372
+ "symbols_with_calls": non_empty_calls,
373
+ "file_dependencies": len(self.index.get("file_dependencies", {})),
374
+ "last_indexed": self.index.get("last_indexed"),
375
+ }
376
+
377
+ def _tool_deadcode(self, args: dict) -> dict:
378
+ """Find potentially dead code."""
379
+ from cortexcode.analysis import detect_dead_code
380
+
381
+ limit = args.get("limit", 20)
382
+ dead = detect_dead_code(self.index)
383
+
384
+ return {
385
+ "count": len(dead),
386
+ "dead_code": dead[:limit]
387
+ }
388
+
389
+ def _tool_complexity(self, args: dict) -> dict:
390
+ """Find most complex functions."""
391
+ from cortexcode.analysis import compute_complexity
392
+
393
+ limit = args.get("limit", 20)
394
+ complex_funcs = compute_complexity(self.index, str(self.index_path.parent.parent))
395
+
396
+ return {
397
+ "count": len(complex_funcs),
398
+ "complex_functions": complex_funcs[:limit]
399
+ }
400
+
401
+ def _tool_impact(self, args: dict) -> dict:
402
+ """Analyze change impact of a symbol."""
403
+ from cortexcode.analysis import analyze_change_impact
404
+
405
+ symbol = args.get("symbol", "")
406
+ if not symbol:
407
+ return {"error": "symbol is required"}
408
+
409
+ impact = analyze_change_impact(self.index, symbol)
410
+ return impact
411
+
412
+ def _tool_file_deps(self, args: dict) -> dict:
413
+ """Get file dependencies."""
414
+ file_deps = self.index.get("file_dependencies", {})
415
+ file_path = args.get("file_path", "")
416
+
417
+ if file_path:
418
+ for rel_path, deps in file_deps.items():
419
+ if file_path.lower() in rel_path.lower():
420
+ # Also find reverse deps (who imports this file)
421
+ imported_by = [
422
+ p for p, d in file_deps.items()
423
+ if any(file_path.lower() in dep.lower() for dep in d)
424
+ ]
425
+ return {
426
+ "file": rel_path,
427
+ "imports_from": deps,
428
+ "imported_by": imported_by,
429
+ }
430
+ return {"error": f"No dependencies found for: {file_path}"}
431
+
432
+ return {"file_dependencies": file_deps}
433
+
434
+
435
+ def run_stdio_server(index_path: Path | None = None):
436
+ """Run MCP server on stdin/stdout (standard MCP transport)."""
437
+ server = CortexCodeMCPServer(index_path)
438
+
439
+ for line in sys.stdin:
440
+ line = line.strip()
441
+ if not line:
442
+ continue
443
+
444
+ try:
445
+ request = json.loads(line)
446
+ except json.JSONDecodeError:
447
+ response = create_mcp_error(None, -32700, "Parse error")
448
+ sys.stdout.write(json.dumps(response) + "\n")
449
+ sys.stdout.flush()
450
+ continue
451
+
452
+ response = server.handle_request(request)
453
+ if response is not None:
454
+ sys.stdout.write(json.dumps(response) + "\n")
455
+ sys.stdout.flush()
cortexcode/plugins.py ADDED
@@ -0,0 +1,188 @@
1
+ """Plugin system for framework-specific extractors.
2
+
3
+ Plugins can register custom symbol extractors, framework detectors,
4
+ and post-processors that run during indexing.
5
+
6
+ Usage:
7
+ # Create a plugin
8
+ class MyPlugin(CortexPlugin):
9
+ name = "my-framework"
10
+ extensions = [".myext"]
11
+
12
+ def extract_symbols(self, source, rel_path):
13
+ return [{"name": "foo", "type": "function", "line": 1}]
14
+
15
+ def detect_framework(self, name, source_str):
16
+ if "MyFramework" in source_str:
17
+ return "my-framework"
18
+ return None
19
+
20
+ # Register it
21
+ plugin_registry.register(MyPlugin())
22
+ """
23
+
24
+ import importlib
25
+ import json
26
+ from pathlib import Path
27
+ from typing import Any, Protocol
28
+
29
+
30
+ class CortexPlugin(Protocol):
31
+ """Protocol for CortexCode plugins."""
32
+
33
+ name: str
34
+ extensions: list[str]
35
+
36
+ def extract_symbols(self, source: str, rel_path: str) -> list[dict[str, Any]]:
37
+ """Extract symbols from source code. Return list of symbol dicts."""
38
+ ...
39
+
40
+ def detect_framework(self, name: str, source_str: str) -> str | None:
41
+ """Detect framework from symbol name and source. Return framework string or None."""
42
+ ...
43
+
44
+ def extract_imports(self, source: str) -> list[dict[str, Any]]:
45
+ """Extract imports from source code. Return list of import dicts."""
46
+ ...
47
+
48
+ def post_process(self, index: dict[str, Any]) -> dict[str, Any]:
49
+ """Post-process the full index after all files are indexed."""
50
+ ...
51
+
52
+
53
+ class BasePlugin:
54
+ """Base class for plugins with default no-op implementations."""
55
+
56
+ name: str = "base"
57
+ extensions: list[str] = []
58
+
59
+ def extract_symbols(self, source: str, rel_path: str) -> list[dict[str, Any]]:
60
+ return []
61
+
62
+ def detect_framework(self, name: str, source_str: str) -> str | None:
63
+ return None
64
+
65
+ def extract_imports(self, source: str) -> list[dict[str, Any]]:
66
+ return []
67
+
68
+ def post_process(self, index: dict[str, Any]) -> dict[str, Any]:
69
+ return index
70
+
71
+
72
+ class PluginRegistry:
73
+ """Central registry for all CortexCode plugins."""
74
+
75
+ def __init__(self):
76
+ self._plugins: dict[str, BasePlugin] = {}
77
+ self._ext_map: dict[str, str] = {} # extension -> plugin name
78
+
79
+ def register(self, plugin: BasePlugin) -> None:
80
+ """Register a plugin."""
81
+ self._plugins[plugin.name] = plugin
82
+ for ext in plugin.extensions:
83
+ self._ext_map[ext] = plugin.name
84
+
85
+ def unregister(self, name: str) -> bool:
86
+ """Unregister a plugin by name."""
87
+ if name in self._plugins:
88
+ plugin = self._plugins.pop(name)
89
+ for ext in plugin.extensions:
90
+ if self._ext_map.get(ext) == name:
91
+ del self._ext_map[ext]
92
+ return True
93
+ return False
94
+
95
+ def get_plugin(self, name: str) -> BasePlugin | None:
96
+ """Get a plugin by name."""
97
+ return self._plugins.get(name)
98
+
99
+ def get_plugin_for_ext(self, ext: str) -> BasePlugin | None:
100
+ """Get the plugin registered for a file extension."""
101
+ name = self._ext_map.get(ext)
102
+ return self._plugins.get(name) if name else None
103
+
104
+ def list_plugins(self) -> list[dict[str, Any]]:
105
+ """List all registered plugins."""
106
+ return [
107
+ {
108
+ "name": p.name,
109
+ "extensions": p.extensions,
110
+ }
111
+ for p in self._plugins.values()
112
+ ]
113
+
114
+ @property
115
+ def registered_extensions(self) -> set[str]:
116
+ """All file extensions handled by plugins."""
117
+ return set(self._ext_map.keys())
118
+
119
+ def extract_symbols(self, source: str, ext: str, rel_path: str) -> list[dict[str, Any]] | None:
120
+ """Try to extract symbols using a registered plugin. Returns None if no plugin handles this ext."""
121
+ plugin = self.get_plugin_for_ext(ext)
122
+ if plugin:
123
+ return plugin.extract_symbols(source, rel_path)
124
+ return None
125
+
126
+ def detect_framework(self, name: str, source_str: str) -> str | None:
127
+ """Run all plugins' framework detection. First match wins."""
128
+ for plugin in self._plugins.values():
129
+ result = plugin.detect_framework(name, source_str)
130
+ if result:
131
+ return result
132
+ return None
133
+
134
+ def extract_imports(self, source: str, ext: str) -> list[dict[str, Any]] | None:
135
+ """Try to extract imports using a registered plugin."""
136
+ plugin = self.get_plugin_for_ext(ext)
137
+ if plugin:
138
+ return plugin.extract_imports(source)
139
+ return None
140
+
141
+ def run_post_processors(self, index: dict[str, Any]) -> dict[str, Any]:
142
+ """Run all plugins' post-processors on the index."""
143
+ for plugin in self._plugins.values():
144
+ index = plugin.post_process(index)
145
+ return index
146
+
147
+ def load_from_config(self, config_path: Path) -> int:
148
+ """Load plugins from a cortexcode config file.
149
+
150
+ Config format (.cortexcode/plugins.json):
151
+ {
152
+ "plugins": [
153
+ {"module": "my_package.cortex_plugin", "class": "MyPlugin"},
154
+ {"module": "another_plugin", "class": "AnotherPlugin"}
155
+ ]
156
+ }
157
+
158
+ Returns number of plugins loaded.
159
+ """
160
+ if not config_path.exists():
161
+ return 0
162
+
163
+ try:
164
+ data = json.loads(config_path.read_text(encoding="utf-8"))
165
+ except (json.JSONDecodeError, OSError):
166
+ return 0
167
+
168
+ count = 0
169
+ for entry in data.get("plugins", []):
170
+ module_name = entry.get("module")
171
+ class_name = entry.get("class")
172
+ if not module_name or not class_name:
173
+ continue
174
+
175
+ try:
176
+ mod = importlib.import_module(module_name)
177
+ cls = getattr(mod, class_name)
178
+ plugin = cls()
179
+ self.register(plugin)
180
+ count += 1
181
+ except (ImportError, AttributeError, TypeError) as e:
182
+ print(f"Failed to load plugin {module_name}.{class_name}: {e}")
183
+
184
+ return count
185
+
186
+
187
+ # Global plugin registry
188
+ plugin_registry = PluginRegistry()