context-mcp-server 1.0.2 → 1.0.4
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.
- package/README.md +12 -16
- package/codegraph/__pycache__/config.cpython-313.pyc +0 -0
- package/codegraph/__pycache__/scanner.cpython-313.pyc +0 -0
- package/codegraph/__pycache__/server.cpython-313.pyc +0 -0
- package/codegraph/config.py +139 -22
- package/codegraph/extractors/__pycache__/ast_extractor.cpython-313.pyc +0 -0
- package/codegraph/extractors/__pycache__/build_extractor.cpython-313.pyc +0 -0
- package/codegraph/extractors/ast_extractor.py +392 -176
- package/codegraph/extractors/build_extractor.py +68 -0
- package/codegraph/scanner.py +5 -21
- package/codegraph/server.py +32 -229
- package/package.json +3 -1
- package/pyproject.toml +69 -0
- package/src/cli.js +2 -2
- package/src/templates/AGENTS.md +8 -15
- package/src/templates/CLAUDE.md +15 -27
- package/src/templates/GEMINI.md +7 -14
- package/uv.lock +1295 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""
|
|
2
|
+
build_extractor.py — extract key metadata from build/config files.
|
|
3
|
+
Returns a SINGLE node per file (not decomposed into functions/classes).
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def extract(abs_path: str, rel_path: str) -> list[dict]:
|
|
12
|
+
"""Return a single node for the build file with key fields in description."""
|
|
13
|
+
p = Path(abs_path)
|
|
14
|
+
name = p.name
|
|
15
|
+
meta = _read_meta(p)
|
|
16
|
+
|
|
17
|
+
node = {
|
|
18
|
+
"id": f"{rel_path}::file::{name}",
|
|
19
|
+
"name": name,
|
|
20
|
+
"type": "file",
|
|
21
|
+
"file": rel_path,
|
|
22
|
+
"description": _format_meta(meta) if meta else None,
|
|
23
|
+
}
|
|
24
|
+
return [node]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _read_meta(p: Path) -> dict:
|
|
28
|
+
try:
|
|
29
|
+
if p.name == "package.json":
|
|
30
|
+
data = json.loads(p.read_text(encoding="utf-8", errors="ignore"))
|
|
31
|
+
return {
|
|
32
|
+
"name": data.get("name"),
|
|
33
|
+
"version": data.get("version"),
|
|
34
|
+
"deps": list((data.get("dependencies") or {}).keys())[:10],
|
|
35
|
+
}
|
|
36
|
+
if p.suffix == ".toml":
|
|
37
|
+
text = p.read_text(encoding="utf-8", errors="ignore")
|
|
38
|
+
name = re.search(r'^name\s*=\s*"([^"]+)"', text, re.M)
|
|
39
|
+
version = re.search(r'^version\s*=\s*"([^"]+)"', text, re.M)
|
|
40
|
+
return {
|
|
41
|
+
"name": name.group(1) if name else None,
|
|
42
|
+
"version": version.group(1) if version else None,
|
|
43
|
+
}
|
|
44
|
+
if p.name in {"requirements.txt", "Pipfile"}:
|
|
45
|
+
lines = p.read_text(encoding="utf-8", errors="ignore").splitlines()
|
|
46
|
+
deps = [l.strip() for l in lines if l.strip() and not l.startswith("#")]
|
|
47
|
+
return {"deps": deps[:10]}
|
|
48
|
+
if p.name == "go.mod":
|
|
49
|
+
text = p.read_text(encoding="utf-8", errors="ignore")
|
|
50
|
+
module = re.search(r'^module\s+(\S+)', text, re.M)
|
|
51
|
+
go_ver = re.search(r'^go\s+(\S+)', text, re.M)
|
|
52
|
+
return {
|
|
53
|
+
"module": module.group(1) if module else None,
|
|
54
|
+
"go": go_ver.group(1) if go_ver else None,
|
|
55
|
+
}
|
|
56
|
+
except Exception:
|
|
57
|
+
pass
|
|
58
|
+
return {}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _format_meta(meta: dict) -> str | None:
|
|
62
|
+
parts = []
|
|
63
|
+
if meta.get("name"): parts.append(f"name={meta['name']}")
|
|
64
|
+
if meta.get("version"): parts.append(f"version={meta['version']}")
|
|
65
|
+
if meta.get("module"): parts.append(f"module={meta['module']}")
|
|
66
|
+
if meta.get("go"): parts.append(f"go={meta['go']}")
|
|
67
|
+
if meta.get("deps"): parts.append(f"deps=[{', '.join(meta['deps'])}]")
|
|
68
|
+
return ", ".join(parts) if parts else None
|
package/codegraph/scanner.py
CHANGED
|
@@ -7,12 +7,7 @@ from pathlib import Path
|
|
|
7
7
|
from typing import Iterator
|
|
8
8
|
|
|
9
9
|
from .cache import file_hash, get_cached_nodes, set_cached_nodes, remove_deleted, load_cache, save_cache
|
|
10
|
-
from .config import
|
|
11
|
-
DEFAULT_IGNORE, MAX_FILE_BYTES,
|
|
12
|
-
CODE_EXTENSIONS, SQL_EXTENSIONS, CONFIG_EXTENSIONS,
|
|
13
|
-
DOC_EXTENSIONS, PDF_EXTENSIONS, IMAGE_EXTENSIONS,
|
|
14
|
-
AUDIO_EXTENSIONS, VIDEO_EXTENSIONS,
|
|
15
|
-
)
|
|
10
|
+
from .config import DEFAULT_IGNORE, MAX_FILE_BYTES, SKIP_FILENAMES, SKIP_EXTENSIONS, classify_file
|
|
16
11
|
|
|
17
12
|
|
|
18
13
|
def _should_ignore(name: str, ignore: set) -> bool:
|
|
@@ -26,6 +21,9 @@ def walk_files(root: str, extra_ignore: set | None = None) -> Iterator[str]:
|
|
|
26
21
|
# Prune ignored dirs in-place so os.walk doesn't descend
|
|
27
22
|
dirnames[:] = [d for d in dirnames if d not in ignore and not d.startswith(".")]
|
|
28
23
|
for fname in filenames:
|
|
24
|
+
ext = Path(fname).suffix.lower()
|
|
25
|
+
if fname in SKIP_FILENAMES or ext in SKIP_EXTENSIONS:
|
|
26
|
+
continue
|
|
29
27
|
abs_path = os.path.join(dirpath, fname)
|
|
30
28
|
try:
|
|
31
29
|
if os.path.getsize(abs_path) > MAX_FILE_BYTES:
|
|
@@ -35,20 +33,6 @@ def walk_files(root: str, extra_ignore: set | None = None) -> Iterator[str]:
|
|
|
35
33
|
yield abs_path
|
|
36
34
|
|
|
37
35
|
|
|
38
|
-
def classify_file(path: str) -> str:
|
|
39
|
-
"""Return extraction category for a file."""
|
|
40
|
-
ext = Path(path).suffix.lower()
|
|
41
|
-
if ext in CODE_EXTENSIONS: return "code"
|
|
42
|
-
if ext in SQL_EXTENSIONS: return "sql"
|
|
43
|
-
if ext in CONFIG_EXTENSIONS: return "config"
|
|
44
|
-
if ext in DOC_EXTENSIONS: return "doc"
|
|
45
|
-
if ext in PDF_EXTENSIONS: return "pdf"
|
|
46
|
-
if ext in IMAGE_EXTENSIONS: return "image"
|
|
47
|
-
if ext in AUDIO_EXTENSIONS: return "audio"
|
|
48
|
-
if ext in VIDEO_EXTENSIONS: return "video"
|
|
49
|
-
return "unknown"
|
|
50
|
-
|
|
51
|
-
|
|
52
36
|
def scan(project_root: str, extra_ignore: set | None = None) -> dict:
|
|
53
37
|
"""
|
|
54
38
|
Walk project, diff against cache.
|
|
@@ -72,7 +56,7 @@ def scan(project_root: str, extra_ignore: set | None = None) -> dict:
|
|
|
72
56
|
rel_path = os.path.relpath(abs_path, root).replace("\\", "/")
|
|
73
57
|
existing_rel.add(rel_path)
|
|
74
58
|
category = classify_file(abs_path)
|
|
75
|
-
if category
|
|
59
|
+
if category in ("unknown", "skip"):
|
|
76
60
|
continue
|
|
77
61
|
h = file_hash(abs_path)
|
|
78
62
|
nodes = get_cached_nodes(cache, rel_path, h)
|
package/codegraph/server.py
CHANGED
|
@@ -3,13 +3,12 @@
|
|
|
3
3
|
codegraph/server.py — MCP server exposing codebase knowledge graph tools.
|
|
4
4
|
|
|
5
5
|
Tools:
|
|
6
|
-
codegraph_build
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
codegraph_path — shortest path between two concepts
|
|
6
|
+
codegraph_build — scan project, extract AST nodes, build graph (local only, no API)
|
|
7
|
+
codegraph_query — fetch details about any part of the codebase via natural language
|
|
8
|
+
codegraph_explain — look up a specific node: type, file, connections
|
|
9
|
+
codegraph_report — return full CODEGRAPH_REPORT.md
|
|
10
|
+
codegraph_nodes — list nodes of a given type
|
|
11
|
+
codegraph_path — shortest path between two concepts
|
|
13
12
|
"""
|
|
14
13
|
|
|
15
14
|
import asyncio
|
|
@@ -22,10 +21,10 @@ from mcp.server import Server
|
|
|
22
21
|
from mcp.server.stdio import stdio_server
|
|
23
22
|
from mcp.types import Tool, TextContent
|
|
24
23
|
|
|
25
|
-
from .scanner import scan
|
|
26
|
-
from .
|
|
24
|
+
from .scanner import scan
|
|
25
|
+
from .config import classify_file
|
|
26
|
+
from .cache import file_hash, set_cached_nodes, save_cache
|
|
27
27
|
from .extractors.ast_extractor import extract as ast_extract
|
|
28
|
-
from .extractors.doc_extractor import extract_text
|
|
29
28
|
from .graph.builder import build, to_json_dict, save_graph, load_graph
|
|
30
29
|
from .graph.query import answer as graph_answer, find_path
|
|
31
30
|
from .graph.clustering import detect_communities
|
|
@@ -41,10 +40,9 @@ TOOLS = [
|
|
|
41
40
|
name="codegraph_build",
|
|
42
41
|
description=(
|
|
43
42
|
"Scan a project directory and build the knowledge graph from code files. "
|
|
44
|
-
"Uses AST
|
|
43
|
+
"Uses tree-sitter AST (with regex fallback) for all code files. "
|
|
45
44
|
"Fast, local, no API key needed. "
|
|
46
|
-
"
|
|
47
|
-
"then calls codegraph_add_nodes to push them into the graph."
|
|
45
|
+
"Run once per project; rebuild whenever code changes."
|
|
48
46
|
),
|
|
49
47
|
inputSchema={
|
|
50
48
|
"type": "object",
|
|
@@ -55,69 +53,14 @@ TOOLS = [
|
|
|
55
53
|
"required": ["path"],
|
|
56
54
|
},
|
|
57
55
|
),
|
|
58
|
-
Tool(
|
|
59
|
-
name="codegraph_extract",
|
|
60
|
-
description=(
|
|
61
|
-
"Return raw content of changed doc files so the AI can read them and extract concepts. "
|
|
62
|
-
"Call this after codegraph_build. Read the returned files, extract key concepts and "
|
|
63
|
-
"relationships, then call codegraph_add_nodes with your findings. "
|
|
64
|
-
"Works with any AI — no API key required."
|
|
65
|
-
),
|
|
66
|
-
inputSchema={
|
|
67
|
-
"type": "object",
|
|
68
|
-
"properties": {
|
|
69
|
-
"path": {"type": "string", "description": "Project root (same as codegraph_build)"},
|
|
70
|
-
"limit": {"type": "integer", "description": "Max files to return per call (default 10)"},
|
|
71
|
-
},
|
|
72
|
-
"required": ["path"],
|
|
73
|
-
},
|
|
74
|
-
),
|
|
75
|
-
Tool(
|
|
76
|
-
name="codegraph_add_nodes",
|
|
77
|
-
description=(
|
|
78
|
-
"Add concept nodes extracted by the AI into the graph. "
|
|
79
|
-
"Call this after reading the output of codegraph_extract. "
|
|
80
|
-
"Each node should have: name, type, file, and optionally description and relations."
|
|
81
|
-
),
|
|
82
|
-
inputSchema={
|
|
83
|
-
"type": "object",
|
|
84
|
-
"properties": {
|
|
85
|
-
"path": {"type": "string", "description": "Project root"},
|
|
86
|
-
"nodes": {
|
|
87
|
-
"type": "array",
|
|
88
|
-
"description": "Concept nodes to add",
|
|
89
|
-
"items": {
|
|
90
|
-
"type": "object",
|
|
91
|
-
"properties": {
|
|
92
|
-
"name": {"type": "string"},
|
|
93
|
-
"type": {"type": "string", "description": "class|function|concept|service|decision|requirement"},
|
|
94
|
-
"file": {"type": "string", "description": "Relative file path this concept came from"},
|
|
95
|
-
"description": {"type": "string"},
|
|
96
|
-
"relations": {
|
|
97
|
-
"type": "array",
|
|
98
|
-
"items": {
|
|
99
|
-
"type": "object",
|
|
100
|
-
"properties": {
|
|
101
|
-
"name": {"type": "string"},
|
|
102
|
-
"relation": {"type": "string", "description": "depends-on|uses|implements|defines|documents"},
|
|
103
|
-
},
|
|
104
|
-
},
|
|
105
|
-
},
|
|
106
|
-
},
|
|
107
|
-
"required": ["name", "type", "file"],
|
|
108
|
-
},
|
|
109
|
-
},
|
|
110
|
-
},
|
|
111
|
-
"required": ["path", "nodes"],
|
|
112
|
-
},
|
|
113
|
-
),
|
|
114
56
|
Tool(
|
|
115
57
|
name="codegraph_query",
|
|
116
58
|
description=(
|
|
117
|
-
"
|
|
118
|
-
"
|
|
119
|
-
"
|
|
120
|
-
"
|
|
59
|
+
"Fetch details about any part of the codebase using a natural language question. "
|
|
60
|
+
"Searches the knowledge graph — instant, no API call. "
|
|
61
|
+
"Use for: finding functions, classes, files, understanding what exists, "
|
|
62
|
+
"what a module contains, what calls what, what imports what. "
|
|
63
|
+
"NOT for: bug investigation or tracing unexpected behavior — read the file for that."
|
|
121
64
|
),
|
|
122
65
|
inputSchema={
|
|
123
66
|
"type": "object",
|
|
@@ -132,9 +75,9 @@ TOOLS = [
|
|
|
132
75
|
Tool(
|
|
133
76
|
name="codegraph_explain",
|
|
134
77
|
description=(
|
|
135
|
-
"Look up a
|
|
136
|
-
"
|
|
137
|
-
"
|
|
78
|
+
"Look up a specific function, class, or module by name — returns its type, file location, "
|
|
79
|
+
"and all direct connections (what it depends on, what uses it). "
|
|
80
|
+
"Use when you already know the name and want its full context in the graph."
|
|
138
81
|
),
|
|
139
82
|
inputSchema={
|
|
140
83
|
"type": "object",
|
|
@@ -198,14 +141,12 @@ async def call_tool(name: str, arguments: dict):
|
|
|
198
141
|
|
|
199
142
|
|
|
200
143
|
async def _dispatch(name: str, args: dict):
|
|
201
|
-
if name == "codegraph_build":
|
|
202
|
-
if name == "
|
|
203
|
-
if name == "
|
|
204
|
-
if name == "
|
|
205
|
-
if name == "
|
|
206
|
-
if name == "
|
|
207
|
-
if name == "codegraph_nodes": return await _nodes(args)
|
|
208
|
-
if name == "codegraph_path": return await _path(args)
|
|
144
|
+
if name == "codegraph_build": return await _build(args)
|
|
145
|
+
if name == "codegraph_query": return await _query(args)
|
|
146
|
+
if name == "codegraph_explain": return await _explain(args)
|
|
147
|
+
if name == "codegraph_report": return await _report(args)
|
|
148
|
+
if name == "codegraph_nodes": return await _nodes(args)
|
|
149
|
+
if name == "codegraph_path": return await _path(args)
|
|
209
150
|
raise ValueError(f"Unknown tool: {name}")
|
|
210
151
|
|
|
211
152
|
|
|
@@ -222,9 +163,7 @@ async def _build(args: dict) -> dict:
|
|
|
222
163
|
changed = scan_result["changed"]
|
|
223
164
|
deleted = scan_result["deleted"]
|
|
224
165
|
|
|
225
|
-
# Local AST extraction — code/sql/config files only
|
|
226
166
|
all_nodes: list[dict] = []
|
|
227
|
-
pending_docs: list[str] = [] # rel_paths of changed docs/images for codegraph_extract
|
|
228
167
|
|
|
229
168
|
for nodes in cached.values():
|
|
230
169
|
all_nodes.extend(nodes)
|
|
@@ -241,9 +180,12 @@ async def _build(args: dict) -> dict:
|
|
|
241
180
|
"name": Path(rel_path).name, "type": "file", "file": rel_path}
|
|
242
181
|
set_cached_nodes(cache, rel_path, file_hash(abs_path), [node])
|
|
243
182
|
all_nodes.append(node)
|
|
244
|
-
elif cat
|
|
245
|
-
|
|
246
|
-
|
|
183
|
+
elif cat == "build":
|
|
184
|
+
from codegraph.extractors.build_extractor import extract as build_extract
|
|
185
|
+
nodes = build_extract(abs_path, rel_path)
|
|
186
|
+
set_cached_nodes(cache, rel_path, file_hash(abs_path), nodes)
|
|
187
|
+
all_nodes.extend(nodes)
|
|
188
|
+
elif cat in ("image", "audio", "video", "doc", "pdf"):
|
|
247
189
|
# Label-only — node in graph so AI can reference the file, no content extraction
|
|
248
190
|
node = {"id": f"{rel_path}::file::{Path(rel_path).name}",
|
|
249
191
|
"name": Path(rel_path).name, "type": "file", "file": rel_path}
|
|
@@ -276,147 +218,9 @@ async def _build(args: dict) -> dict:
|
|
|
276
218
|
"summary": f"Built graph: {len(graph_dict.get('nodes', []))} nodes from code files.",
|
|
277
219
|
}
|
|
278
220
|
|
|
279
|
-
if pending_docs:
|
|
280
|
-
result["pending_docs"] = len(pending_docs)
|
|
281
|
-
result["hint"] = (
|
|
282
|
-
f"{len(pending_docs)} doc/image file(s) need concept extraction. "
|
|
283
|
-
"Call codegraph_extract to get their content, then codegraph_add_nodes with your findings."
|
|
284
|
-
)
|
|
285
|
-
|
|
286
221
|
return result
|
|
287
222
|
|
|
288
223
|
|
|
289
|
-
# ── Extract (return raw content for AI to read) ───────────────────────────────
|
|
290
|
-
|
|
291
|
-
async def _extract(args: dict) -> dict:
|
|
292
|
-
root = args["path"]
|
|
293
|
-
limit = args.get("limit", 10)
|
|
294
|
-
force = args.get("force", False)
|
|
295
|
-
|
|
296
|
-
scan_result = scan(root)
|
|
297
|
-
cache = scan_result["cache"]
|
|
298
|
-
scan_root = scan_result["root"]
|
|
299
|
-
# force=True: return all files; otherwise only changed
|
|
300
|
-
if force:
|
|
301
|
-
from codegraph.scanner import walk_files, classify_file
|
|
302
|
-
candidates = {
|
|
303
|
-
os.path.relpath(p, scan_root).replace("\\", "/"): p
|
|
304
|
-
for p in walk_files(scan_root)
|
|
305
|
-
if classify_file(p) in ("doc", "code")
|
|
306
|
-
}
|
|
307
|
-
else:
|
|
308
|
-
candidates = scan_result["changed"]
|
|
309
|
-
|
|
310
|
-
files = []
|
|
311
|
-
for rel_path, abs_path in list(candidates.items()):
|
|
312
|
-
if len(files) >= limit:
|
|
313
|
-
break
|
|
314
|
-
cat = classify_file(abs_path)
|
|
315
|
-
if cat in ("doc", "code"):
|
|
316
|
-
text = extract_text(abs_path)
|
|
317
|
-
if not text:
|
|
318
|
-
continue
|
|
319
|
-
entry: dict = {"rel_path": rel_path, "type": cat, "content": text}
|
|
320
|
-
# For code files, include existing AST nodes so AI knows what to describe
|
|
321
|
-
if cat == "code":
|
|
322
|
-
cached_entry = cache.get(rel_path, {})
|
|
323
|
-
existing_nodes = cached_entry.get("nodes", [])
|
|
324
|
-
entry["existing_nodes"] = [
|
|
325
|
-
{"name": n.get("name"), "type": n.get("type")}
|
|
326
|
-
for n in existing_nodes
|
|
327
|
-
]
|
|
328
|
-
files.append(entry)
|
|
329
|
-
|
|
330
|
-
remaining = max(0, len(candidates) - limit)
|
|
331
|
-
return {
|
|
332
|
-
"files": files,
|
|
333
|
-
"returned": len(files),
|
|
334
|
-
"remaining": remaining,
|
|
335
|
-
"instruction": (
|
|
336
|
-
"For each file: read the content. "
|
|
337
|
-
"For code files, write a description for each existing_node (name + type listed). "
|
|
338
|
-
"For doc files, extract key concepts, decisions, and relationships as new nodes. "
|
|
339
|
-
"Then call codegraph_add_nodes with all nodes (include description field)."
|
|
340
|
-
),
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
# ── Add nodes (AI pushes extracted concepts in) ───────────────────────────────
|
|
345
|
-
|
|
346
|
-
async def _add_nodes(args: dict) -> dict:
|
|
347
|
-
root = args["path"]
|
|
348
|
-
nodes = args.get("nodes", [])
|
|
349
|
-
|
|
350
|
-
if not nodes:
|
|
351
|
-
return {"success": False, "message": "No nodes provided."}
|
|
352
|
-
|
|
353
|
-
graph_dict = load_graph(root) or {"nodes": [], "edges": [], "communities": [], "god_nodes": []}
|
|
354
|
-
|
|
355
|
-
# Assign IDs and merge; update description on existing nodes
|
|
356
|
-
existing_map = {n["id"]: n for n in graph_dict["nodes"]}
|
|
357
|
-
added = 0
|
|
358
|
-
updated = 0
|
|
359
|
-
for node in nodes:
|
|
360
|
-
nid = f"{node['file']}::concept::{node['name']}"
|
|
361
|
-
desc = node.get("description", "")
|
|
362
|
-
if nid in existing_map:
|
|
363
|
-
if desc and desc != existing_map[nid].get("description", ""):
|
|
364
|
-
existing_map[nid]["description"] = desc
|
|
365
|
-
updated += 1
|
|
366
|
-
# Still add edges below
|
|
367
|
-
else:
|
|
368
|
-
new_node = {
|
|
369
|
-
"id": nid,
|
|
370
|
-
"name": node["name"],
|
|
371
|
-
"type": node.get("type", "concept"),
|
|
372
|
-
"file": node["file"],
|
|
373
|
-
"description": desc,
|
|
374
|
-
}
|
|
375
|
-
graph_dict["nodes"].append(new_node)
|
|
376
|
-
existing_map[nid] = new_node
|
|
377
|
-
added += 1
|
|
378
|
-
|
|
379
|
-
# Also try to enrich AST nodes (different ID pattern: file::type::name)
|
|
380
|
-
for id_pattern in [
|
|
381
|
-
f"{node['file']}::{node.get('type','function')}::{node['name']}",
|
|
382
|
-
f"{node['file']}::function::{node['name']}",
|
|
383
|
-
f"{node['file']}::class::{node['name']}",
|
|
384
|
-
f"{node['file']}::module::{node['name']}",
|
|
385
|
-
]:
|
|
386
|
-
if id_pattern in existing_map and desc:
|
|
387
|
-
if not existing_map[id_pattern].get("description"):
|
|
388
|
-
existing_map[id_pattern]["description"] = desc
|
|
389
|
-
updated += 1
|
|
390
|
-
|
|
391
|
-
# Add relation edges
|
|
392
|
-
for rel in node.get("relations", []):
|
|
393
|
-
graph_dict["edges"].append({
|
|
394
|
-
"from": nid,
|
|
395
|
-
"to": rel["name"],
|
|
396
|
-
"relation": rel.get("relation", "relates-to"),
|
|
397
|
-
"confidence": "EXTRACTED",
|
|
398
|
-
})
|
|
399
|
-
|
|
400
|
-
# Persist descriptions to semantic cache (never overwritten by rebuild)
|
|
401
|
-
by_file: dict = {}
|
|
402
|
-
for node in nodes:
|
|
403
|
-
if node.get("description"):
|
|
404
|
-
by_file.setdefault(node["file"], []).append(node)
|
|
405
|
-
if by_file:
|
|
406
|
-
save_semantic_cache(root, by_file)
|
|
407
|
-
|
|
408
|
-
save_graph(root, graph_dict)
|
|
409
|
-
generate_report(graph_dict, root)
|
|
410
|
-
|
|
411
|
-
return {
|
|
412
|
-
"success": True,
|
|
413
|
-
"nodes_added": added,
|
|
414
|
-
"nodes_updated": updated,
|
|
415
|
-
"total_nodes": len(graph_dict["nodes"]),
|
|
416
|
-
"message": f"Added {added}, updated {updated} node description(s).",
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
|
|
420
224
|
# ── Query / Report / Nodes / Path ─────────────────────────────────────────────
|
|
421
225
|
|
|
422
226
|
async def _query(args: dict) -> dict:
|
|
@@ -466,8 +270,7 @@ async def _explain(args: dict) -> dict:
|
|
|
466
270
|
"description": match.get("description") or None,
|
|
467
271
|
"depends_on": depends_on[:20],
|
|
468
272
|
"used_by": used_by[:20],
|
|
469
|
-
"hint": None
|
|
470
|
-
"No description yet. Call codegraph_extract → codegraph_add_nodes to enrich.",
|
|
273
|
+
"hint": None,
|
|
471
274
|
}
|
|
472
275
|
|
|
473
276
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-mcp-server",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Persistent AI memory + codebase knowledge graph MCP server. Works across Claude Code, Cursor, Gemini CLI, Codex, Windsurf, VS Code Copilot, Claude.ai, and ChatGPT.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
"files": [
|
|
22
22
|
"src/",
|
|
23
23
|
"codegraph/",
|
|
24
|
+
"pyproject.toml",
|
|
25
|
+
"uv.lock",
|
|
24
26
|
"README.md"
|
|
25
27
|
],
|
|
26
28
|
"keywords": [
|
package/pyproject.toml
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "codegraph-mcp"
|
|
7
|
+
version = "1.0.4"
|
|
8
|
+
description = "Codebase knowledge graph MCP server — AST extraction, graph queries, community detection"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
keywords = ["mcp", "ai", "codegraph", "knowledge-graph", "ast"]
|
|
13
|
+
dependencies = [
|
|
14
|
+
"mcp>=1.0.0",
|
|
15
|
+
"networkx>=3.0",
|
|
16
|
+
"pymupdf>=1.24",
|
|
17
|
+
"tree-sitter>=0.23.0",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.optional-dependencies]
|
|
21
|
+
treesitter = [
|
|
22
|
+
"tree-sitter>=0.23.0",
|
|
23
|
+
"tree-sitter-python",
|
|
24
|
+
"tree-sitter-javascript",
|
|
25
|
+
"tree-sitter-typescript",
|
|
26
|
+
"tree-sitter-go",
|
|
27
|
+
"tree-sitter-rust",
|
|
28
|
+
"tree-sitter-java",
|
|
29
|
+
"tree-sitter-c",
|
|
30
|
+
"tree-sitter-cpp",
|
|
31
|
+
"tree-sitter-c-sharp",
|
|
32
|
+
"tree-sitter-ruby",
|
|
33
|
+
"tree-sitter-php",
|
|
34
|
+
"tree-sitter-swift",
|
|
35
|
+
"tree-sitter-lua",
|
|
36
|
+
"tree-sitter-kotlin",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[project.scripts]
|
|
40
|
+
codegraph-mcp = "codegraph.server:main"
|
|
41
|
+
|
|
42
|
+
[tool.hatch.build.targets.wheel]
|
|
43
|
+
packages = ["codegraph"]
|
|
44
|
+
|
|
45
|
+
# ── uv ────────────────────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
[dependency-groups]
|
|
48
|
+
dev = [
|
|
49
|
+
"pytest>=8.0",
|
|
50
|
+
"pytest-asyncio>=0.23",
|
|
51
|
+
"ruff>=0.4",
|
|
52
|
+
"mypy>=1.10",
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
# ── ruff ─────────────────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
[tool.ruff]
|
|
58
|
+
target-version = "py311"
|
|
59
|
+
line-length = 100
|
|
60
|
+
|
|
61
|
+
[tool.ruff.lint]
|
|
62
|
+
select = ["E", "F", "I"]
|
|
63
|
+
ignore = ["E501"]
|
|
64
|
+
|
|
65
|
+
# ── pytest ────────────────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
[tool.pytest.ini_options]
|
|
68
|
+
asyncio_mode = "auto"
|
|
69
|
+
testpaths = ["tests"]
|
package/src/cli.js
CHANGED
|
@@ -637,7 +637,7 @@ function cmdInstall(args) {
|
|
|
637
637
|
const venvPath = join(pkgRootInit, '.venv');
|
|
638
638
|
spawnSync('cmd', ['/c', 'rmdir', '/s', '/q', venvPath], { encoding: 'utf8' });
|
|
639
639
|
}
|
|
640
|
-
const sync2 = spawnSync('uv', ['sync', '--no-dev'], {
|
|
640
|
+
const sync2 = spawnSync('uv', ['--directory', pkgRootInit, 'sync', '--no-dev'], { encoding: 'utf8' });
|
|
641
641
|
if (sync2.status !== 0) {
|
|
642
642
|
console.log(` ${bad('✗')} uv sync failed:\n${faint((sync2.stderr || sync2.stdout || '').trim())}`);
|
|
643
643
|
} else {
|
|
@@ -696,7 +696,7 @@ function cmdInstall(args) {
|
|
|
696
696
|
// Package root is one level up from src/
|
|
697
697
|
const __dirname_cli = dirname(fileURLToPath(import.meta.url));
|
|
698
698
|
const pkgRoot = join(__dirname_cli, '..');
|
|
699
|
-
const sync = spawnSync('uv', ['sync', '--no-dev'], {
|
|
699
|
+
const sync = spawnSync('uv', ['--directory', pkgRoot, 'sync', '--no-dev'], { encoding: 'utf8' });
|
|
700
700
|
if (sync.status !== 0) {
|
|
701
701
|
console.log(` ${bad('✗')} uv sync failed:\n${faint((sync.stderr || sync.stdout || '').trim())}`);
|
|
702
702
|
} else {
|
package/src/templates/AGENTS.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Context-MCP — Codex CLI Usage Guide
|
|
2
2
|
|
|
3
3
|
Persistent memory + codebase knowledge graph.
|
|
4
|
-
Every conversation starts with `context.resume`. Every
|
|
4
|
+
Every conversation starts with `context.resume`. Every codebase question uses `codegraph_query`. Files only read for bugs/logic.
|
|
5
5
|
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -37,21 +37,15 @@ Always pass `project`. Auto-compact fires at >50 entries.
|
|
|
37
37
|
|
|
38
38
|
## 3. CodeGraph Pipeline
|
|
39
39
|
|
|
40
|
-
### Step 1 — Build (once,
|
|
40
|
+
### Step 1 — Build (once, fast, local)
|
|
41
41
|
```
|
|
42
42
|
codegraph_build(path) → AST graph: functions, classes, imports, edges
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
### Step 2 —
|
|
45
|
+
### Step 2 — Query (free, instant)
|
|
46
46
|
```
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
### Step 3 — Query (free, instant)
|
|
52
|
-
```
|
|
53
|
-
codegraph_query(path, question) → NODE/EDGE subgraph (token_budget default 2000)
|
|
54
|
-
codegraph_explain(path, node) → single node + neighbors
|
|
47
|
+
codegraph_query(path, question) → fetch any details about the codebase
|
|
48
|
+
codegraph_explain(path, node) → single node: type, file, connections
|
|
55
49
|
codegraph_path(path, from, to) → shortest path
|
|
56
50
|
codegraph_nodes(path, type) → list nodes by type
|
|
57
51
|
codegraph_report(path) → full graph analysis
|
|
@@ -61,8 +55,8 @@ codegraph_report(path) → full graph analysis
|
|
|
61
55
|
|
|
62
56
|
## 4. Graph vs File
|
|
63
57
|
|
|
64
|
-
**Graph** —
|
|
65
|
-
**File** — bugs, logic, tracing behavior.
|
|
58
|
+
**Graph** — use for any question about what exists: finding functions, classes, files, dependencies, callers, imports, paths between concepts.
|
|
59
|
+
**File** — bugs, logic, tracing unexpected behavior.
|
|
66
60
|
|
|
67
61
|
---
|
|
68
62
|
|
|
@@ -71,6 +65,5 @@ codegraph_report(path) → full graph analysis
|
|
|
71
65
|
1. **`context.resume` first** — before any tool or response
|
|
72
66
|
2. **Always pass `project`**
|
|
73
67
|
3. **`search` before asking** — if user references past work
|
|
74
|
-
4. **`codegraph_query` before reading files**
|
|
68
|
+
4. **`codegraph_query` before reading files** — graph is faster and cheaper
|
|
75
69
|
5. **Read files for bugs/logic only**
|
|
76
|
-
6. **Enrich once** — descriptions persist forever
|