mne-docs-mcp 1.0.0

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 (122) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +140 -0
  3. package/dist/cache/manager.d.ts +41 -0
  4. package/dist/cache/manager.d.ts.map +1 -0
  5. package/dist/cache/manager.js +123 -0
  6. package/dist/cache/manager.js.map +1 -0
  7. package/dist/config.d.ts +75 -0
  8. package/dist/config.d.ts.map +1 -0
  9. package/dist/config.js +120 -0
  10. package/dist/config.js.map +1 -0
  11. package/dist/forum/client.d.ts +9 -0
  12. package/dist/forum/client.d.ts.map +1 -0
  13. package/dist/forum/client.js +48 -0
  14. package/dist/forum/client.js.map +1 -0
  15. package/dist/github/client.d.ts +33 -0
  16. package/dist/github/client.d.ts.map +1 -0
  17. package/dist/github/client.js +198 -0
  18. package/dist/github/client.js.map +1 -0
  19. package/dist/github/types.d.ts +238 -0
  20. package/dist/github/types.d.ts.map +1 -0
  21. package/dist/github/types.js +2 -0
  22. package/dist/github/types.js.map +1 -0
  23. package/dist/index.d.ts +2 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +96 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/parser/bridge.d.ts +46 -0
  28. package/dist/parser/bridge.d.ts.map +1 -0
  29. package/dist/parser/bridge.js +225 -0
  30. package/dist/parser/bridge.js.map +1 -0
  31. package/dist/parser/types.d.ts +36 -0
  32. package/dist/parser/types.d.ts.map +1 -0
  33. package/dist/parser/types.js +2 -0
  34. package/dist/parser/types.js.map +1 -0
  35. package/dist/server.d.ts +4 -0
  36. package/dist/server.d.ts.map +1 -0
  37. package/dist/server.js +218 -0
  38. package/dist/server.js.map +1 -0
  39. package/dist/tools/findMneSymbol.d.ts +22 -0
  40. package/dist/tools/findMneSymbol.d.ts.map +1 -0
  41. package/dist/tools/findMneSymbol.js +74 -0
  42. package/dist/tools/findMneSymbol.js.map +1 -0
  43. package/dist/tools/getMneChangelog.d.ts +31 -0
  44. package/dist/tools/getMneChangelog.d.ts.map +1 -0
  45. package/dist/tools/getMneChangelog.js +233 -0
  46. package/dist/tools/getMneChangelog.js.map +1 -0
  47. package/dist/tools/getMneDoc.d.ts +19 -0
  48. package/dist/tools/getMneDoc.d.ts.map +1 -0
  49. package/dist/tools/getMneDoc.js +248 -0
  50. package/dist/tools/getMneDoc.js.map +1 -0
  51. package/dist/tools/getMneExample.d.ts +31 -0
  52. package/dist/tools/getMneExample.d.ts.map +1 -0
  53. package/dist/tools/getMneExample.js +212 -0
  54. package/dist/tools/getMneExample.js.map +1 -0
  55. package/dist/tools/getMneFile.d.ts +3 -0
  56. package/dist/tools/getMneFile.d.ts.map +1 -0
  57. package/dist/tools/getMneFile.js +60 -0
  58. package/dist/tools/getMneFile.js.map +1 -0
  59. package/dist/tools/getMneForumTopic.d.ts +33 -0
  60. package/dist/tools/getMneForumTopic.d.ts.map +1 -0
  61. package/dist/tools/getMneForumTopic.js +136 -0
  62. package/dist/tools/getMneForumTopic.js.map +1 -0
  63. package/dist/tools/getMneIssue.d.ts +7 -0
  64. package/dist/tools/getMneIssue.d.ts.map +1 -0
  65. package/dist/tools/getMneIssue.js +22 -0
  66. package/dist/tools/getMneIssue.js.map +1 -0
  67. package/dist/tools/getMneIssueComments.d.ts +10 -0
  68. package/dist/tools/getMneIssueComments.d.ts.map +1 -0
  69. package/dist/tools/getMneIssueComments.js +26 -0
  70. package/dist/tools/getMneIssueComments.js.map +1 -0
  71. package/dist/tools/getRelatedSymbols.d.ts +26 -0
  72. package/dist/tools/getRelatedSymbols.d.ts.map +1 -0
  73. package/dist/tools/getRelatedSymbols.js +120 -0
  74. package/dist/tools/getRelatedSymbols.js.map +1 -0
  75. package/dist/tools/getSymbolReferences.d.ts +11 -0
  76. package/dist/tools/getSymbolReferences.d.ts.map +1 -0
  77. package/dist/tools/getSymbolReferences.js +251 -0
  78. package/dist/tools/getSymbolReferences.js.map +1 -0
  79. package/dist/tools/listMneVersions.d.ts +8 -0
  80. package/dist/tools/listMneVersions.d.ts.map +1 -0
  81. package/dist/tools/listMneVersions.js +32 -0
  82. package/dist/tools/listMneVersions.js.map +1 -0
  83. package/dist/tools/lookupMneError.d.ts +59 -0
  84. package/dist/tools/lookupMneError.d.ts.map +1 -0
  85. package/dist/tools/lookupMneError.js +280 -0
  86. package/dist/tools/lookupMneError.js.map +1 -0
  87. package/dist/tools/searchMneDocs.d.ts +3 -0
  88. package/dist/tools/searchMneDocs.d.ts.map +1 -0
  89. package/dist/tools/searchMneDocs.js +58 -0
  90. package/dist/tools/searchMneDocs.js.map +1 -0
  91. package/dist/tools/searchMneForum.d.ts +9 -0
  92. package/dist/tools/searchMneForum.d.ts.map +1 -0
  93. package/dist/tools/searchMneForum.js +29 -0
  94. package/dist/tools/searchMneForum.js.map +1 -0
  95. package/dist/tools/searchMneIssues.d.ts +8 -0
  96. package/dist/tools/searchMneIssues.d.ts.map +1 -0
  97. package/dist/tools/searchMneIssues.js +23 -0
  98. package/dist/tools/searchMneIssues.js.map +1 -0
  99. package/dist/types.d.ts +140 -0
  100. package/dist/types.d.ts.map +1 -0
  101. package/dist/types.js +35 -0
  102. package/dist/types.js.map +1 -0
  103. package/dist/utils/docstring.d.ts +26 -0
  104. package/dist/utils/docstring.d.ts.map +1 -0
  105. package/dist/utils/docstring.js +261 -0
  106. package/dist/utils/docstring.js.map +1 -0
  107. package/dist/utils/logger.d.ts +19 -0
  108. package/dist/utils/logger.d.ts.map +1 -0
  109. package/dist/utils/logger.js +73 -0
  110. package/dist/utils/logger.js.map +1 -0
  111. package/dist/utils/metrics.d.ts +38 -0
  112. package/dist/utils/metrics.d.ts.map +1 -0
  113. package/dist/utils/metrics.js +57 -0
  114. package/dist/utils/metrics.js.map +1 -0
  115. package/dist/version.d.ts +2 -0
  116. package/dist/version.d.ts.map +1 -0
  117. package/dist/version.js +18 -0
  118. package/dist/version.js.map +1 -0
  119. package/package.json +71 -0
  120. package/python/ast_extractor.py +422 -0
  121. package/python/parser.py +68 -0
  122. package/python/requirements.txt +2 -0
@@ -0,0 +1,422 @@
1
+ import ast
2
+ from typing import Optional, List, Dict, Any
3
+
4
+ def extract_docstring_params(docstring: str) -> List[Dict[str, str]]:
5
+ """Parse numpy-style docstring to extract parameter info."""
6
+ params = []
7
+ if not docstring:
8
+ return params
9
+
10
+ lines = docstring.split('\n')
11
+ in_params = False
12
+ current_param = None
13
+
14
+ for line in lines:
15
+ line = line.strip()
16
+ if line == 'Parameters':
17
+ in_params = True
18
+ continue
19
+ if line.startswith('----------') or line == 'Returns':
20
+ if in_params:
21
+ break
22
+
23
+ if in_params and line:
24
+ # Check for parameter definition "name : type"
25
+ if ' : ' in line:
26
+ parts = line.split(' : ')
27
+ name = parts[0].strip()
28
+ type_info = parts[1].strip()
29
+ current_param = {'name': name, 'type': type_info, 'description': ''}
30
+ params.append(current_param)
31
+ elif current_param:
32
+ # Append description
33
+ if current_param['description']:
34
+ current_param['description'] += ' ' + line
35
+ else:
36
+ current_param['description'] = line
37
+
38
+ return params
39
+
40
+
41
+ def extract_docstring_examples(docstring: str) -> List[str]:
42
+ """Extract code examples from numpy-style docstring."""
43
+ examples = []
44
+ if not docstring:
45
+ return examples
46
+
47
+ lines = docstring.split('\n')
48
+ in_examples = False
49
+ skip_separator = False
50
+ current_example = []
51
+
52
+ for line in lines:
53
+ stripped = line.strip()
54
+
55
+ # Check for Examples section
56
+ if stripped == 'Examples':
57
+ in_examples = True
58
+ skip_separator = True # Skip the next line (-------)
59
+ continue
60
+
61
+ # Skip the separator line after section header
62
+ if skip_separator:
63
+ skip_separator = False
64
+ if stripped.startswith('---'):
65
+ continue
66
+
67
+ # Check for end of Examples section (next section header)
68
+ if in_examples and stripped in ('See Also', 'Notes', 'References', 'Attributes', 'Methods', 'Returns', 'Parameters'):
69
+ break
70
+
71
+ if in_examples:
72
+ # Detect start of code block with >>>
73
+ if stripped.startswith('>>>'):
74
+ if current_example:
75
+ examples.append('\n'.join(current_example))
76
+ current_example = [stripped[4:] if stripped.startswith('>>> ') else stripped[3:]]
77
+ elif stripped.startswith('...'):
78
+ # Continuation line
79
+ if current_example:
80
+ current_example.append(stripped[4:] if stripped.startswith('... ') else stripped[3:])
81
+ elif current_example and stripped and not stripped.startswith('---'):
82
+ # Output line - include it as comment
83
+ current_example.append('# Output: ' + stripped)
84
+
85
+ # Don't forget the last example
86
+ if current_example:
87
+ examples.append('\n'.join(current_example))
88
+
89
+ return examples[:5] # Limit to 5 examples
90
+
91
+ def get_decorators(node: ast.AST) -> List[str]:
92
+ decorators = []
93
+ if hasattr(node, 'decorator_list'):
94
+ for decorator in node.decorator_list:
95
+ if isinstance(decorator, ast.Name):
96
+ decorators.append(decorator.id)
97
+ elif isinstance(decorator, ast.Call):
98
+ if isinstance(decorator.func, ast.Name):
99
+ decorators.append(decorator.func.id)
100
+ elif isinstance(decorator.func, ast.Attribute):
101
+ decorators.append(decorator.func.attr)
102
+ return decorators
103
+
104
+ def get_signature(node: ast.FunctionDef) -> str:
105
+ args = []
106
+ # Handle positional args
107
+ for arg in node.args.args:
108
+ arg_str = arg.arg
109
+ if arg.annotation:
110
+ if isinstance(arg.annotation, ast.Name):
111
+ arg_str += f": {arg.annotation.id}"
112
+ elif isinstance(arg.annotation, ast.Constant):
113
+ arg_str += f": {arg.annotation.value}"
114
+ args.append(arg_str)
115
+
116
+ # Handle defaults (naive approach, matching from end)
117
+ if node.args.defaults:
118
+ for i, default in enumerate(reversed(node.args.defaults)):
119
+ if isinstance(default, ast.Constant):
120
+ args[-(i+1)] += f"={default.value}"
121
+
122
+ return f"({', '.join(args)})"
123
+
124
+ class SymbolVisitor(ast.NodeVisitor):
125
+ def __init__(self, module_name: str = "", file_url: str = ""):
126
+ self.symbols = []
127
+ self.module_name = module_name
128
+ self.file_url = file_url
129
+ self.current_class = None
130
+
131
+ def visit_ClassDef(self, node):
132
+ qualified_name = f"{self.module_name}.{node.name}" if self.module_name else node.name
133
+
134
+ symbol = {
135
+ 'name': node.name,
136
+ 'qualifiedName': qualified_name,
137
+ 'kind': 'class',
138
+ 'signature': f"class {node.name}",
139
+ 'docstring': ast.get_docstring(node) or "",
140
+ 'lineno': node.lineno,
141
+ 'endLineno': node.end_lineno,
142
+ 'decorators': get_decorators(node),
143
+ 'baseClasses': [b.id for b in node.bases if isinstance(b, ast.Name)],
144
+ 'parameters': extract_docstring_params(ast.get_docstring(node) or "")
145
+ }
146
+ self.symbols.append(symbol)
147
+
148
+ old_class = self.current_class
149
+ self.current_class = qualified_name
150
+ self.generic_visit(node)
151
+ self.current_class = old_class
152
+
153
+ def visit_FunctionDef(self, node):
154
+ is_method = self.current_class is not None
155
+ name = node.name
156
+ qualified_name = f"{self.current_class}.{name}" if is_method else (f"{self.module_name}.{name}" if self.module_name else name)
157
+
158
+ symbol = {
159
+ 'name': name,
160
+ 'qualifiedName': qualified_name,
161
+ 'kind': 'method' if is_method else 'function',
162
+ 'signature': f"def {name}{get_signature(node)}",
163
+ 'docstring': ast.get_docstring(node) or "",
164
+ 'lineno': node.lineno,
165
+ 'endLineno': node.end_lineno,
166
+ 'decorators': get_decorators(node),
167
+ 'parameters': extract_docstring_params(ast.get_docstring(node) or "")
168
+ }
169
+ self.symbols.append(symbol)
170
+ self.generic_visit(node)
171
+
172
+ def parse_file(source: str, module_name: str = "", file_url: str = "") -> List[Dict[str, Any]]:
173
+ try:
174
+ tree = ast.parse(source)
175
+ visitor = SymbolVisitor(module_name, file_url)
176
+ visitor.visit(tree)
177
+ return visitor.symbols
178
+ except Exception:
179
+ return []
180
+
181
+ def extract_symbol(source: str, symbol_name: str, module_name: str = "", file_url: str = "") -> Optional[Dict[str, Any]]:
182
+ symbols = parse_file(source, module_name, file_url)
183
+ result = None
184
+
185
+ # Try exact match first
186
+ for s in symbols:
187
+ if s['name'] == symbol_name or s['qualifiedName'] == symbol_name:
188
+ result = s
189
+ break
190
+
191
+ # Try partial match for methods (e.g. "Epochs.plot" matching "plot" inside "Epochs")
192
+ if result is None and '.' in symbol_name:
193
+ parts = symbol_name.split('.')
194
+ short_name = parts[-1]
195
+ for s in symbols:
196
+ if s['name'] == short_name and (s['qualifiedName'].endswith(symbol_name) or symbol_name.endswith(s['qualifiedName'])):
197
+ result = s
198
+ break
199
+
200
+ if result:
201
+ # Extract examples from docstring
202
+ examples = extract_docstring_examples(result.get('docstring', ''))
203
+ if examples:
204
+ result['examples'] = examples
205
+
206
+ # If we found a class, also include its methods with full details
207
+ if result['kind'] == 'class':
208
+ class_qualified_name = result['qualifiedName']
209
+ methods = []
210
+ for s in symbols:
211
+ if s['kind'] == 'method' and s['qualifiedName'].startswith(class_qualified_name + '.'):
212
+ # Include __init__ and public methods
213
+ if not s['name'].startswith('_') or s['name'] == '__init__':
214
+ # Get a meaningful docstring summary (first line or first sentence)
215
+ docstring = s.get('docstring', '')
216
+ if docstring:
217
+ # Get first non-empty line as summary
218
+ lines = [l.strip() for l in docstring.split('\n') if l.strip()]
219
+ summary = lines[0] if lines else ''
220
+ # Truncate if too long
221
+ if len(summary) > 200:
222
+ summary = summary[:197] + '...'
223
+ else:
224
+ summary = ''
225
+
226
+ methods.append({
227
+ 'name': s['name'],
228
+ 'signature': s['signature'],
229
+ 'docstring': summary,
230
+ 'lineno': s.get('lineno'),
231
+ 'parameters': s.get('parameters', [])[:5], # First 5 params
232
+ })
233
+
234
+ # Sort methods: __init__ first, then alphabetically
235
+ methods.sort(key=lambda m: (0 if m['name'] == '__init__' else 1, m['name']))
236
+ result['methods'] = methods[:30] # Limit to 30 methods
237
+
238
+ return result
239
+
240
+ def find_symbols(source: str, query: str, module_name: str = "", max_results: int = 20) -> List[Dict[str, Any]]:
241
+ symbols = parse_file(source, module_name)
242
+ query = query.lower()
243
+ matches = []
244
+
245
+ for s in symbols:
246
+ if query in s['name'].lower() or query in s['qualifiedName'].lower():
247
+ matches.append(s)
248
+
249
+ return matches[:max_results]
250
+
251
+
252
+ class CrossReferenceVisitor(ast.NodeVisitor):
253
+ """Extract function calls and attribute accesses from source code."""
254
+
255
+ def __init__(self, module_name: str = ""):
256
+ self.module_name = module_name
257
+ self.imports = {} # alias -> full module path
258
+ self.from_imports = {} # name -> (module, original_name)
259
+ self.calls = [] # List of function/method calls
260
+ self.current_function = None
261
+ self.current_class = None
262
+
263
+ def visit_Import(self, node):
264
+ for alias in node.names:
265
+ name = alias.asname or alias.name
266
+ self.imports[name] = alias.name
267
+ self.generic_visit(node)
268
+
269
+ def visit_ImportFrom(self, node):
270
+ module = node.module or ''
271
+ for alias in node.names:
272
+ name = alias.asname or alias.name
273
+ self.from_imports[name] = (module, alias.name)
274
+ self.generic_visit(node)
275
+
276
+ def visit_ClassDef(self, node):
277
+ old_class = self.current_class
278
+ self.current_class = node.name
279
+ self.generic_visit(node)
280
+ self.current_class = old_class
281
+
282
+ def visit_FunctionDef(self, node):
283
+ old_func = self.current_function
284
+ if self.current_class:
285
+ self.current_function = f"{self.current_class}.{node.name}"
286
+ else:
287
+ self.current_function = node.name
288
+ self.generic_visit(node)
289
+ self.current_function = old_func
290
+
291
+ visit_AsyncFunctionDef = visit_FunctionDef
292
+
293
+ def visit_Call(self, node):
294
+ call_info = self._resolve_call(node)
295
+ if call_info:
296
+ call_info['caller'] = self.current_function
297
+ call_info['lineno'] = node.lineno
298
+ self.calls.append(call_info)
299
+ self.generic_visit(node)
300
+
301
+ def _resolve_call(self, node) -> Optional[Dict[str, Any]]:
302
+ """Resolve a Call node to a callee name."""
303
+ func = node.func
304
+
305
+ if isinstance(func, ast.Name):
306
+ # Direct function call: func()
307
+ name = func.id
308
+ if name in self.from_imports:
309
+ module, orig_name = self.from_imports[name]
310
+ return {'callee': f"{module}.{orig_name}", 'kind': 'function'}
311
+ return {'callee': name, 'kind': 'function'}
312
+
313
+ elif isinstance(func, ast.Attribute):
314
+ # Method/attribute call: obj.method()
315
+ parts = []
316
+ current = func
317
+ while isinstance(current, ast.Attribute):
318
+ parts.append(current.attr)
319
+ current = current.value
320
+
321
+ if isinstance(current, ast.Name):
322
+ parts.append(current.id)
323
+ parts.reverse()
324
+
325
+ # Check if first part is an import alias
326
+ first = parts[0]
327
+ if first in self.imports:
328
+ parts[0] = self.imports[first]
329
+ elif first in self.from_imports:
330
+ module, orig_name = self.from_imports[first]
331
+ parts[0] = f"{module}.{orig_name}"
332
+
333
+ return {'callee': '.'.join(parts), 'kind': 'method'}
334
+
335
+ return None
336
+
337
+
338
+ def extract_cross_references(source: str, symbol_name: str, module_name: str = "") -> Dict[str, Any]:
339
+ """
340
+ Extract cross-references for a symbol: what it calls (callees) and what calls it (callers).
341
+
342
+ symbol_name can be:
343
+ - "function_name" for module-level functions
344
+ - "ClassName.method_name" for class methods
345
+ - "ClassName" for all methods in a class
346
+ """
347
+ try:
348
+ tree = ast.parse(source)
349
+ except SyntaxError:
350
+ return {'symbol': symbol_name, 'callees': [], 'callers': [], 'error': 'Syntax error in source'}
351
+
352
+ visitor = CrossReferenceVisitor(module_name)
353
+ visitor.visit(tree)
354
+
355
+ # Parse symbol_name to handle Class.method format
356
+ class_context = None
357
+ method_name = symbol_name
358
+ if '.' in symbol_name:
359
+ parts = symbol_name.split('.')
360
+ # Check if first part looks like a class (PascalCase)
361
+ if parts[0] and parts[0][0].isupper():
362
+ class_context = parts[0]
363
+ method_name = parts[1] if len(parts) > 1 else None
364
+
365
+ # Find callees (what the symbol calls)
366
+ callees = []
367
+ # Find callers (what calls the symbol)
368
+ callers = []
369
+
370
+ for call in visitor.calls:
371
+ caller = call.get('caller')
372
+ callee = call.get('callee', '')
373
+
374
+ # Check if this call is FROM our symbol (symbol is the caller)
375
+ caller_matches = False
376
+ if caller:
377
+ if class_context and method_name:
378
+ # Looking for ClassName.method_name
379
+ caller_matches = caller == f"{class_context}.{method_name}"
380
+ elif class_context and not method_name:
381
+ # Looking for any method in ClassName
382
+ caller_matches = caller.startswith(f"{class_context}.")
383
+ else:
384
+ # Looking for a function name
385
+ caller_matches = (caller == symbol_name or
386
+ caller.endswith(f".{symbol_name}"))
387
+
388
+ if caller_matches:
389
+ callees.append({
390
+ 'name': callee,
391
+ 'kind': call.get('kind'),
392
+ 'lineno': call.get('lineno')
393
+ })
394
+
395
+ # Check if this call is TO our symbol (symbol is the callee)
396
+ callee_matches = (callee == symbol_name or
397
+ callee.endswith(f".{symbol_name}") or
398
+ (method_name and callee.endswith(f".{method_name}")))
399
+
400
+ if callee_matches:
401
+ callers.append({
402
+ 'name': caller or '<module>',
403
+ 'kind': 'function' if caller else 'module',
404
+ 'lineno': call.get('lineno')
405
+ })
406
+
407
+ # Deduplicate callees by name
408
+ seen_callees = set()
409
+ unique_callees = []
410
+ for c in callees:
411
+ if c['name'] not in seen_callees:
412
+ seen_callees.add(c['name'])
413
+ unique_callees.append(c)
414
+
415
+ return {
416
+ 'symbol': symbol_name,
417
+ 'module': module_name,
418
+ 'callees': unique_callees[:50], # Limit results
419
+ 'callers': callers[:50],
420
+ 'totalCallees': len(unique_callees),
421
+ 'totalCallers': len(callers)
422
+ }
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Python AST parser for MNE documentation extraction.
4
+ Communicates with TypeScript via stdin/stdout JSON.
5
+ """
6
+
7
+ import ast
8
+ import json
9
+ import sys
10
+ import traceback
11
+ from typing import Optional, List, Dict, Any
12
+ from ast_extractor import extract_symbol, find_symbols, parse_file, extract_cross_references
13
+
14
+ def main():
15
+ """Main entry point - read command from stdin, write result to stdout."""
16
+ # Ensure stdout is using utf-8
17
+ sys.stdout.reconfigure(encoding='utf-8')
18
+ sys.stdin.reconfigure(encoding='utf-8')
19
+
20
+ try:
21
+ # Read all input from stdin
22
+ input_data = sys.stdin.read()
23
+ if not input_data:
24
+ return
25
+
26
+ command = json.loads(input_data)
27
+ action = command.get('action')
28
+
29
+ result = None
30
+
31
+ if action == 'parse_file':
32
+ result = parse_file(
33
+ command['source'],
34
+ command.get('moduleName', ''),
35
+ command.get('fileUrl', '')
36
+ )
37
+ elif action == 'extract_symbol':
38
+ result = extract_symbol(
39
+ command['source'],
40
+ command['symbolName'],
41
+ command.get('moduleName', ''),
42
+ command.get('fileUrl', '')
43
+ )
44
+ elif action == 'find_symbols':
45
+ result = find_symbols(
46
+ command['source'],
47
+ command['query'],
48
+ command.get('moduleName', ''),
49
+ command.get('maxResults', 20)
50
+ )
51
+ elif action == 'extract_cross_references':
52
+ result = extract_cross_references(
53
+ command['source'],
54
+ command['symbolName'],
55
+ command.get('moduleName', '')
56
+ )
57
+ else:
58
+ result = {'error': f'Unknown action: {action}'}
59
+
60
+ json.dump({'success': True, 'data': result}, sys.stdout)
61
+
62
+ except Exception as e:
63
+ # Capture full traceback for debugging
64
+ tb = traceback.format_exc()
65
+ json.dump({'success': False, 'error': str(e), 'traceback': tb}, sys.stdout)
66
+
67
+ if __name__ == '__main__':
68
+ main()
@@ -0,0 +1,2 @@
1
+ # No external dependencies needed for AST parsing
2
+ # Standard library only: ast, json, sys, typing