symdex 0.1.0__tar.gz

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.
symdex-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Muhammad Husnain
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
symdex-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,134 @@
1
+ Metadata-Version: 2.4
2
+ Name: symdex
3
+ Version: 0.1.0
4
+ Summary: Universal code-indexer MCP server for AI coding agents
5
+ Author: Muhammad Husnain
6
+ License: MIT
7
+ Project-URL: Homepage, https://symdex.dev
8
+ Project-URL: Repository, https://github.com/husnain/symdex
9
+ Keywords: mcp,code-indexer,ai,llm,tree-sitter,semantic-search
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
18
+ Requires-Python: >=3.11
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: tree-sitter<0.26,>=0.25
22
+ Requires-Dist: tree-sitter-python>=0.1
23
+ Requires-Dist: tree-sitter-javascript>=0.1
24
+ Requires-Dist: tree-sitter-typescript>=0.1
25
+ Requires-Dist: tree-sitter-go>=0.1
26
+ Requires-Dist: tree-sitter-rust>=0.1
27
+ Requires-Dist: tree-sitter-java>=0.1
28
+ Requires-Dist: tree-sitter-php>=0.1
29
+ Requires-Dist: tree-sitter-c-sharp>=0.1
30
+ Requires-Dist: tree-sitter-c>=0.1
31
+ Requires-Dist: tree-sitter-cpp>=0.1
32
+ Requires-Dist: tree-sitter-elixir>=0.1
33
+ Requires-Dist: tree-sitter-ruby>=0.1
34
+ Requires-Dist: fastmcp>=2.0
35
+ Requires-Dist: typer>=0.12
36
+ Requires-Dist: rich>=13
37
+ Requires-Dist: sentence-transformers>=3.0
38
+ Requires-Dist: sqlite-vec>=0.1
39
+ Requires-Dist: numpy>=1.26
40
+ Provides-Extra: dev
41
+ Requires-Dist: pytest>=7; extra == "dev"
42
+ Requires-Dist: pytest-cov>=4; extra == "dev"
43
+ Requires-Dist: pytest-bdd>=7; extra == "dev"
44
+ Dynamic: license-file
45
+
46
+ # SymDex
47
+
48
+ Universal code-indexer MCP server for AI coding agents — Claude, Codex, Gemini, Cursor, and any agent that speaks MCP.
49
+
50
+ **The problem:** Reading a full file to find one function costs ~7,500 tokens. SymDex pre-indexes your codebase once, then returns precise byte-offset locations. The same lookup costs ~200 tokens — a **97% reduction**.
51
+
52
+ ## Installation
53
+
54
+ ```bash
55
+ pip install symdex
56
+ ```
57
+
58
+ ## Quickstart
59
+
60
+ ### 1. Index a project
61
+
62
+ ```bash
63
+ symdex index ./myproject --name myproject
64
+ ```
65
+
66
+ ### 2. Search for a symbol
67
+
68
+ ```bash
69
+ symdex search "validate email"
70
+ ```
71
+
72
+ ### 3. Start the MCP server
73
+
74
+ ```bash
75
+ symdex serve # stdio mode (for local agents)
76
+ symdex serve --port 8080 # HTTP mode
77
+ ```
78
+
79
+ ## MCP Tool Reference
80
+
81
+ | Tool | Description |
82
+ |------|-------------|
83
+ | `index_folder` | Index a local folder (run once per session) |
84
+ | `search_symbols` | Find function/class by name (~200 tokens) |
85
+ | `get_symbol` | Get one function's full source by byte offset |
86
+ | `get_file_outline` | All symbols in a file, no full content |
87
+ | `get_repo_outline` | Directory structure + symbol stats |
88
+ | `search_text` | Text search, returns matching lines only |
89
+ | `get_file_tree` | Directory tree without content |
90
+ | `list_repos` | List all indexed repos |
91
+ | `get_symbols` | Bulk symbol retrieval |
92
+ | `index_repo` | Index a named repo |
93
+ | `invalidate_cache` | Force re-index on next request |
94
+ | `semantic_search` | Find symbols by meaning (embedding similarity) |
95
+ | `get_callers` | Who calls this function |
96
+ | `get_callees` | What this function calls |
97
+
98
+ ## CLI Commands
99
+
100
+ ```bash
101
+ symdex index ./myproject # Index a folder
102
+ symdex search "validate email" # Search symbols
103
+ symdex find MyClass # Exact name lookup
104
+ symdex outline myproject/module.py --repo myproject # File outline
105
+ symdex text "TODO" --repo myproject # Text search
106
+ symdex semantic "parse authentication token" --repo myproject # Semantic search
107
+ symdex callers my_function --repo myproject # Call graph: who calls this
108
+ symdex callees my_function --repo myproject # Call graph: what this calls
109
+ symdex repos # List indexed repos
110
+ symdex serve # Start MCP server
111
+ ```
112
+
113
+ ## Supported Languages
114
+
115
+ Python, JavaScript, TypeScript, Go, Rust, Java, PHP, C#, C, C++, Elixir, Ruby (13 languages via tree-sitter)
116
+
117
+ ## Agent Configuration
118
+
119
+ Add to your agent's MCP config:
120
+
121
+ ```json
122
+ {
123
+ "mcpServers": {
124
+ "symdex": {
125
+ "command": "symdex",
126
+ "args": ["serve"]
127
+ }
128
+ }
129
+ }
130
+ ```
131
+
132
+ ## License
133
+
134
+ MIT — see [LICENSE](LICENSE)
symdex-0.1.0/README.md ADDED
@@ -0,0 +1,89 @@
1
+ # SymDex
2
+
3
+ Universal code-indexer MCP server for AI coding agents — Claude, Codex, Gemini, Cursor, and any agent that speaks MCP.
4
+
5
+ **The problem:** Reading a full file to find one function costs ~7,500 tokens. SymDex pre-indexes your codebase once, then returns precise byte-offset locations. The same lookup costs ~200 tokens — a **97% reduction**.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ pip install symdex
11
+ ```
12
+
13
+ ## Quickstart
14
+
15
+ ### 1. Index a project
16
+
17
+ ```bash
18
+ symdex index ./myproject --name myproject
19
+ ```
20
+
21
+ ### 2. Search for a symbol
22
+
23
+ ```bash
24
+ symdex search "validate email"
25
+ ```
26
+
27
+ ### 3. Start the MCP server
28
+
29
+ ```bash
30
+ symdex serve # stdio mode (for local agents)
31
+ symdex serve --port 8080 # HTTP mode
32
+ ```
33
+
34
+ ## MCP Tool Reference
35
+
36
+ | Tool | Description |
37
+ |------|-------------|
38
+ | `index_folder` | Index a local folder (run once per session) |
39
+ | `search_symbols` | Find function/class by name (~200 tokens) |
40
+ | `get_symbol` | Get one function's full source by byte offset |
41
+ | `get_file_outline` | All symbols in a file, no full content |
42
+ | `get_repo_outline` | Directory structure + symbol stats |
43
+ | `search_text` | Text search, returns matching lines only |
44
+ | `get_file_tree` | Directory tree without content |
45
+ | `list_repos` | List all indexed repos |
46
+ | `get_symbols` | Bulk symbol retrieval |
47
+ | `index_repo` | Index a named repo |
48
+ | `invalidate_cache` | Force re-index on next request |
49
+ | `semantic_search` | Find symbols by meaning (embedding similarity) |
50
+ | `get_callers` | Who calls this function |
51
+ | `get_callees` | What this function calls |
52
+
53
+ ## CLI Commands
54
+
55
+ ```bash
56
+ symdex index ./myproject # Index a folder
57
+ symdex search "validate email" # Search symbols
58
+ symdex find MyClass # Exact name lookup
59
+ symdex outline myproject/module.py --repo myproject # File outline
60
+ symdex text "TODO" --repo myproject # Text search
61
+ symdex semantic "parse authentication token" --repo myproject # Semantic search
62
+ symdex callers my_function --repo myproject # Call graph: who calls this
63
+ symdex callees my_function --repo myproject # Call graph: what this calls
64
+ symdex repos # List indexed repos
65
+ symdex serve # Start MCP server
66
+ ```
67
+
68
+ ## Supported Languages
69
+
70
+ Python, JavaScript, TypeScript, Go, Rust, Java, PHP, C#, C, C++, Elixir, Ruby (13 languages via tree-sitter)
71
+
72
+ ## Agent Configuration
73
+
74
+ Add to your agent's MCP config:
75
+
76
+ ```json
77
+ {
78
+ "mcpServers": {
79
+ "symdex": {
80
+ "command": "symdex",
81
+ "args": ["serve"]
82
+ }
83
+ }
84
+ }
85
+ ```
86
+
87
+ ## License
88
+
89
+ MIT — see [LICENSE](LICENSE)
@@ -0,0 +1,67 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "symdex"
7
+ version = "0.1.0"
8
+ description = "Universal code-indexer MCP server for AI coding agents"
9
+ authors = [{name = "Muhammad Husnain"}]
10
+ license = {text = "MIT"}
11
+ requires-python = ">=3.11"
12
+ readme = "README.md"
13
+ keywords = ["mcp", "code-indexer", "ai", "llm", "tree-sitter", "semantic-search"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Topic :: Software Development :: Libraries :: Python Modules",
22
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
23
+ ]
24
+ dependencies = [
25
+ "tree-sitter>=0.25,<0.26",
26
+ "tree-sitter-python>=0.1",
27
+ "tree-sitter-javascript>=0.1",
28
+ "tree-sitter-typescript>=0.1",
29
+ "tree-sitter-go>=0.1",
30
+ "tree-sitter-rust>=0.1",
31
+ "tree-sitter-java>=0.1",
32
+ "tree-sitter-php>=0.1",
33
+ # tree-sitter-dart: not available on PyPI, excluded
34
+ "tree-sitter-c-sharp>=0.1",
35
+ "tree-sitter-c>=0.1",
36
+ "tree-sitter-cpp>=0.1",
37
+ "tree-sitter-elixir>=0.1",
38
+ "tree-sitter-ruby>=0.1",
39
+ "fastmcp>=2.0",
40
+ "typer>=0.12",
41
+ "rich>=13",
42
+ "sentence-transformers>=3.0",
43
+ "sqlite-vec>=0.1",
44
+ "numpy>=1.26",
45
+ ]
46
+
47
+ [project.urls]
48
+ Homepage = "https://symdex.dev"
49
+ Repository = "https://github.com/husnain/symdex"
50
+
51
+ [project.optional-dependencies]
52
+ dev = [
53
+ "pytest>=7",
54
+ "pytest-cov>=4",
55
+ "pytest-bdd>=7",
56
+ ]
57
+
58
+ [project.scripts]
59
+ symdex = "symdex.cli:app"
60
+
61
+ [tool.setuptools.packages.find]
62
+ where = ["."]
63
+ include = ["symdex*"]
64
+
65
+ [tool.pytest.ini_options]
66
+ testpaths = ["tests"]
67
+ python_files = ["test_*.py", "*_test.py", "*_steps.py"]
symdex-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ # Copyright (c) 2026 Muhammad Husnain
2
+ # This file is part of SymDex.
3
+ # License: See LICENSE file in the project root.
@@ -0,0 +1,314 @@
1
+ # Copyright (c) 2026 Muhammad Husnain
2
+ # This file is part of SymDex.
3
+ # License: See LICENSE file in the project root.
4
+
5
+ import json
6
+ import os
7
+
8
+ import typer
9
+ from rich.console import Console
10
+ from rich.table import Table
11
+
12
+ from symdex.core.indexer import index_folder as _index_folder, invalidate as _invalidate
13
+ from symdex.core.storage import (
14
+ get_connection,
15
+ get_db_path,
16
+ get_registry_path, # noqa: F401 — imported for monkeypatching
17
+ query_file_symbols,
18
+ query_repos,
19
+ search_text_in_index,
20
+ upsert_repo,
21
+ )
22
+ from symdex.search.symbol_search import search_symbols as _search_symbols
23
+
24
+ app = typer.Typer(name="symdex", help="SymDex — universal code indexer")
25
+ console = Console()
26
+ err_console = Console(stderr=True)
27
+
28
+
29
+ @app.command()
30
+ def index(
31
+ path: str = typer.Argument(..., help="Directory to index"),
32
+ name: str = typer.Option(None, "--name", "-n", help="Repo name (defaults to folder name)"),
33
+ ) -> None:
34
+ """Index a folder and register it."""
35
+ if not os.path.isdir(path):
36
+ err_console.print(f"[red]Error:[/red] Path does not exist: {path}")
37
+ raise typer.Exit(code=1)
38
+ result = _index_folder(path, name=name)
39
+ upsert_repo(result.repo, root_path=os.path.abspath(path), db_path=result.db_path)
40
+ table = Table(title="Index Result")
41
+ table.add_column("Repo", style="cyan")
42
+ table.add_column("Indexed", style="green")
43
+ table.add_column("Skipped", style="yellow")
44
+ table.add_column("DB Path")
45
+ table.add_row(result.repo, str(result.indexed_count), str(result.skipped_count), result.db_path)
46
+ console.print(table)
47
+
48
+
49
+ @app.command()
50
+ def search(
51
+ query: str = typer.Argument(..., help="Symbol name to search for"),
52
+ repo: str = typer.Option(None, "--repo", "-r", help="Repo name (omit to search all repos)"),
53
+ kind: str = typer.Option(None, "--kind", "-k", help="Symbol kind filter"),
54
+ limit: int = typer.Option(20, "--limit", "-l", help="Max results"),
55
+ json_output: bool = typer.Option(False, "--json", help="Output raw JSON"),
56
+ ) -> None:
57
+ """Find functions/classes by name (omit --repo to search all indexed repos)."""
58
+ if repo:
59
+ conn = get_connection(get_db_path(repo))
60
+ try:
61
+ symbols = _search_symbols(conn, repo=repo, query=query, kind=kind, limit=limit)
62
+ finally:
63
+ conn.close()
64
+ else:
65
+ from symdex.graph.registry import search_across_repos
66
+ symbols = search_across_repos(query=query, kind=kind, limit=limit)
67
+ if not symbols:
68
+ err_console.print(f"[red]Error:[/red] No symbols found matching: {query}")
69
+ raise typer.Exit(code=1)
70
+ if json_output:
71
+ typer.echo(json.dumps({"symbols": symbols}))
72
+ return
73
+ table = Table(title=f"Symbols matching '{query}'")
74
+ table.add_column("Repo", style="blue")
75
+ table.add_column("Name", style="cyan")
76
+ table.add_column("Kind", style="magenta")
77
+ table.add_column("File")
78
+ table.add_column("Start", style="dim")
79
+ for s in symbols:
80
+ table.add_row(s.get("repo", repo or ""), s["name"], s["kind"], s["file"], str(s["start_byte"]))
81
+ console.print(table)
82
+
83
+
84
+ @app.command()
85
+ def find(
86
+ name: str = typer.Argument(..., help="Exact symbol name"),
87
+ repo: str = typer.Option(None, "--repo", "-r", help="Repo name"),
88
+ json_output: bool = typer.Option(False, "--json", help="Output raw JSON"),
89
+ ) -> None:
90
+ """Exact symbol name lookup by symbol name."""
91
+ if not repo:
92
+ err_console.print("[red]Error:[/red] --repo is required")
93
+ raise typer.Exit(code=1)
94
+ conn = get_connection(get_db_path(repo))
95
+ try:
96
+ symbols = _search_symbols(conn, repo=repo, query=name, limit=1)
97
+ finally:
98
+ conn.close()
99
+ if not symbols:
100
+ err_console.print(f"[red]Error:[/red] Symbol not found: {name}")
101
+ raise typer.Exit(code=1)
102
+ if json_output:
103
+ typer.echo(json.dumps({"symbols": symbols}))
104
+ return
105
+ s = symbols[0]
106
+ table = Table(title=f"Symbol: {name}")
107
+ table.add_column("Field")
108
+ table.add_column("Value")
109
+ for k, v in s.items():
110
+ table.add_row(k, str(v) if v is not None else "")
111
+ console.print(table)
112
+
113
+
114
+ @app.command()
115
+ def outline(
116
+ file: str = typer.Argument(..., help="Relative file path within repo"),
117
+ repo: str = typer.Option(..., "--repo", "-r", help="Repo name"),
118
+ json_output: bool = typer.Option(False, "--json", help="Output raw JSON"),
119
+ ) -> None:
120
+ """List all symbols in a file."""
121
+ conn = get_connection(get_db_path(repo))
122
+ try:
123
+ symbols = query_file_symbols(conn, repo=repo, file=file)
124
+ finally:
125
+ conn.close()
126
+ if not symbols:
127
+ err_console.print(f"[red]Error:[/red] No symbols found in: {file}")
128
+ raise typer.Exit(code=1)
129
+ if json_output:
130
+ typer.echo(json.dumps({"symbols": symbols}))
131
+ return
132
+ table = Table(title=f"Outline: {file}")
133
+ table.add_column("Name", style="cyan")
134
+ table.add_column("Kind", style="magenta")
135
+ table.add_column("Start", style="dim")
136
+ table.add_column("End", style="dim")
137
+ for s in symbols:
138
+ table.add_row(s["name"], s["kind"], str(s["start_byte"]), str(s["end_byte"]))
139
+ console.print(table)
140
+
141
+
142
+ @app.command()
143
+ def text(
144
+ query: str = typer.Argument(..., help="Text to search for"),
145
+ repo: str = typer.Option(None, "--repo", "-r", help="Repo name"),
146
+ pattern: str = typer.Option(None, "--pattern", "-p", help="File glob pattern"),
147
+ json_output: bool = typer.Option(False, "--json", help="Output raw JSON"),
148
+ ) -> None:
149
+ """Text search across indexed files."""
150
+ if not repo:
151
+ err_console.print("[red]Error:[/red] --repo is required")
152
+ raise typer.Exit(code=1)
153
+ all_repos = query_repos()
154
+ repo_info = next((r for r in all_repos if r["name"] == repo), None)
155
+ if repo_info is None:
156
+ err_console.print(f"[red]Error:[/red] Repo not indexed: {repo}")
157
+ raise typer.Exit(code=1)
158
+ conn = get_connection(get_db_path(repo))
159
+ try:
160
+ matches = search_text_in_index(conn, repo=repo, query=query, repo_root=repo_info["root_path"], file_pattern=pattern)
161
+ finally:
162
+ conn.close()
163
+ if not matches:
164
+ err_console.print(f"[red]Error:[/red] No matches found for: {query}")
165
+ raise typer.Exit(code=1)
166
+ if json_output:
167
+ typer.echo(json.dumps({"matches": matches}))
168
+ return
169
+ table = Table(title=f"Text matches for '{query}'")
170
+ table.add_column("File")
171
+ table.add_column("Line", style="dim")
172
+ table.add_column("Text")
173
+ for m in matches:
174
+ table.add_row(m["file"], str(m["line"]), m["text"])
175
+ console.print(table)
176
+
177
+
178
+ @app.command()
179
+ def semantic(
180
+ query: str = typer.Argument(..., help="Natural language query"),
181
+ repo: str = typer.Option(None, "--repo", "-r", help="Repo name"),
182
+ limit: int = typer.Option(10, "--limit", "-l", help="Max results"),
183
+ json_output: bool = typer.Option(False, "--json", help="Output raw JSON"),
184
+ ) -> None:
185
+ """Semantic similarity search by meaning."""
186
+ from symdex.search.semantic import search_semantic
187
+ if not repo:
188
+ err_console.print("[red]Error:[/red] --repo is required")
189
+ raise typer.Exit(code=1)
190
+ conn = get_connection(get_db_path(repo))
191
+ try:
192
+ results = search_semantic(conn, query=query, repo=repo, limit=limit)
193
+ finally:
194
+ conn.close()
195
+ if not results:
196
+ err_console.print(f"[red]Error:[/red] No semantic matches found for: {query}")
197
+ raise typer.Exit(code=1)
198
+ if json_output:
199
+ typer.echo(json.dumps({"symbols": results}))
200
+ return
201
+ table = Table(title=f"Semantic matches for '{query}'")
202
+ table.add_column("Name", style="cyan")
203
+ table.add_column("Kind", style="magenta")
204
+ table.add_column("Score", style="green")
205
+ table.add_column("File")
206
+ for s in results:
207
+ table.add_row(s["name"], s["kind"], str(s["score"]), s["file"])
208
+ console.print(table)
209
+
210
+
211
+ @app.command()
212
+ def callers(
213
+ name: str = typer.Argument(..., help="Function name to find callers of"),
214
+ repo: str = typer.Option(..., "--repo", "-r", help="Repo name"),
215
+ json_output: bool = typer.Option(False, "--json", help="Output raw JSON"),
216
+ ) -> None:
217
+ """Show all functions that call the named function."""
218
+ from symdex.graph.call_graph import get_callers as _get_callers
219
+ conn = get_connection(get_db_path(repo))
220
+ try:
221
+ results = _get_callers(conn, name=name, repo=repo)
222
+ finally:
223
+ conn.close()
224
+ if not results:
225
+ err_console.print(f"[red]Error:[/red] No callers found for: {name}")
226
+ raise typer.Exit(code=1)
227
+ if json_output:
228
+ typer.echo(json.dumps({"callers": results}))
229
+ return
230
+ table = Table(title=f"Callers of '{name}'")
231
+ table.add_column("Name", style="cyan")
232
+ table.add_column("Kind", style="magenta")
233
+ table.add_column("File")
234
+ for s in results:
235
+ table.add_row(s["name"], s["kind"], s["file"])
236
+ console.print(table)
237
+
238
+
239
+ @app.command()
240
+ def callees(
241
+ name: str = typer.Argument(..., help="Function name to find callees of"),
242
+ repo: str = typer.Option(..., "--repo", "-r", help="Repo name"),
243
+ json_output: bool = typer.Option(False, "--json", help="Output raw JSON"),
244
+ ) -> None:
245
+ """Show all functions called by the named function."""
246
+ from symdex.graph.call_graph import get_callees as _get_callees
247
+ conn = get_connection(get_db_path(repo))
248
+ try:
249
+ results = _get_callees(conn, name=name, repo=repo)
250
+ finally:
251
+ conn.close()
252
+ if not results:
253
+ err_console.print(f"[red]Error:[/red] No callees found for: {name}")
254
+ raise typer.Exit(code=1)
255
+ if json_output:
256
+ typer.echo(json.dumps({"callees": results}))
257
+ return
258
+ table = Table(title=f"Callees of '{name}'")
259
+ table.add_column("Name", style="cyan")
260
+ table.add_column("File")
261
+ for s in results:
262
+ table.add_row(s["name"], s.get("file") or "")
263
+ console.print(table)
264
+
265
+
266
+ @app.command()
267
+ def repos(
268
+ json_output: bool = typer.Option(False, "--json", help="Output raw JSON"),
269
+ ) -> None:
270
+ """List all indexed repositories."""
271
+ all_repos = query_repos()
272
+ if not all_repos:
273
+ err_console.print("[red]Error:[/red] No repos indexed yet.")
274
+ raise typer.Exit(code=1)
275
+ if json_output:
276
+ typer.echo(json.dumps({"repos": all_repos}))
277
+ return
278
+ table = Table(title="Indexed Repositories")
279
+ table.add_column("Name", style="cyan")
280
+ table.add_column("Root Path")
281
+ table.add_column("Last Indexed", style="dim")
282
+ for r in all_repos:
283
+ table.add_row(r["name"], r["root_path"], str(r.get("last_indexed", "")))
284
+ console.print(table)
285
+
286
+
287
+ @app.command()
288
+ def invalidate(
289
+ repo: str = typer.Option(..., "--repo", "-r", help="Repo name"),
290
+ file: str = typer.Option(None, "--file", "-f", help="Specific file to invalidate"),
291
+ json_output: bool = typer.Option(False, "--json", help="Output raw JSON"),
292
+ ) -> None:
293
+ """Force re-index of a repo or specific file."""
294
+ count = _invalidate(repo, file=file)
295
+ if json_output:
296
+ typer.echo(json.dumps({"invalidated": count}))
297
+ return
298
+ console.print(f"Invalidated [green]{count}[/green] record(s) for repo '[cyan]{repo}[/cyan]'")
299
+
300
+
301
+ @app.command()
302
+ def serve(
303
+ port: int = typer.Option(None, "--port", "-p", help="HTTP port (omit for stdio mode)"),
304
+ ) -> None:
305
+ """Start the MCP server."""
306
+ from symdex.mcp.server import mcp
307
+ if port:
308
+ mcp.run(transport="streamable-http", port=port)
309
+ else:
310
+ mcp.run()
311
+
312
+
313
+ if __name__ == "__main__":
314
+ app()
@@ -0,0 +1,3 @@
1
+ # Copyright (c) 2026 Muhammad Husnain
2
+ # This file is part of SymDex.
3
+ # License: See LICENSE file in the project root.