cicada-mcp 0.1.4__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.

Potentially problematic release.


This version of cicada-mcp might be problematic. Click here for more details.

Files changed (48) hide show
  1. cicada/__init__.py +30 -0
  2. cicada/clean.py +297 -0
  3. cicada/command_logger.py +293 -0
  4. cicada/dead_code_analyzer.py +282 -0
  5. cicada/extractors/__init__.py +36 -0
  6. cicada/extractors/base.py +66 -0
  7. cicada/extractors/call.py +176 -0
  8. cicada/extractors/dependency.py +361 -0
  9. cicada/extractors/doc.py +179 -0
  10. cicada/extractors/function.py +246 -0
  11. cicada/extractors/module.py +123 -0
  12. cicada/extractors/spec.py +151 -0
  13. cicada/find_dead_code.py +270 -0
  14. cicada/formatter.py +918 -0
  15. cicada/git_helper.py +646 -0
  16. cicada/indexer.py +629 -0
  17. cicada/install.py +724 -0
  18. cicada/keyword_extractor.py +364 -0
  19. cicada/keyword_search.py +553 -0
  20. cicada/lightweight_keyword_extractor.py +298 -0
  21. cicada/mcp_server.py +1559 -0
  22. cicada/mcp_tools.py +291 -0
  23. cicada/parser.py +124 -0
  24. cicada/pr_finder.py +435 -0
  25. cicada/pr_indexer/__init__.py +20 -0
  26. cicada/pr_indexer/cli.py +62 -0
  27. cicada/pr_indexer/github_api_client.py +431 -0
  28. cicada/pr_indexer/indexer.py +297 -0
  29. cicada/pr_indexer/line_mapper.py +209 -0
  30. cicada/pr_indexer/pr_index_builder.py +253 -0
  31. cicada/setup.py +339 -0
  32. cicada/utils/__init__.py +52 -0
  33. cicada/utils/call_site_formatter.py +95 -0
  34. cicada/utils/function_grouper.py +57 -0
  35. cicada/utils/hash_utils.py +173 -0
  36. cicada/utils/index_utils.py +290 -0
  37. cicada/utils/path_utils.py +240 -0
  38. cicada/utils/signature_builder.py +106 -0
  39. cicada/utils/storage.py +111 -0
  40. cicada/utils/subprocess_runner.py +182 -0
  41. cicada/utils/text_utils.py +90 -0
  42. cicada/version_check.py +116 -0
  43. cicada_mcp-0.1.4.dist-info/METADATA +619 -0
  44. cicada_mcp-0.1.4.dist-info/RECORD +48 -0
  45. cicada_mcp-0.1.4.dist-info/WHEEL +5 -0
  46. cicada_mcp-0.1.4.dist-info/entry_points.txt +8 -0
  47. cicada_mcp-0.1.4.dist-info/licenses/LICENSE +21 -0
  48. cicada_mcp-0.1.4.dist-info/top_level.txt +1 -0
cicada/mcp_tools.py ADDED
@@ -0,0 +1,291 @@
1
+ """
2
+ Tool definitions for Cicada MCP Server.
3
+
4
+ This module contains all tool schemas that define the interface
5
+ for the Cicada MCP server without any implementation logic.
6
+ """
7
+
8
+ from mcp.types import Tool
9
+
10
+
11
+ def get_tool_definitions() -> list[Tool]:
12
+ """Return all tool definitions for the Cicada MCP server."""
13
+ return [
14
+ Tool(
15
+ name="search_module",
16
+ description=(
17
+ "PREFERRED for Elixir: View a module's complete API - functions with arity, signatures, docs, typespecs, and line numbers.\n\n"
18
+ "Search by module_name='MyApp.User' or file_path='lib/my_app/user.ex'. "
19
+ "Control visibility with private_functions: 'exclude' (default), 'include', or 'only'.\n\n"
20
+ "Returns public functions in markdown format by default. Start here when exploring modules."
21
+ ),
22
+ inputSchema={
23
+ "type": "object",
24
+ "properties": {
25
+ "module_name": {
26
+ "type": "string",
27
+ "description": "Full module name to search (e.g., 'MyApp.User'). Provide either this or file_path.",
28
+ },
29
+ "file_path": {
30
+ "type": "string",
31
+ "description": "Path to an Elixir file (e.g., 'lib/my_app/user.ex'). Provide either this or module_name.",
32
+ },
33
+ "format": {
34
+ "type": "string",
35
+ "description": "Output format: 'markdown' (default) or 'json'",
36
+ "enum": ["markdown", "json"],
37
+ "default": "markdown",
38
+ },
39
+ "private_functions": {
40
+ "type": "string",
41
+ "description": "How to handle private functions: 'exclude' (default, hide private functions), 'include' (show all functions), or 'only' (show only private functions)",
42
+ "enum": ["exclude", "include", "only"],
43
+ "default": "exclude",
44
+ },
45
+ },
46
+ "required": [],
47
+ },
48
+ _meta={"anti_pattern": "Searching for module structure"},
49
+ ),
50
+ Tool(
51
+ name="search_function",
52
+ description=(
53
+ "PREFERRED for Elixir: Find function definitions and call sites across the codebase.\n\n"
54
+ "Search formats: 'create_user', 'create_user/2', or 'MyApp.User.create_user'. "
55
+ "Returns definition with full signature, docs, typespecs, and call sites (module, function, line number). "
56
+ "Set include_usage_examples=true and max_examples=N for code snippets, test_files_only=true for test usage only.\n\n"
57
+ "Tip: Start without usage examples for quick overview, then enable for usage patterns."
58
+ ),
59
+ inputSchema={
60
+ "type": "object",
61
+ "properties": {
62
+ "function_name": {
63
+ "type": "string",
64
+ "description": "Function name to search. Formats: 'create_user', 'create_user/2' (all modules), or 'MyApp.User.create_user', 'MyApp.User.create_user/2' (specific module)",
65
+ },
66
+ "format": {
67
+ "type": "string",
68
+ "description": "Output format: 'markdown' (default) or 'json'",
69
+ "enum": ["markdown", "json"],
70
+ "default": "markdown",
71
+ },
72
+ "include_usage_examples": {
73
+ "type": "boolean",
74
+ "description": "Include actual code lines showing how the function is called (default: false)",
75
+ "default": False,
76
+ },
77
+ "max_examples": {
78
+ "type": "integer",
79
+ "description": "Maximum number of usage examples to show per function (default: 5)",
80
+ "default": 5,
81
+ "minimum": 1,
82
+ "maximum": 20,
83
+ },
84
+ "test_files_only": {
85
+ "type": "boolean",
86
+ "description": "Only show calls from test files (files with 'test' in their path) (default: false)",
87
+ "default": False,
88
+ },
89
+ },
90
+ "required": ["function_name"],
91
+ },
92
+ _meta={"anti_pattern": "Searching for function definitions"},
93
+ ),
94
+ Tool(
95
+ name="search_module_usage",
96
+ description=(
97
+ "PREFERRED for Elixir: Find all module usage and dependencies for impact analysis.\n\n"
98
+ "Provide module_name='MyApp.User' to see aliases, imports, requires, uses, function calls, and line numbers. "
99
+ "Essential for understanding scope before refactoring."
100
+ ),
101
+ inputSchema={
102
+ "type": "object",
103
+ "properties": {
104
+ "module_name": {
105
+ "type": "string",
106
+ "description": "Full module name to search for usage (e.g., 'MyApp.User')",
107
+ },
108
+ "format": {
109
+ "type": "string",
110
+ "description": "Output format: 'markdown' (default) or 'json'",
111
+ "enum": ["markdown", "json"],
112
+ "default": "markdown",
113
+ },
114
+ },
115
+ "required": ["module_name"],
116
+ },
117
+ _meta={"anti_pattern": "Searching for module imports/usage"},
118
+ ),
119
+ Tool(
120
+ name="find_pr_for_line",
121
+ description=(
122
+ "PREFERRED for git history: Discover why code exists and who wrote it.\n\n"
123
+ "Provide file_path and line_number to get PR number, title, author, commit SHA, message, date, and PR link. "
124
+ "Better than git blame - shows full PR context. Cached for fast lookups."
125
+ ),
126
+ inputSchema={
127
+ "type": "object",
128
+ "properties": {
129
+ "file_path": {
130
+ "type": "string",
131
+ "description": "Path to the file (relative to repo root or absolute)",
132
+ },
133
+ "line_number": {
134
+ "type": "integer",
135
+ "description": "Line number (1-indexed)",
136
+ "minimum": 1,
137
+ },
138
+ "format": {
139
+ "type": "string",
140
+ "description": "Output format: 'text' (default), 'json', or 'markdown'",
141
+ "enum": ["text", "json", "markdown"],
142
+ "default": "text",
143
+ },
144
+ },
145
+ "required": ["file_path", "line_number"],
146
+ },
147
+ ),
148
+ Tool(
149
+ name="get_commit_history",
150
+ description=(
151
+ "PREFERRED for git history: Get commit log for files or functions.\n\n"
152
+ "Provide file_path for full history. Add function_name for heuristic search, or start_line/end_line with precise_tracking=True for git log -L. "
153
+ "Returns commit SHA, author, date, message. Set show_evolution=True for creation/modification metadata.\n\n"
154
+ "Complements find_pr_for_line with full commit history."
155
+ ),
156
+ inputSchema={
157
+ "type": "object",
158
+ "properties": {
159
+ "file_path": {
160
+ "type": "string",
161
+ "description": "Path to the file (relative to repo root)",
162
+ },
163
+ "function_name": {
164
+ "type": "string",
165
+ "description": "Optional: function name for heuristic search (filters by function name in commits)",
166
+ },
167
+ "start_line": {
168
+ "type": "integer",
169
+ "description": "Optional: starting line number of function (for precise tracking)",
170
+ "minimum": 1,
171
+ },
172
+ "end_line": {
173
+ "type": "integer",
174
+ "description": "Optional: ending line number of function (for precise tracking)",
175
+ "minimum": 1,
176
+ },
177
+ "precise_tracking": {
178
+ "type": "boolean",
179
+ "description": "Use git log -L for exact line-range tracking (requires start_line and end_line). More accurate than heuristic search. (default: False)",
180
+ "default": False,
181
+ },
182
+ "show_evolution": {
183
+ "type": "boolean",
184
+ "description": "Include function evolution metadata: creation date, last modified, total modifications (requires start_line and end_line). (default: False)",
185
+ "default": False,
186
+ },
187
+ "max_commits": {
188
+ "type": "integer",
189
+ "description": "Maximum number of commits to return (default: 10)",
190
+ "default": 10,
191
+ "minimum": 1,
192
+ "maximum": 50,
193
+ },
194
+ },
195
+ "required": ["file_path"],
196
+ },
197
+ ),
198
+ Tool(
199
+ name="get_blame",
200
+ description=(
201
+ "PREFERRED for authorship: Git blame showing who wrote each line.\n\n"
202
+ "Provide file_path, start_line, and end_line to see author, email, commit SHA, date, and code content grouped by author/commit."
203
+ ),
204
+ inputSchema={
205
+ "type": "object",
206
+ "properties": {
207
+ "file_path": {
208
+ "type": "string",
209
+ "description": "Path to the file (relative to repo root)",
210
+ },
211
+ "start_line": {
212
+ "type": "integer",
213
+ "description": "Starting line number of the code section",
214
+ "minimum": 1,
215
+ },
216
+ "end_line": {
217
+ "type": "integer",
218
+ "description": "Ending line number of the code section",
219
+ "minimum": 1,
220
+ },
221
+ },
222
+ "required": ["file_path", "start_line", "end_line"],
223
+ },
224
+ ),
225
+ Tool(
226
+ name="get_file_pr_history",
227
+ description=(
228
+ "Get all PRs that modified a file with descriptions and review comments.\n\n"
229
+ "Provide file_path to see PR number, title, URL, body, author, merge status, and review comments (with line numbers). "
230
+ "Sorted newest first."
231
+ ),
232
+ inputSchema={
233
+ "type": "object",
234
+ "properties": {
235
+ "file_path": {
236
+ "type": "string",
237
+ "description": "Path to the file (relative to repo root or absolute)",
238
+ },
239
+ },
240
+ "required": ["file_path"],
241
+ },
242
+ ),
243
+ Tool(
244
+ name="search_by_keywords",
245
+ description=(
246
+ "Semantic search for code by concept/topic when exact names are unknown.\n\n"
247
+ "Supports wildcards: keywords=['create*', 'test_*'] or concepts: keywords=['authentication', 'user']. "
248
+ "Returns top 5 results by confidence with matched keywords and location."
249
+ ),
250
+ inputSchema={
251
+ "type": "object",
252
+ "properties": {
253
+ "keywords": {
254
+ "type": "array",
255
+ "items": {"type": "string"},
256
+ "description": "List of keywords to search for (e.g., ['performance', 'benchmark', 'test'] or ['create*', 'test_*'] for wildcards)",
257
+ },
258
+ },
259
+ "required": ["keywords"],
260
+ },
261
+ _meta={
262
+ "anti_pattern": "Searching for module/function usecase by keyword",
263
+ },
264
+ ),
265
+ Tool(
266
+ name="find_dead_code",
267
+ description=(
268
+ "Find potentially unused public functions with confidence levels.\n\n"
269
+ "Returns unused functions grouped by confidence: high (likely dead), medium (possible callbacks), low (possible dynamic calls). "
270
+ "Filter with min_confidence='high' (default), 'medium', or 'low'."
271
+ ),
272
+ inputSchema={
273
+ "type": "object",
274
+ "properties": {
275
+ "min_confidence": {
276
+ "type": "string",
277
+ "description": "Minimum confidence level: 'high' (default, zero usage + no indicators), 'medium' (zero usage + behaviors/uses), 'low' (all candidates)",
278
+ "enum": ["high", "medium", "low"],
279
+ "default": "high",
280
+ },
281
+ "format": {
282
+ "type": "string",
283
+ "description": "Output format: 'markdown' (default) or 'json'",
284
+ "enum": ["markdown", "json"],
285
+ "default": "markdown",
286
+ },
287
+ },
288
+ "required": [],
289
+ },
290
+ ),
291
+ ]
cicada/parser.py ADDED
@@ -0,0 +1,124 @@
1
+ """
2
+ Elixir Parser using tree-sitter.
3
+
4
+ Parses Elixir source files to extract modules and functions.
5
+
6
+ Author: Cursor(Auto)
7
+ """
8
+
9
+ import tree_sitter_elixir as ts_elixir
10
+ from tree_sitter import Parser, Language
11
+
12
+ from .extractors import (
13
+ extract_modules,
14
+ extract_functions,
15
+ extract_specs,
16
+ match_specs_to_functions,
17
+ extract_docs,
18
+ match_docs_to_functions,
19
+ extract_aliases,
20
+ extract_imports,
21
+ extract_requires,
22
+ extract_uses,
23
+ extract_behaviours,
24
+ extract_function_calls,
25
+ extract_value_mentions,
26
+ )
27
+
28
+
29
+ class ElixirParser:
30
+ """Parser for extracting modules and functions from Elixir files."""
31
+
32
+ def __init__(self):
33
+ """Initialize the tree-sitter parser with Elixir grammar."""
34
+ self.parser = Parser(Language(ts_elixir.language())) # type: ignore[no-matching-overload]
35
+
36
+ def parse_file(self, file_path: str) -> list[dict] | None:
37
+ """
38
+ Parse an Elixir file and extract module and function information.
39
+
40
+ Args:
41
+ file_path: Path to the .ex or .exs file to parse
42
+
43
+ Returns:
44
+ Dictionary containing module name and functions list, or None if parsing fails
45
+ """
46
+ try:
47
+ with open(file_path, "rb") as f:
48
+ source_code = f.read()
49
+
50
+ tree = self.parser.parse(source_code)
51
+ root_node = tree.root_node
52
+
53
+ # Check for parse errors
54
+ if root_node.has_error:
55
+ print(f"Parse error in {file_path}")
56
+ return None
57
+
58
+ # Extract all modules
59
+ modules = extract_modules(root_node, source_code)
60
+
61
+ if not modules:
62
+ return None
63
+
64
+ # Process each module to extract additional information
65
+ for module_info in modules:
66
+ do_block = module_info.pop("do_block") # Remove do_block from result
67
+
68
+ # Extract functions and specs
69
+ functions = extract_functions(do_block, source_code)
70
+ specs = extract_specs(do_block, source_code)
71
+
72
+ # Match specs with functions
73
+ functions_with_specs = match_specs_to_functions(functions, specs)
74
+
75
+ # Extract and match docs
76
+ docs = extract_docs(do_block, source_code)
77
+ match_docs_to_functions(functions_with_specs, docs)
78
+
79
+ # Extract dependencies
80
+ aliases = extract_aliases(do_block, source_code)
81
+ imports = extract_imports(do_block, source_code)
82
+ requires = extract_requires(do_block, source_code)
83
+ uses = extract_uses(do_block, source_code)
84
+ behaviours = extract_behaviours(do_block, source_code)
85
+
86
+ # Extract function calls and value mentions
87
+ function_calls = extract_function_calls(do_block, source_code)
88
+ value_mentions = extract_value_mentions(do_block, source_code)
89
+
90
+ # Add all extracted information to module_info
91
+ module_info["functions"] = functions_with_specs
92
+ module_info["aliases"] = aliases
93
+ module_info["imports"] = imports
94
+ module_info["requires"] = requires
95
+ module_info["uses"] = uses
96
+ module_info["behaviours"] = behaviours
97
+ module_info["value_mentions"] = value_mentions
98
+ module_info["calls"] = function_calls
99
+
100
+ return modules if modules else None
101
+
102
+ except Exception as e:
103
+ print(f"Error parsing {file_path}: {e}")
104
+ import traceback
105
+
106
+ traceback.print_exc()
107
+ return None
108
+
109
+
110
+ if __name__ == "__main__":
111
+ # Simple test
112
+ import sys
113
+
114
+ if len(sys.argv) > 1:
115
+ parser = ElixirParser()
116
+ result = parser.parse_file(sys.argv[1])
117
+ if result:
118
+ import json
119
+
120
+ print(json.dumps(result, indent=2))
121
+ else:
122
+ print("Failed to parse file")
123
+ else:
124
+ print("Usage: python parser.py <elixir_file.ex>")