context-mcp-server 1.0.1
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 +464 -0
- package/codegraph/__init__.py +0 -0
- package/codegraph/__main__.py +24 -0
- package/codegraph/__pycache__/__init__.cpython-313.pyc +0 -0
- package/codegraph/__pycache__/__main__.cpython-313.pyc +0 -0
- package/codegraph/__pycache__/cache.cpython-313.pyc +0 -0
- package/codegraph/__pycache__/config.cpython-313.pyc +0 -0
- package/codegraph/__pycache__/report.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/cache.py +137 -0
- package/codegraph/config.py +31 -0
- package/codegraph/extractors/__init__.py +0 -0
- package/codegraph/extractors/__pycache__/__init__.cpython-313.pyc +0 -0
- package/codegraph/extractors/__pycache__/ast_extractor.cpython-313.pyc +0 -0
- package/codegraph/extractors/__pycache__/audio_extractor.cpython-313.pyc +0 -0
- package/codegraph/extractors/__pycache__/doc_extractor.cpython-313.pyc +0 -0
- package/codegraph/extractors/__pycache__/image_extractor.cpython-313.pyc +0 -0
- package/codegraph/extractors/ast_extractor.py +222 -0
- package/codegraph/extractors/audio_extractor.py +8 -0
- package/codegraph/extractors/doc_extractor.py +34 -0
- package/codegraph/extractors/image_extractor.py +26 -0
- package/codegraph/graph/__init__.py +0 -0
- package/codegraph/graph/__pycache__/__init__.cpython-313.pyc +0 -0
- package/codegraph/graph/__pycache__/builder.cpython-313.pyc +0 -0
- package/codegraph/graph/__pycache__/clustering.cpython-313.pyc +0 -0
- package/codegraph/graph/__pycache__/query.cpython-313.pyc +0 -0
- package/codegraph/graph/builder.py +145 -0
- package/codegraph/graph/clustering.py +40 -0
- package/codegraph/graph/query.py +283 -0
- package/codegraph/report.py +115 -0
- package/codegraph/scanner.py +92 -0
- package/codegraph/server.py +514 -0
- package/package.json +62 -0
- package/src/cli.js +1010 -0
- package/src/config.js +89 -0
- package/src/db.js +786 -0
- package/src/guard.js +20 -0
- package/src/hooks/autoContext.js +17 -0
- package/src/hooks/autoLink.js +7 -0
- package/src/http.js +765 -0
- package/src/index.js +47 -0
- package/src/search.js +50 -0
- package/src/server.js +80 -0
- package/src/summarizer.js +124 -0
- package/src/templates/AGENTS.md +76 -0
- package/src/templates/CLAUDE.md +94 -0
- package/src/templates/GEMINI.md +76 -0
- package/src/templates/cursor-rules.mdc +41 -0
- package/src/templates/windsurf-rules.md +35 -0
- package/src/tools/codegraph.js +215 -0
- package/src/tools/context.js +188 -0
- package/src/tools/discussion.js +123 -0
- package/src/tools/errorCheck.js +65 -0
- package/src/tools/fileTools.js +185 -0
- package/src/tools/gitTools.js +259 -0
- package/src/tools/search.js +55 -0
- package/src/vector.js +153 -0
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
codegraph/server.py — MCP server exposing codebase knowledge graph tools.
|
|
4
|
+
|
|
5
|
+
Tools:
|
|
6
|
+
codegraph_build — scan project, extract AST nodes, build graph (local only, no API)
|
|
7
|
+
codegraph_extract — return raw doc content for the AI to read and extract concepts from
|
|
8
|
+
codegraph_add_nodes — AI pushes extracted concept nodes back into the graph
|
|
9
|
+
codegraph_query — natural language question → graph traversal answer
|
|
10
|
+
codegraph_report — return full CODEGRAPH_REPORT.md
|
|
11
|
+
codegraph_nodes — list nodes of a given type
|
|
12
|
+
codegraph_path — shortest path between two concepts
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import asyncio
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import time
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from mcp.server import Server
|
|
22
|
+
from mcp.server.stdio import stdio_server
|
|
23
|
+
from mcp.types import Tool, TextContent
|
|
24
|
+
|
|
25
|
+
from .scanner import scan, classify_file
|
|
26
|
+
from .cache import file_hash, set_cached_nodes, save_cache, save_semantic_cache
|
|
27
|
+
from .extractors.ast_extractor import extract as ast_extract
|
|
28
|
+
from .extractors.doc_extractor import extract_text
|
|
29
|
+
from .graph.builder import build, to_json_dict, save_graph, load_graph
|
|
30
|
+
from .graph.query import answer as graph_answer, find_path
|
|
31
|
+
from .graph.clustering import detect_communities
|
|
32
|
+
from .report import generate as generate_report
|
|
33
|
+
|
|
34
|
+
app = Server("codegraph")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ── Tool definitions ──────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
TOOLS = [
|
|
40
|
+
Tool(
|
|
41
|
+
name="codegraph_build",
|
|
42
|
+
description=(
|
|
43
|
+
"Scan a project directory and build the knowledge graph from code files. "
|
|
44
|
+
"Uses AST extraction (with regex fallback) for all code files. "
|
|
45
|
+
"Fast, local, no API key needed. "
|
|
46
|
+
"For docs and images, call codegraph_extract afterward — the AI reads and extracts concepts, "
|
|
47
|
+
"then calls codegraph_add_nodes to push them into the graph."
|
|
48
|
+
),
|
|
49
|
+
inputSchema={
|
|
50
|
+
"type": "object",
|
|
51
|
+
"properties": {
|
|
52
|
+
"path": {"type": "string", "description": "Absolute path to project root"},
|
|
53
|
+
"cluster": {"type": "boolean", "description": "Run community detection after build (default true)"},
|
|
54
|
+
},
|
|
55
|
+
"required": ["path"],
|
|
56
|
+
},
|
|
57
|
+
),
|
|
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
|
+
Tool(
|
|
115
|
+
name="codegraph_query",
|
|
116
|
+
description=(
|
|
117
|
+
"Ask a structural question about the codebase. Pure graph traversal — instant, no API call. "
|
|
118
|
+
"Returns structured NODE/EDGE text truncated to token_budget. "
|
|
119
|
+
"Good for: dependencies, callers, module relationships. "
|
|
120
|
+
"NOT for: bug investigation or understanding code logic — read the file for that."
|
|
121
|
+
),
|
|
122
|
+
inputSchema={
|
|
123
|
+
"type": "object",
|
|
124
|
+
"properties": {
|
|
125
|
+
"path": {"type": "string", "description": "Project root"},
|
|
126
|
+
"question": {"type": "string", "description": "Natural language question"},
|
|
127
|
+
"token_budget": {"type": "integer", "description": "Max tokens in response (default 2000)"},
|
|
128
|
+
},
|
|
129
|
+
"required": ["path", "question"],
|
|
130
|
+
},
|
|
131
|
+
),
|
|
132
|
+
Tool(
|
|
133
|
+
name="codegraph_explain",
|
|
134
|
+
description=(
|
|
135
|
+
"Look up a node by name — returns description, type, file, and direct neighbors. "
|
|
136
|
+
"Use to understand what a specific function/class/module does and how it connects. "
|
|
137
|
+
"Descriptions are AI-written via codegraph_add_nodes."
|
|
138
|
+
),
|
|
139
|
+
inputSchema={
|
|
140
|
+
"type": "object",
|
|
141
|
+
"properties": {
|
|
142
|
+
"path": {"type": "string", "description": "Project root"},
|
|
143
|
+
"node": {"type": "string", "description": "Node name or partial name"},
|
|
144
|
+
},
|
|
145
|
+
"required": ["path", "node"],
|
|
146
|
+
},
|
|
147
|
+
),
|
|
148
|
+
Tool(
|
|
149
|
+
name="codegraph_report",
|
|
150
|
+
description="Return CODEGRAPH_REPORT.md — god nodes, clusters, surprising connections, suggested questions.",
|
|
151
|
+
inputSchema={
|
|
152
|
+
"type": "object",
|
|
153
|
+
"properties": {"path": {"type": "string"}},
|
|
154
|
+
"required": ["path"],
|
|
155
|
+
},
|
|
156
|
+
),
|
|
157
|
+
Tool(
|
|
158
|
+
name="codegraph_nodes",
|
|
159
|
+
description="List all nodes of a given type in the graph.",
|
|
160
|
+
inputSchema={
|
|
161
|
+
"type": "object",
|
|
162
|
+
"properties": {
|
|
163
|
+
"path": {"type": "string"},
|
|
164
|
+
"type": {"type": "string", "enum": ["class", "function", "module", "concept", "service", "file", "struct", "table"]},
|
|
165
|
+
"limit": {"type": "integer", "description": "Max results (default 50)"},
|
|
166
|
+
},
|
|
167
|
+
"required": ["path", "type"],
|
|
168
|
+
},
|
|
169
|
+
),
|
|
170
|
+
Tool(
|
|
171
|
+
name="codegraph_path",
|
|
172
|
+
description="Find the shortest relationship path between two concepts in the graph.",
|
|
173
|
+
inputSchema={
|
|
174
|
+
"type": "object",
|
|
175
|
+
"properties": {
|
|
176
|
+
"path": {"type": "string"},
|
|
177
|
+
"from": {"type": "string"},
|
|
178
|
+
"to": {"type": "string"},
|
|
179
|
+
},
|
|
180
|
+
"required": ["path", "from", "to"],
|
|
181
|
+
},
|
|
182
|
+
),
|
|
183
|
+
]
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@app.list_tools()
|
|
187
|
+
async def list_tools():
|
|
188
|
+
return TOOLS
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@app.call_tool()
|
|
192
|
+
async def call_tool(name: str, arguments: dict):
|
|
193
|
+
try:
|
|
194
|
+
result = await _dispatch(name, arguments)
|
|
195
|
+
return [TextContent(type="text", text=json.dumps(result, indent=2))]
|
|
196
|
+
except Exception as e:
|
|
197
|
+
return [TextContent(type="text", text=json.dumps({"error": str(e)}))]
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
async def _dispatch(name: str, args: dict):
|
|
201
|
+
if name == "codegraph_build": return await _build(args)
|
|
202
|
+
if name == "codegraph_extract": return await _extract(args)
|
|
203
|
+
if name == "codegraph_add_nodes": return await _add_nodes(args)
|
|
204
|
+
if name == "codegraph_query": return await _query(args)
|
|
205
|
+
if name == "codegraph_explain": return await _explain(args)
|
|
206
|
+
if name == "codegraph_report": return await _report(args)
|
|
207
|
+
if name == "codegraph_nodes": return await _nodes(args)
|
|
208
|
+
if name == "codegraph_path": return await _path(args)
|
|
209
|
+
raise ValueError(f"Unknown tool: {name}")
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
# ── Build ─────────────────────────────────────────────────────────────────────
|
|
213
|
+
|
|
214
|
+
async def _build(args: dict) -> dict:
|
|
215
|
+
root = args["path"]
|
|
216
|
+
do_cluster = args.get("cluster", True)
|
|
217
|
+
t0 = time.time()
|
|
218
|
+
|
|
219
|
+
scan_result = scan(root)
|
|
220
|
+
cache = scan_result["cache"]
|
|
221
|
+
cached = scan_result["cached"]
|
|
222
|
+
changed = scan_result["changed"]
|
|
223
|
+
deleted = scan_result["deleted"]
|
|
224
|
+
|
|
225
|
+
# Local AST extraction — code/sql/config files only
|
|
226
|
+
all_nodes: list[dict] = []
|
|
227
|
+
pending_docs: list[str] = [] # rel_paths of changed docs/images for codegraph_extract
|
|
228
|
+
|
|
229
|
+
for nodes in cached.values():
|
|
230
|
+
all_nodes.extend(nodes)
|
|
231
|
+
|
|
232
|
+
for rel_path, abs_path in changed.items():
|
|
233
|
+
cat = classify_file(abs_path)
|
|
234
|
+
if cat in ("code", "sql"):
|
|
235
|
+
nodes = ast_extract(abs_path, rel_path)
|
|
236
|
+
set_cached_nodes(cache, rel_path, file_hash(abs_path), nodes)
|
|
237
|
+
all_nodes.extend(nodes)
|
|
238
|
+
elif cat == "config":
|
|
239
|
+
# Label config files as a single node — don't decompose every key
|
|
240
|
+
node = {"id": f"{rel_path}::file::{Path(rel_path).name}",
|
|
241
|
+
"name": Path(rel_path).name, "type": "file", "file": rel_path}
|
|
242
|
+
set_cached_nodes(cache, rel_path, file_hash(abs_path), [node])
|
|
243
|
+
all_nodes.append(node)
|
|
244
|
+
elif cat in ("doc", "pdf"):
|
|
245
|
+
pending_docs.append(rel_path)
|
|
246
|
+
elif cat in ("image", "audio", "video"):
|
|
247
|
+
# Label-only — node in graph so AI can reference the file, no content extraction
|
|
248
|
+
node = {"id": f"{rel_path}::file::{Path(rel_path).name}",
|
|
249
|
+
"name": Path(rel_path).name, "type": "file", "file": rel_path}
|
|
250
|
+
set_cached_nodes(cache, rel_path, file_hash(abs_path), [node])
|
|
251
|
+
all_nodes.append(node)
|
|
252
|
+
|
|
253
|
+
G = build(all_nodes)
|
|
254
|
+
communities = []
|
|
255
|
+
if do_cluster:
|
|
256
|
+
try:
|
|
257
|
+
communities = detect_communities(G)
|
|
258
|
+
except Exception:
|
|
259
|
+
pass
|
|
260
|
+
|
|
261
|
+
graph_dict = to_json_dict(G)
|
|
262
|
+
save_graph(root, graph_dict)
|
|
263
|
+
generate_report(graph_dict, root)
|
|
264
|
+
save_cache(root, cache)
|
|
265
|
+
|
|
266
|
+
elapsed_ms = int((time.time() - t0) * 1000)
|
|
267
|
+
result = {
|
|
268
|
+
"success": True,
|
|
269
|
+
"nodes": len(graph_dict.get("nodes", [])),
|
|
270
|
+
"edges": len(graph_dict.get("edges", [])),
|
|
271
|
+
"communities": len(communities),
|
|
272
|
+
"cached": len(cached),
|
|
273
|
+
"changed": len(changed),
|
|
274
|
+
"deleted": len(deleted),
|
|
275
|
+
"time_ms": elapsed_ms,
|
|
276
|
+
"summary": f"Built graph: {len(graph_dict.get('nodes', []))} nodes from code files.",
|
|
277
|
+
}
|
|
278
|
+
|
|
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
|
+
return result
|
|
287
|
+
|
|
288
|
+
|
|
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
|
+
# ── Query / Report / Nodes / Path ─────────────────────────────────────────────
|
|
421
|
+
|
|
422
|
+
async def _query(args: dict) -> dict:
|
|
423
|
+
graph_dict = load_graph(args["path"])
|
|
424
|
+
if not graph_dict:
|
|
425
|
+
raise ValueError("No graph found. Run codegraph_build first.")
|
|
426
|
+
return graph_answer(args["question"], graph_dict, token_budget=args.get("token_budget", 2000))
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
async def _explain(args: dict) -> dict:
|
|
430
|
+
graph_dict = load_graph(args["path"])
|
|
431
|
+
if not graph_dict:
|
|
432
|
+
raise ValueError("No graph found. Run codegraph_build first.")
|
|
433
|
+
|
|
434
|
+
query = args["node"].lower()
|
|
435
|
+
nodes = graph_dict.get("nodes", [])
|
|
436
|
+
edges = graph_dict.get("edges", [])
|
|
437
|
+
|
|
438
|
+
match = next((n for n in nodes if n.get("name", "").lower() == query), None)
|
|
439
|
+
if not match:
|
|
440
|
+
match = next((n for n in nodes if query in n.get("name", "").lower()), None)
|
|
441
|
+
if not match:
|
|
442
|
+
candidates = [n["name"] for n in nodes if query in n.get("id", "").lower()]
|
|
443
|
+
return {"found": False, "query": args["node"],
|
|
444
|
+
"message": f"No node matching '{args['node']}'.",
|
|
445
|
+
"suggestions": candidates[:10]}
|
|
446
|
+
|
|
447
|
+
nid = match["id"]
|
|
448
|
+
depends_on, used_by = [], []
|
|
449
|
+
for e in edges:
|
|
450
|
+
if e.get("from") == nid:
|
|
451
|
+
t = next((n for n in nodes if n.get("id") == e.get("to")), None)
|
|
452
|
+
depends_on.append({"name": t["name"] if t else e["to"],
|
|
453
|
+
"file": t.get("file","") if t else "",
|
|
454
|
+
"relation": e.get("relation","→")})
|
|
455
|
+
elif e.get("to") == nid:
|
|
456
|
+
s = next((n for n in nodes if n.get("id") == e.get("from")), None)
|
|
457
|
+
used_by.append({"name": s["name"] if s else e["from"],
|
|
458
|
+
"file": s.get("file","") if s else "",
|
|
459
|
+
"relation": e.get("relation","→")})
|
|
460
|
+
|
|
461
|
+
return {
|
|
462
|
+
"found": True,
|
|
463
|
+
"name": match.get("name"),
|
|
464
|
+
"type": match.get("type"),
|
|
465
|
+
"file": match.get("file"),
|
|
466
|
+
"description": match.get("description") or None,
|
|
467
|
+
"depends_on": depends_on[:20],
|
|
468
|
+
"used_by": used_by[:20],
|
|
469
|
+
"hint": None if match.get("description") else
|
|
470
|
+
"No description yet. Call codegraph_extract → codegraph_add_nodes to enrich.",
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
async def _report(args: dict) -> dict:
|
|
475
|
+
report_path = Path(args["path"]) / "codegraph-cache" / "CODEGRAPH_REPORT.md"
|
|
476
|
+
if report_path.exists():
|
|
477
|
+
return {"content": report_path.read_text(encoding="utf-8")}
|
|
478
|
+
graph_dict = load_graph(args["path"])
|
|
479
|
+
if not graph_dict:
|
|
480
|
+
raise ValueError("No graph found. Run codegraph_build first.")
|
|
481
|
+
return {"content": generate_report(graph_dict, args["path"])}
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
async def _nodes(args: dict) -> dict:
|
|
485
|
+
graph_dict = load_graph(args["path"])
|
|
486
|
+
if not graph_dict:
|
|
487
|
+
raise ValueError("No graph found. Run codegraph_build first.")
|
|
488
|
+
node_type = args["type"]
|
|
489
|
+
limit = args.get("limit", 50)
|
|
490
|
+
matched = [n for n in graph_dict.get("nodes", []) if n.get("type") == node_type]
|
|
491
|
+
return {"type": node_type, "count": len(matched), "nodes": matched[:limit]}
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
async def _path(args: dict) -> dict:
|
|
495
|
+
graph_dict = load_graph(args["path"])
|
|
496
|
+
if not graph_dict:
|
|
497
|
+
raise ValueError("No graph found. Run codegraph_build first.")
|
|
498
|
+
return find_path(args["from"], args["to"], graph_dict)
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
# ── Entry point ───────────────────────────────────────────────────────────────
|
|
502
|
+
|
|
503
|
+
async def _async_main():
|
|
504
|
+
async with stdio_server() as (read_stream, write_stream):
|
|
505
|
+
await app.run(read_stream, write_stream, app.create_initialization_options())
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def main():
|
|
509
|
+
"""Sync entry point — required by pyproject.toml [project.scripts]."""
|
|
510
|
+
asyncio.run(_async_main())
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
if __name__ == "__main__":
|
|
514
|
+
main()
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "context-mcp-server",
|
|
3
|
+
"version": "1.0.1",
|
|
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
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"context-mcp": "./src/index.js",
|
|
8
|
+
"context-mcp-http": "./src/http.js",
|
|
9
|
+
"ctx": "./src/cli.js"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"mcp": "node src/index.js",
|
|
13
|
+
"mcp-server": "node src/http.js",
|
|
14
|
+
"cli": "node src/cli.js",
|
|
15
|
+
"check": "node --check src/index.js && node --check src/server.js && node --check src/db.js && node --check src/vector.js && node --check src/summarizer.js && node --check src/search.js && node --check src/config.js && node --check src/cli.js && node --check src/http.js && node --check src/tools/context.js && node --check src/tools/search.js && node --check src/tools/discussion.js && node --check src/tools/errorCheck.js && node --check src/tools/fileTools.js && node --check src/tools/gitTools.js && node --check src/tools/codegraph.js && node --check src/hooks/autoLink.js && node --check src/hooks/autoContext.js",
|
|
16
|
+
"check-mcp": "npm run check",
|
|
17
|
+
"test": "node --test",
|
|
18
|
+
"prepublishOnly": "npm run check"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"src/",
|
|
22
|
+
"codegraph/",
|
|
23
|
+
"README.md"
|
|
24
|
+
],
|
|
25
|
+
"keywords": [
|
|
26
|
+
"mcp",
|
|
27
|
+
"ai",
|
|
28
|
+
"memory",
|
|
29
|
+
"context",
|
|
30
|
+
"claude",
|
|
31
|
+
"cursor",
|
|
32
|
+
"gemini",
|
|
33
|
+
"codex",
|
|
34
|
+
"windsurf",
|
|
35
|
+
"knowledge-graph",
|
|
36
|
+
"codegraph",
|
|
37
|
+
"modelcontextprotocol",
|
|
38
|
+
"persistent-memory",
|
|
39
|
+
"ai-tools"
|
|
40
|
+
],
|
|
41
|
+
"author": "Vibhas Dutta",
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"homepage": "https://github.com/vibhasdutta/context-mcp#readme",
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "git+https://github.com/vibhasdutta/context-mcp.git"
|
|
47
|
+
},
|
|
48
|
+
"bugs": {
|
|
49
|
+
"url": "https://github.com/vibhasdutta/context-mcp/issues"
|
|
50
|
+
},
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">=18.0.0"
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
56
|
+
},
|
|
57
|
+
"optionalDependencies": {
|
|
58
|
+
"keytar": "^7.9.0",
|
|
59
|
+
"ws": "^8.18.0"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {}
|
|
62
|
+
}
|