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.
- cortexcode/__init__.py +3 -0
- cortexcode/analysis.py +331 -0
- cortexcode/cli.py +845 -0
- cortexcode/context.py +298 -0
- cortexcode/dashboard.py +152 -0
- cortexcode/docs.py +1266 -0
- cortexcode/git_diff.py +157 -0
- cortexcode/indexer.py +1860 -0
- cortexcode/lsp_server.py +315 -0
- cortexcode/mcp_server.py +455 -0
- cortexcode/plugins.py +188 -0
- cortexcode/semantic_search.py +237 -0
- cortexcode/vuln_scan.py +241 -0
- cortexcode/watcher.py +122 -0
- cortexcode/workspace.py +180 -0
- cortexcode-0.1.0.dist-info/METADATA +448 -0
- cortexcode-0.1.0.dist-info/RECORD +21 -0
- cortexcode-0.1.0.dist-info/WHEEL +5 -0
- cortexcode-0.1.0.dist-info/entry_points.txt +2 -0
- cortexcode-0.1.0.dist-info/licenses/LICENSE +21 -0
- cortexcode-0.1.0.dist-info/top_level.txt +1 -0
cortexcode/mcp_server.py
ADDED
|
@@ -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()
|