contextl-mcp 0.1.2__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.
@@ -0,0 +1,150 @@
1
+ Metadata-Version: 2.4
2
+ Name: contextl-mcp
3
+ Version: 0.1.2
4
+ Summary: Repository Intelligence Engine — MCP server for AI coding agents
5
+ Project-URL: Homepage, https://github.com/dev7shah/prune
6
+ Project-URL: Issues, https://github.com/dev7shah/prune/issues
7
+ Author: dev7shah
8
+ License: MIT
9
+ Keywords: ai,claude,code-search,context,cursor,mcp,model-context-protocol,nextjs,repository,typescript,vscode,windsurf
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Software Development :: Libraries
20
+ Classifier: Topic :: Utilities
21
+ Requires-Python: >=3.9
22
+ Requires-Dist: mcp
23
+ Requires-Dist: networkx
24
+ Description-Content-Type: text/markdown
25
+
26
+ # contextl
27
+
28
+ > **Context-selection engine for AI coding assistants.**
29
+ > Finds the most relevant files in your codebase for a natural-language change request — no LLM, no embeddings, no vector database. Pure graph + text scoring.
30
+
31
+ ```
32
+ "fix the upload error" → [FileUploader.tsx, lib/upload.ts, UploadSection.tsx]
33
+ ```
34
+
35
+ Instead of feeding your entire repo to an AI, `contextl` reduces 5 000 files down to the 5 most relevant ones in milliseconds.
36
+
37
+ ---
38
+
39
+ ## Installation
40
+
41
+ ```bash
42
+ pip install contextl
43
+ ```
44
+
45
+ Python 3.9+ required. `networkx` and `mcp` are installed automatically as dependencies.
46
+
47
+ ---
48
+
49
+ ## Quick start — connect to your IDE
50
+
51
+ Paste this into your IDE's MCP config file:
52
+
53
+ ```json
54
+ {
55
+ "mcpServers": {
56
+ "contextl": {
57
+ "command": "contextl"
58
+ }
59
+ }
60
+ }
61
+ ```
62
+
63
+ #### Config file locations
64
+
65
+ | IDE | Config file |
66
+ |-----|-------------|
67
+ | **Antigravity** | `~/.gemini/antigravity/mcp/` (MCP server directory) |
68
+ | **Cursor** | `~/.cursor/mcp.json` |
69
+ | **Windsurf** | `~/.codeium/windsurf/mcp_config.json` |
70
+ | **Claude Code** | `~/.claude.json` (or run `claude mcp add`) |
71
+ | **VS Code** | `.vscode/mcp.json` in your workspace root |
72
+
73
+ ### Use it
74
+
75
+ Just talk to your IDE's AI normally:
76
+
77
+ ```
78
+ You: "fix the file upload error handler"
79
+ IDE: calls query_repo → gets [FileUploader.tsx, lib/upload.ts, …]
80
+ IDE: reads only those 5 files instead of the whole repo
81
+ ```
82
+
83
+ ---
84
+
85
+ ## Tools exposed
86
+
87
+ ### `query_repo(repo_path, query, top_n?)`
88
+
89
+ Ranks the most relevant files for a change request.
90
+
91
+ **Parameters:**
92
+ - `repo_path` — absolute path to the repository root
93
+ - `query` — natural-language description of the change (e.g. `"change the download button color"`)
94
+ - `top_n` — max results to return (default `5`, max `20`)
95
+
96
+ **Returns:**
97
+ ```json
98
+ {
99
+ "query": "change the download button",
100
+ "repo": "/path/to/repo",
101
+ "total_files_scanned": 142,
102
+ "results": [
103
+ {
104
+ "rank": 1,
105
+ "path": "components/DownloadButton.tsx",
106
+ "score": 0.9800,
107
+ "confidence": "high",
108
+ "matched_terms": ["button", "download"],
109
+ "reasoning": "Filename strongly matches query terms; file contents heavily reference query terms."
110
+ }
111
+ ]
112
+ }
113
+ ```
114
+
115
+ ### `scan_repo(repo_path)`
116
+
117
+ Lists all source files the engine can see in a repository.
118
+
119
+ ---
120
+
121
+ ## How it works
122
+
123
+ The engine runs **entirely locally** — no network calls, no AI APIs, no data leaves your machine.
124
+
125
+ Scoring uses four signals:
126
+
127
+ | Signal | Weight | Description |
128
+ |--------|--------|-------------|
129
+ | Keyword match | 0.5 | Query terms in the file path / name |
130
+ | Content match | 0.5 | Query terms inside the file source |
131
+ | Neighbor bonus | +0.15 | Files near high-scoring files in the import graph |
132
+ | PageRank | 0.05 | Tiebreaker: more connected files rank slightly higher |
133
+
134
+ Supports **Next.js / React / TypeScript** repos (`.ts`, `.tsx`, `.js`, `.jsx`). Automatically detects `@/` path aliases from `tsconfig.json`.
135
+
136
+ ---
137
+
138
+ ## Also available as an npm package
139
+
140
+ ```bash
141
+ npx contextl
142
+ ```
143
+
144
+ For IDE configs that prefer `npx` over a Python command.
145
+
146
+ ---
147
+
148
+ ## License
149
+
150
+ MIT
@@ -0,0 +1,125 @@
1
+ # contextl
2
+
3
+ > **Context-selection engine for AI coding assistants.**
4
+ > Finds the most relevant files in your codebase for a natural-language change request — no LLM, no embeddings, no vector database. Pure graph + text scoring.
5
+
6
+ ```
7
+ "fix the upload error" → [FileUploader.tsx, lib/upload.ts, UploadSection.tsx]
8
+ ```
9
+
10
+ Instead of feeding your entire repo to an AI, `contextl` reduces 5 000 files down to the 5 most relevant ones in milliseconds.
11
+
12
+ ---
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ pip install contextl
18
+ ```
19
+
20
+ Python 3.9+ required. `networkx` and `mcp` are installed automatically as dependencies.
21
+
22
+ ---
23
+
24
+ ## Quick start — connect to your IDE
25
+
26
+ Paste this into your IDE's MCP config file:
27
+
28
+ ```json
29
+ {
30
+ "mcpServers": {
31
+ "contextl": {
32
+ "command": "contextl"
33
+ }
34
+ }
35
+ }
36
+ ```
37
+
38
+ #### Config file locations
39
+
40
+ | IDE | Config file |
41
+ |-----|-------------|
42
+ | **Antigravity** | `~/.gemini/antigravity/mcp/` (MCP server directory) |
43
+ | **Cursor** | `~/.cursor/mcp.json` |
44
+ | **Windsurf** | `~/.codeium/windsurf/mcp_config.json` |
45
+ | **Claude Code** | `~/.claude.json` (or run `claude mcp add`) |
46
+ | **VS Code** | `.vscode/mcp.json` in your workspace root |
47
+
48
+ ### Use it
49
+
50
+ Just talk to your IDE's AI normally:
51
+
52
+ ```
53
+ You: "fix the file upload error handler"
54
+ IDE: calls query_repo → gets [FileUploader.tsx, lib/upload.ts, …]
55
+ IDE: reads only those 5 files instead of the whole repo
56
+ ```
57
+
58
+ ---
59
+
60
+ ## Tools exposed
61
+
62
+ ### `query_repo(repo_path, query, top_n?)`
63
+
64
+ Ranks the most relevant files for a change request.
65
+
66
+ **Parameters:**
67
+ - `repo_path` — absolute path to the repository root
68
+ - `query` — natural-language description of the change (e.g. `"change the download button color"`)
69
+ - `top_n` — max results to return (default `5`, max `20`)
70
+
71
+ **Returns:**
72
+ ```json
73
+ {
74
+ "query": "change the download button",
75
+ "repo": "/path/to/repo",
76
+ "total_files_scanned": 142,
77
+ "results": [
78
+ {
79
+ "rank": 1,
80
+ "path": "components/DownloadButton.tsx",
81
+ "score": 0.9800,
82
+ "confidence": "high",
83
+ "matched_terms": ["button", "download"],
84
+ "reasoning": "Filename strongly matches query terms; file contents heavily reference query terms."
85
+ }
86
+ ]
87
+ }
88
+ ```
89
+
90
+ ### `scan_repo(repo_path)`
91
+
92
+ Lists all source files the engine can see in a repository.
93
+
94
+ ---
95
+
96
+ ## How it works
97
+
98
+ The engine runs **entirely locally** — no network calls, no AI APIs, no data leaves your machine.
99
+
100
+ Scoring uses four signals:
101
+
102
+ | Signal | Weight | Description |
103
+ |--------|--------|-------------|
104
+ | Keyword match | 0.5 | Query terms in the file path / name |
105
+ | Content match | 0.5 | Query terms inside the file source |
106
+ | Neighbor bonus | +0.15 | Files near high-scoring files in the import graph |
107
+ | PageRank | 0.05 | Tiebreaker: more connected files rank slightly higher |
108
+
109
+ Supports **Next.js / React / TypeScript** repos (`.ts`, `.tsx`, `.js`, `.jsx`). Automatically detects `@/` path aliases from `tsconfig.json`.
110
+
111
+ ---
112
+
113
+ ## Also available as an npm package
114
+
115
+ ```bash
116
+ npx contextl
117
+ ```
118
+
119
+ For IDE configs that prefer `npx` over a Python command.
120
+
121
+ ---
122
+
123
+ ## License
124
+
125
+ MIT
@@ -0,0 +1 @@
1
+ __version__ = "0.1.2"
@@ -0,0 +1,46 @@
1
+ """
2
+ prune-mcp — entry point
3
+
4
+ Called when a user runs:
5
+ prune-mcp (console_scripts shim installed by pip)
6
+ python -m prune_mcp (module execution)
7
+
8
+ Resolves the bundled mcp_server.py, sets up the environment, then uses
9
+ os.execve to *replace* the current process with Python running mcp_server.py.
10
+ This means no subprocess wrapper, no extra PID, and clean signal forwarding —
11
+ the IDE talks directly to mcp_server.py over stdio.
12
+ """
13
+
14
+ import os
15
+ import sys
16
+ from pathlib import Path
17
+
18
+
19
+ def main() -> None:
20
+ # The bundled engine files live in python/ next to this package.
21
+ # Installed layout (wheel):
22
+ # site-packages/
23
+ # prune_mcp/ ← this file lives here
24
+ # python/ ← engine files live here
25
+ python_dir = Path(__file__).parent.parent / "python"
26
+ mcp_server = python_dir / "mcp_server.py"
27
+
28
+ if not mcp_server.exists():
29
+ print(
30
+ f"Error: cannot find mcp_server.py at {mcp_server}\n"
31
+ "The package may be corrupted — try reinstalling:\n"
32
+ " pip install --force-reinstall prune-mcp",
33
+ file=sys.stderr,
34
+ )
35
+ sys.exit(1)
36
+
37
+ env = os.environ.copy()
38
+ env["PYTHONPATH"] = str(python_dir)
39
+ env["PYTHONUNBUFFERED"] = "1"
40
+
41
+ # Replace this process entirely — clean stdio passthrough, no extra PID.
42
+ os.execve(sys.executable, [sys.executable, str(mcp_server)], env)
43
+
44
+
45
+ if __name__ == "__main__":
46
+ main()
@@ -0,0 +1,50 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "contextl-mcp"
7
+ version = "0.1.2"
8
+ description = "Repository Intelligence Engine — MCP server for AI coding agents"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ authors = [{ name = "dev7shah" }]
12
+ keywords = [
13
+ "mcp",
14
+ "model-context-protocol",
15
+ "ai",
16
+ "code-search",
17
+ "context",
18
+ "cursor",
19
+ "windsurf",
20
+ "claude",
21
+ "vscode",
22
+ "typescript",
23
+ "nextjs",
24
+ "repository",
25
+ ]
26
+ classifiers = [
27
+ "Development Status :: 4 - Beta",
28
+ "Intended Audience :: Developers",
29
+ "License :: OSI Approved :: MIT License",
30
+ "Programming Language :: Python :: 3",
31
+ "Programming Language :: Python :: 3.9",
32
+ "Programming Language :: Python :: 3.10",
33
+ "Programming Language :: Python :: 3.11",
34
+ "Programming Language :: Python :: 3.12",
35
+ "Programming Language :: Python :: 3.13",
36
+ "Topic :: Software Development :: Libraries",
37
+ "Topic :: Utilities",
38
+ ]
39
+ requires-python = ">=3.9"
40
+ dependencies = ["networkx", "mcp"]
41
+
42
+ [project.urls]
43
+ Homepage = "https://github.com/dev7shah/prune"
44
+ Issues = "https://github.com/dev7shah/prune/issues"
45
+
46
+ [project.scripts]
47
+ contextl = "prune_mcp.__main__:main"
48
+
49
+ [tool.hatch.build.targets.wheel]
50
+ packages = ["prune_mcp", "python"]
@@ -0,0 +1,171 @@
1
+ """
2
+ Repository Intelligence Engine
3
+ Step 3: Graph Builder
4
+
5
+ Takes the import relationships from the parser and builds a directed graph
6
+ where nodes are files and edges are import dependencies.
7
+
8
+ Adds useful metadata to each node:
9
+ - in_degree: how many files import this file (how "shared" it is)
10
+ - out_degree: how many files this file imports (how many deps it has)
11
+ - centrality: PageRank score (overall importance in the graph)
12
+
13
+ Also computes connected clusters so we can understand which files
14
+ belong to the same logical feature.
15
+ """
16
+
17
+ import networkx as nx
18
+ from dataclasses import dataclass, field
19
+ from pathlib import Path
20
+
21
+ from scanner import scan_repo
22
+ from import_parser import parse_imports, ParseResult
23
+
24
+
25
+ @dataclass
26
+ class FileNode:
27
+ """A file in the repository graph with computed metrics."""
28
+ path: str
29
+ extension: str
30
+ size_bytes: int
31
+
32
+ # Graph metrics (computed after graph is built)
33
+ in_degree: int = 0 # files that import this
34
+ out_degree: int = 0 # files this imports
35
+ centrality: float = 0.0 # PageRank score
36
+
37
+
38
+ @dataclass
39
+ class RepoGraph:
40
+ """
41
+ The complete dependency graph of the repository.
42
+ Wraps a NetworkX DiGraph with helper methods.
43
+ """
44
+ graph: nx.DiGraph
45
+ nodes: dict[str, FileNode] # path → FileNode
46
+ root: str
47
+
48
+ def get_dependents(self, file_path: str) -> list[str]:
49
+ """Files that directly import this file (who uses me?)."""
50
+ return list(self.graph.predecessors(file_path))
51
+
52
+ def get_dependencies(self, file_path: str) -> list[str]:
53
+ """Files this file directly imports (what do I use?)."""
54
+ return list(self.graph.successors(file_path))
55
+
56
+ def get_neighbors(self, file_path: str, depth: int = 1) -> set[str]:
57
+ """
58
+ All files within `depth` hops of file_path (both directions).
59
+ depth=1 → direct imports + direct importers
60
+ depth=2 → their imports/importers too
61
+ """
62
+ neighbors = set()
63
+ frontier = {file_path}
64
+
65
+ for _ in range(depth):
66
+ next_frontier = set()
67
+ for node in frontier:
68
+ next_frontier.update(self.graph.predecessors(node))
69
+ next_frontier.update(self.graph.successors(node))
70
+ new_nodes = next_frontier - neighbors - {file_path}
71
+ neighbors.update(new_nodes)
72
+ frontier = new_nodes
73
+
74
+ return neighbors
75
+
76
+ def most_central_files(self, top_n: int = 5) -> list[FileNode]:
77
+ """Return the top N files by PageRank centrality."""
78
+ sorted_nodes = sorted(
79
+ self.nodes.values(),
80
+ key=lambda n: n.centrality,
81
+ reverse=True,
82
+ )
83
+ return sorted_nodes[:top_n]
84
+
85
+ def summary(self) -> str:
86
+ lines = [
87
+ f"Repository: {self.root}",
88
+ f"Nodes (files): {self.graph.number_of_nodes()}",
89
+ f"Edges (imports): {self.graph.number_of_edges()}",
90
+ f"Connected components: {nx.number_weakly_connected_components(self.graph)}",
91
+ "",
92
+ "Most central files (PageRank):",
93
+ ]
94
+ for node in self.most_central_files():
95
+ lines.append(
96
+ f" {node.centrality:.4f} {node.path}"
97
+ f" (imported by {node.in_degree}, imports {node.out_degree})"
98
+ )
99
+ return "\n".join(lines)
100
+
101
+ def print_adjacency(self) -> None:
102
+ """Print a human-readable view of the full graph."""
103
+ print("Full dependency graph:")
104
+ for node_path in sorted(self.graph.nodes):
105
+ deps = self.get_dependencies(node_path)
106
+ used_by = self.get_dependents(node_path)
107
+ print(f"\n {node_path}")
108
+ if deps:
109
+ for d in deps:
110
+ print(f" imports → {d}")
111
+ if used_by:
112
+ for u in used_by:
113
+ print(f" used by ← {u}")
114
+
115
+
116
+ def build_graph(scan_result, parse_result: ParseResult) -> RepoGraph:
117
+ """
118
+ Build a directed dependency graph from scan + parse results.
119
+
120
+ Args:
121
+ scan_result: Output from scan_repo()
122
+ parse_result: Output from parse_imports()
123
+
124
+ Returns:
125
+ RepoGraph with computed metrics on every node.
126
+ """
127
+ G = nx.DiGraph()
128
+
129
+ # Add all scanned files as nodes
130
+ file_nodes: dict[str, FileNode] = {}
131
+ for f in scan_result.files:
132
+ node = FileNode(
133
+ path=f.path,
134
+ extension=f.extension,
135
+ size_bytes=f.size_bytes,
136
+ )
137
+ file_nodes[f.path] = node
138
+ G.add_node(f.path, **vars(node))
139
+
140
+ # Add edges from import relationships
141
+ for rel in parse_result.relationships:
142
+ if rel.source in G and rel.target in G:
143
+ G.add_edge(rel.source, rel.target, raw_import=rel.raw_import)
144
+
145
+ # Compute PageRank (importance score)
146
+ try:
147
+ pagerank = nx.pagerank(G, alpha=0.85)
148
+ except nx.PowerIterationFailedConvergence:
149
+ pagerank = {n: 1.0 / len(G.nodes) for n in G.nodes}
150
+
151
+ # Attach metrics to each node
152
+ for path, node in file_nodes.items():
153
+ node.in_degree = G.in_degree(path)
154
+ node.out_degree = G.out_degree(path)
155
+ node.centrality = pagerank.get(path, 0.0)
156
+
157
+ return RepoGraph(graph=G, nodes=file_nodes, root=scan_result.root)
158
+
159
+
160
+ if __name__ == "__main__":
161
+ import sys
162
+
163
+ target = sys.argv[1] if len(sys.argv) > 1 else "."
164
+
165
+ scan = scan_repo(target)
166
+ parse = parse_imports(scan)
167
+ repo_graph = build_graph(scan, parse)
168
+
169
+ print(repo_graph.summary())
170
+ print()
171
+ repo_graph.print_adjacency()