codecortex-context-engine 0.1.0a1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codecortex/__init__.py +3 -0
- codecortex/architecture/__init__.py +23 -0
- codecortex/architecture/drift.py +182 -0
- codecortex/architecture/inference.py +160 -0
- codecortex/backends/__init__.py +35 -0
- codecortex/backends/base.py +38 -0
- codecortex/backends/context.py +118 -0
- codecortex/backends/contracts.py +65 -0
- codecortex/backends/factory.py +60 -0
- codecortex/backends/graph.py +107 -0
- codecortex/backends/manager.py +303 -0
- codecortex/backends/mcp_client.py +196 -0
- codecortex/backends/pool.py +128 -0
- codecortex/backends/spec.py +83 -0
- codecortex/backends/symbols.py +189 -0
- codecortex/benchmark.py +211 -0
- codecortex/cli.py +409 -0
- codecortex/config.py +27 -0
- codecortex/context/__init__.py +13 -0
- codecortex/context/budget.py +62 -0
- codecortex/context/integrated.py +60 -0
- codecortex/context/pipeline.py +223 -0
- codecortex/core/__init__.py +1 -0
- codecortex/core/contracts.py +45 -0
- codecortex/core/errors.py +17 -0
- codecortex/core/models.py +69 -0
- codecortex/dashboard.py +259 -0
- codecortex/editing.py +41 -0
- codecortex/engines/__init__.py +5 -0
- codecortex/engines/builtin/__init__.py +5 -0
- codecortex/engines/builtin/factory.py +26 -0
- codecortex/engines/builtin/memory.py +35 -0
- codecortex/engines/builtin/repository.py +73 -0
- codecortex/engines/builtin/symbols.py +89 -0
- codecortex/engines/builtin/validation.py +54 -0
- codecortex/engines/registry.py +26 -0
- codecortex/entrypoint.py +266 -0
- codecortex/evaluation/__init__.py +55 -0
- codecortex/evaluation/external.py +265 -0
- codecortex/evaluation/production.py +670 -0
- codecortex/evaluation/regression.py +188 -0
- codecortex/gateway.py +38 -0
- codecortex/git_intelligence.py +252 -0
- codecortex/indexing/__init__.py +6 -0
- codecortex/indexing/graph.py +78 -0
- codecortex/indexing/impact.py +127 -0
- codecortex/indexing/incremental.py +163 -0
- codecortex/indexing/incremental_graph.py +189 -0
- codecortex/indexing/indexer.py +172 -0
- codecortex/indexing/relationships.py +179 -0
- codecortex/indexing/resolution.py +88 -0
- codecortex/integrations/__init__.py +5 -0
- codecortex/integrations/agents.py +235 -0
- codecortex/interfaces/__init__.py +1 -0
- codecortex/interfaces/mcp_bridge.py +66 -0
- codecortex/languages/__init__.py +5 -0
- codecortex/languages/native.py +166 -0
- codecortex/languages/registry.py +232 -0
- codecortex/mcp/__init__.py +5 -0
- codecortex/mcp/extended.py +114 -0
- codecortex/mcp/server.py +473 -0
- codecortex/memory/__init__.py +11 -0
- codecortex/memory/json_store.py +52 -0
- codecortex/memory/knowledge.py +193 -0
- codecortex/memory/team_store.py +193 -0
- codecortex/orchestrator.py +153 -0
- codecortex/pr_intelligence.py +214 -0
- codecortex/retrieval/__init__.py +16 -0
- codecortex/retrieval/hybrid.py +67 -0
- codecortex/retrieval/index.py +135 -0
- codecortex/retrieval/providers.py +67 -0
- codecortex/retrieval/repository.py +94 -0
- codecortex/router/__init__.py +5 -0
- codecortex/router/router.py +79 -0
- codecortex/runtime.py +69 -0
- codecortex/setup.py +100 -0
- codecortex/symbols/__init__.py +5 -0
- codecortex/symbols/providers.py +192 -0
- codecortex/telemetry/__init__.py +5 -0
- codecortex/telemetry/collector.py +43 -0
- codecortex/tracing/__init__.py +9 -0
- codecortex/tracing/task_trace.py +235 -0
- codecortex/workspace/__init__.py +9 -0
- codecortex/workspace/federation.py +173 -0
- codecortex_context_engine-0.1.0a1.dist-info/METADATA +381 -0
- codecortex_context_engine-0.1.0a1.dist-info/RECORD +90 -0
- codecortex_context_engine-0.1.0a1.dist-info/WHEEL +4 -0
- codecortex_context_engine-0.1.0a1.dist-info/entry_points.txt +3 -0
- codecortex_context_engine-0.1.0a1.dist-info/licenses/LICENSE +201 -0
- codecortex_context_engine-0.1.0a1.dist-info/licenses/NOTICE +2 -0
codecortex/mcp/server.py
ADDED
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
"""Native stateless MCP server with stdio transport."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from dataclasses import asdict
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from codecortex.architecture import (
|
|
13
|
+
ArchitectureDriftDetector,
|
|
14
|
+
ArchitectureFingerprint,
|
|
15
|
+
ArchitectureInferenceEngine,
|
|
16
|
+
)
|
|
17
|
+
from codecortex.context import ContextPipeline
|
|
18
|
+
from codecortex.git_intelligence import GitIntelligence
|
|
19
|
+
from codecortex.indexing.impact import ImpactAnalyzer
|
|
20
|
+
from codecortex.indexing.incremental_graph import IncrementalGraphIndex
|
|
21
|
+
from codecortex.memory import TeamMemoryStore
|
|
22
|
+
from codecortex.memory.knowledge import ProjectKnowledgeExtractor
|
|
23
|
+
from codecortex.pr_intelligence import PRIntelligence
|
|
24
|
+
from codecortex.retrieval import RepositorySemanticIndex
|
|
25
|
+
from codecortex.runtime import CortexRuntime, build_runtime
|
|
26
|
+
from codecortex.tracing import TaskTraceRecorder
|
|
27
|
+
from codecortex.workspace import MultiRepositoryWorkspace
|
|
28
|
+
|
|
29
|
+
PROTOCOL_VERSION = "2026-07-28"
|
|
30
|
+
SERVER_INFO = {"name": "codecortex", "version": "0.1.0"}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _schema(properties: dict[str, Any], required: list[str] | None = None) -> dict[str, Any]:
|
|
34
|
+
schema: dict[str, Any] = {
|
|
35
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
36
|
+
"type": "object",
|
|
37
|
+
"properties": properties,
|
|
38
|
+
"additionalProperties": False,
|
|
39
|
+
}
|
|
40
|
+
if required:
|
|
41
|
+
schema["required"] = required
|
|
42
|
+
return schema
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class MCPApplication:
|
|
46
|
+
def __init__(self, runtime: CortexRuntime) -> None:
|
|
47
|
+
self.runtime = runtime
|
|
48
|
+
self.root = runtime.config.project_root
|
|
49
|
+
|
|
50
|
+
def tools(self) -> list[dict[str, Any]]:
|
|
51
|
+
text = {"type": "string", "minLength": 1}
|
|
52
|
+
positive_int = {"type": "integer", "minimum": 1}
|
|
53
|
+
return [
|
|
54
|
+
{
|
|
55
|
+
"name": "cortex_repository_map",
|
|
56
|
+
"description": "Inspect repository graph counts and matching nodes.",
|
|
57
|
+
"inputSchema": _schema({"query": {"type": "string", "default": ""}}),
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"name": "cortex_find_symbol",
|
|
61
|
+
"description": "Find code symbols across supported languages.",
|
|
62
|
+
"inputSchema": _schema({"query": text}, ["query"]),
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"name": "cortex_find_references",
|
|
66
|
+
"description": "Find graph references to a symbol or file.",
|
|
67
|
+
"inputSchema": _schema({"query": text}, ["query"]),
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"name": "cortex_dependency_graph",
|
|
71
|
+
"description": "Return dependency and call relationships around a target.",
|
|
72
|
+
"inputSchema": _schema({"query": text}, ["query"]),
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"name": "cortex_impact",
|
|
76
|
+
"description": "Estimate impact and risk of changing a target.",
|
|
77
|
+
"inputSchema": _schema({"query": text}, ["query"]),
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"name": "cortex_semantic_search",
|
|
81
|
+
"description": "Hybrid semantic, lexical, and structural repository search.",
|
|
82
|
+
"inputSchema": _schema(
|
|
83
|
+
{"query": text, "limit": {**positive_int, "default": 20}},
|
|
84
|
+
["query"],
|
|
85
|
+
),
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"name": "cortex_context",
|
|
89
|
+
"description": "Build compact query-specific context.",
|
|
90
|
+
"inputSchema": _schema(
|
|
91
|
+
{
|
|
92
|
+
"query": text,
|
|
93
|
+
"budget": {"type": "integer", "minimum": 128, "default": 32000},
|
|
94
|
+
},
|
|
95
|
+
["query"],
|
|
96
|
+
),
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"name": "cortex_architecture",
|
|
100
|
+
"description": "Infer repository architecture with evidence and confidence.",
|
|
101
|
+
"inputSchema": _schema({}),
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"name": "cortex_architecture_drift",
|
|
105
|
+
"description": "Compare current architecture with a saved baseline fingerprint.",
|
|
106
|
+
"inputSchema": _schema({}),
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"name": "cortex_symbol_history",
|
|
110
|
+
"description": "Return Git history, blame, and ownership for a symbol line range.",
|
|
111
|
+
"inputSchema": _schema(
|
|
112
|
+
{"path": text, "start": positive_int, "end": positive_int},
|
|
113
|
+
["path", "start", "end"],
|
|
114
|
+
),
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"name": "cortex_pr_intelligence",
|
|
118
|
+
"description": "Analyze a Git diff for changed symbols, impact, tests, and risk.",
|
|
119
|
+
"inputSchema": _schema(
|
|
120
|
+
{
|
|
121
|
+
"base_ref": text,
|
|
122
|
+
"head_ref": {"type": "string", "default": "HEAD"},
|
|
123
|
+
},
|
|
124
|
+
["base_ref"],
|
|
125
|
+
),
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
"name": "cortex_memory_search",
|
|
129
|
+
"description": "Search local project memory and extracted knowledge.",
|
|
130
|
+
"inputSchema": _schema({"query": text}, ["query"]),
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
"name": "cortex_team_memory_search",
|
|
134
|
+
"description": "Search revisioned shared team memory.",
|
|
135
|
+
"inputSchema": _schema(
|
|
136
|
+
{
|
|
137
|
+
"query": text,
|
|
138
|
+
"namespace": {"type": "string", "default": "project"},
|
|
139
|
+
},
|
|
140
|
+
["query"],
|
|
141
|
+
),
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"name": "cortex_remember",
|
|
145
|
+
"description": "Save a project decision or durable fact.",
|
|
146
|
+
"inputSchema": _schema({"key": text, "value": text}, ["key", "value"]),
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
"name": "cortex_workspace_search",
|
|
150
|
+
"description": "Search all repositories registered in the current workspace.",
|
|
151
|
+
"inputSchema": _schema(
|
|
152
|
+
{"query": text, "limit": {**positive_int, "default": 40}},
|
|
153
|
+
["query"],
|
|
154
|
+
),
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"name": "cortex_trace_summary",
|
|
158
|
+
"description": "Summarize a recorded agent task trace.",
|
|
159
|
+
"inputSchema": _schema({"trace_id": text}, ["trace_id"]),
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
"name": "cortex_validate",
|
|
163
|
+
"description": "Run validation for a coding request.",
|
|
164
|
+
"inputSchema": _schema({"query": text}, ["query"]),
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
"name": "cortex_stats",
|
|
168
|
+
"description": "Return index, Git, graph, and runtime statistics.",
|
|
169
|
+
"inputSchema": _schema({}),
|
|
170
|
+
},
|
|
171
|
+
]
|
|
172
|
+
|
|
173
|
+
def _graph(self):
|
|
174
|
+
return IncrementalGraphIndex(self.root).refresh()[0]
|
|
175
|
+
|
|
176
|
+
async def call(self, name: str, arguments: dict[str, Any]) -> dict[str, Any]:
|
|
177
|
+
if name == "cortex_repository_map":
|
|
178
|
+
graph = self._graph()
|
|
179
|
+
query = str(arguments.get("query", "")).strip()
|
|
180
|
+
matches = graph.search(query, 30) if query else []
|
|
181
|
+
return {
|
|
182
|
+
"counts": graph.counts(),
|
|
183
|
+
"edges": len(graph.edges),
|
|
184
|
+
"matches": [node.model_dump(mode="json") for node in matches],
|
|
185
|
+
}
|
|
186
|
+
if name == "cortex_find_symbol":
|
|
187
|
+
graph = self._graph()
|
|
188
|
+
matches = [
|
|
189
|
+
node
|
|
190
|
+
for node in graph.search(str(arguments["query"]), 80)
|
|
191
|
+
if node.kind not in {"file", "module", "reference"}
|
|
192
|
+
]
|
|
193
|
+
return {"symbols": [node.model_dump(mode="json") for node in matches[:50]]}
|
|
194
|
+
if name in {"cortex_find_references", "cortex_dependency_graph"}:
|
|
195
|
+
graph = self._graph()
|
|
196
|
+
matches = graph.search(str(arguments["query"]), 3)
|
|
197
|
+
if not matches:
|
|
198
|
+
return {"nodes": [], "edges": []}
|
|
199
|
+
ids = {node.id for node in matches}
|
|
200
|
+
edges = [
|
|
201
|
+
edge
|
|
202
|
+
for edge in graph.edges
|
|
203
|
+
if edge.source in ids or edge.target in ids
|
|
204
|
+
]
|
|
205
|
+
if name == "cortex_find_references":
|
|
206
|
+
edges = [edge for edge in edges if edge.target in ids]
|
|
207
|
+
connected = ids | {edge.source for edge in edges} | {edge.target for edge in edges}
|
|
208
|
+
nodes = [node for node in graph.nodes if node.id in connected]
|
|
209
|
+
return {
|
|
210
|
+
"nodes": [node.model_dump(mode="json") for node in nodes],
|
|
211
|
+
"edges": [edge.model_dump(mode="json") for edge in edges],
|
|
212
|
+
}
|
|
213
|
+
if name == "cortex_impact":
|
|
214
|
+
report = ImpactAnalyzer(self._graph()).analyze(str(arguments["query"]))
|
|
215
|
+
return {
|
|
216
|
+
"target": report.target.model_dump(mode="json"),
|
|
217
|
+
"risk_score": report.risk_score,
|
|
218
|
+
"direct": [self._impact_item(item) for item in report.direct],
|
|
219
|
+
"indirect": [self._impact_item(item) for item in report.indirect],
|
|
220
|
+
"affected_tests": [self._impact_item(item) for item in report.affected_tests],
|
|
221
|
+
}
|
|
222
|
+
if name == "cortex_semantic_search":
|
|
223
|
+
semantic = RepositorySemanticIndex(self.root)
|
|
224
|
+
semantic.refresh(self._graph())
|
|
225
|
+
hits = semantic.search(str(arguments["query"]), int(arguments.get("limit", 20)))
|
|
226
|
+
return {
|
|
227
|
+
"hits": [
|
|
228
|
+
{
|
|
229
|
+
"id": hit.document.id,
|
|
230
|
+
"score": hit.score,
|
|
231
|
+
"vector_score": hit.vector_score,
|
|
232
|
+
"lexical_score": hit.lexical_score,
|
|
233
|
+
"structural_score": hit.structural_score,
|
|
234
|
+
"metadata": hit.document.metadata,
|
|
235
|
+
}
|
|
236
|
+
for hit in hits
|
|
237
|
+
]
|
|
238
|
+
}
|
|
239
|
+
if name == "cortex_context":
|
|
240
|
+
query = str(arguments["query"])
|
|
241
|
+
budget = int(arguments.get("budget", 32000))
|
|
242
|
+
execution = await self.runtime.gateway.query(query, str(self.root))
|
|
243
|
+
chunks = [chunk for result in execution.results for chunk in result.chunks]
|
|
244
|
+
prepared = await ContextPipeline(self.root, self._graph()).prepare(
|
|
245
|
+
query,
|
|
246
|
+
chunks,
|
|
247
|
+
budget,
|
|
248
|
+
)
|
|
249
|
+
return {
|
|
250
|
+
"chunks": [chunk.model_dump(mode="json") for chunk in prepared.chunks],
|
|
251
|
+
"metrics": asdict(prepared.metrics),
|
|
252
|
+
"trace_id": execution.metadata.get("trace_id"),
|
|
253
|
+
}
|
|
254
|
+
if name == "cortex_architecture":
|
|
255
|
+
report = ArchitectureInferenceEngine().analyze(self._graph())
|
|
256
|
+
return asdict(report)
|
|
257
|
+
if name == "cortex_architecture_drift":
|
|
258
|
+
detector = ArchitectureDriftDetector()
|
|
259
|
+
current = detector.fingerprint(self._graph())
|
|
260
|
+
baseline_path = self.root / ".codecortex" / "architecture" / "baseline.json"
|
|
261
|
+
baseline = ArchitectureFingerprint.load(baseline_path)
|
|
262
|
+
if baseline is None:
|
|
263
|
+
return {
|
|
264
|
+
"baseline": "missing",
|
|
265
|
+
"current": asdict(current),
|
|
266
|
+
"hint": "Create a baseline with the architecture-baseline CLI command.",
|
|
267
|
+
}
|
|
268
|
+
return {
|
|
269
|
+
"baseline": asdict(baseline),
|
|
270
|
+
"current": asdict(current),
|
|
271
|
+
"drift": asdict(detector.compare(baseline, current)),
|
|
272
|
+
}
|
|
273
|
+
if name == "cortex_symbol_history":
|
|
274
|
+
history = GitIntelligence(self.root).symbol_history(
|
|
275
|
+
str(arguments["path"]),
|
|
276
|
+
int(arguments["start"]),
|
|
277
|
+
int(arguments["end"]),
|
|
278
|
+
)
|
|
279
|
+
return asdict(history)
|
|
280
|
+
if name == "cortex_pr_intelligence":
|
|
281
|
+
report = PRIntelligence(self.root, self._graph()).analyze(
|
|
282
|
+
str(arguments["base_ref"]),
|
|
283
|
+
str(arguments.get("head_ref", "HEAD")),
|
|
284
|
+
)
|
|
285
|
+
return {
|
|
286
|
+
"base_ref": report.base_ref,
|
|
287
|
+
"head_ref": report.head_ref,
|
|
288
|
+
"risk_score": report.risk_score,
|
|
289
|
+
"risk_level": report.risk_level,
|
|
290
|
+
"affected_tests": list(report.affected_tests),
|
|
291
|
+
"files": [asdict(item) for item in report.files],
|
|
292
|
+
"symbols": [
|
|
293
|
+
{
|
|
294
|
+
"node": item.node.model_dump(mode="json"),
|
|
295
|
+
"impact_risk": item.impact_risk,
|
|
296
|
+
"affected_nodes": item.affected_nodes,
|
|
297
|
+
"affected_tests": item.affected_tests,
|
|
298
|
+
}
|
|
299
|
+
for item in report.symbols
|
|
300
|
+
],
|
|
301
|
+
}
|
|
302
|
+
if name == "cortex_memory_search":
|
|
303
|
+
query = str(arguments["query"])
|
|
304
|
+
project = await self.runtime.memory.search("project", query, 10)
|
|
305
|
+
knowledge = await self.runtime.memory.search("project_knowledge", query, 10)
|
|
306
|
+
return {"results": [*project, *knowledge][:20]}
|
|
307
|
+
if name == "cortex_team_memory_search":
|
|
308
|
+
store = TeamMemoryStore(
|
|
309
|
+
self.root / ".codecortex" / "memory" / "team.sqlite3"
|
|
310
|
+
)
|
|
311
|
+
entries = store.search_entries(
|
|
312
|
+
str(arguments.get("namespace", "project")),
|
|
313
|
+
str(arguments["query"]),
|
|
314
|
+
20,
|
|
315
|
+
)
|
|
316
|
+
return {"results": [asdict(entry) for entry in entries]}
|
|
317
|
+
if name == "cortex_remember":
|
|
318
|
+
await self.runtime.gateway.remember(
|
|
319
|
+
str(arguments["key"]),
|
|
320
|
+
str(arguments["value"]),
|
|
321
|
+
)
|
|
322
|
+
return {"saved": True}
|
|
323
|
+
if name == "cortex_workspace_search":
|
|
324
|
+
workspace = MultiRepositoryWorkspace(
|
|
325
|
+
self.root / ".codecortex" / "workspace.json"
|
|
326
|
+
)
|
|
327
|
+
hits = workspace.search(
|
|
328
|
+
str(arguments["query"]),
|
|
329
|
+
int(arguments.get("limit", 40)),
|
|
330
|
+
)
|
|
331
|
+
return {
|
|
332
|
+
"hits": [
|
|
333
|
+
{
|
|
334
|
+
"repository": hit.repository,
|
|
335
|
+
"score": hit.score,
|
|
336
|
+
"node": hit.node.model_dump(mode="json"),
|
|
337
|
+
}
|
|
338
|
+
for hit in hits
|
|
339
|
+
]
|
|
340
|
+
}
|
|
341
|
+
if name == "cortex_trace_summary":
|
|
342
|
+
recorder = TaskTraceRecorder(
|
|
343
|
+
self.root / ".codecortex" / "runtime" / "traces.jsonl"
|
|
344
|
+
)
|
|
345
|
+
return asdict(recorder.summarize(str(arguments["trace_id"])))
|
|
346
|
+
if name == "cortex_validate":
|
|
347
|
+
result = await self.runtime.gateway.query(str(arguments["query"]), str(self.root))
|
|
348
|
+
return {
|
|
349
|
+
"validation": [
|
|
350
|
+
item.model_dump(mode="json")
|
|
351
|
+
for item in result.results
|
|
352
|
+
if item.capability.value == "validation"
|
|
353
|
+
],
|
|
354
|
+
"trace_id": result.metadata.get("trace_id"),
|
|
355
|
+
}
|
|
356
|
+
if name == "cortex_stats":
|
|
357
|
+
graph, graph_stats = IncrementalGraphIndex(self.root).refresh()
|
|
358
|
+
git = GitIntelligence(self.root).analyze(300)
|
|
359
|
+
knowledge = ProjectKnowledgeExtractor(self.root).extract()
|
|
360
|
+
return {
|
|
361
|
+
"index": {
|
|
362
|
+
"tracked": graph_stats.index.tracked,
|
|
363
|
+
"added": len(graph_stats.index.added),
|
|
364
|
+
"changed": len(graph_stats.index.changed),
|
|
365
|
+
"removed": len(graph_stats.index.removed),
|
|
366
|
+
"files_reparsed": graph_stats.files_reparsed,
|
|
367
|
+
"full_rebuild": graph_stats.full_rebuild,
|
|
368
|
+
"duration_ms": graph_stats.index.duration_ms,
|
|
369
|
+
},
|
|
370
|
+
"graph": {
|
|
371
|
+
"nodes": len(graph.nodes),
|
|
372
|
+
"edges": len(graph.edges),
|
|
373
|
+
**graph.counts(),
|
|
374
|
+
},
|
|
375
|
+
"git": {
|
|
376
|
+
"commits": git.commits,
|
|
377
|
+
"hot_files": [item.path for item in git.hot_files[:10]],
|
|
378
|
+
},
|
|
379
|
+
"knowledge": knowledge.facts(),
|
|
380
|
+
"health": await self.runtime.gateway.health(),
|
|
381
|
+
}
|
|
382
|
+
raise KeyError(f"Unknown tool: {name}")
|
|
383
|
+
|
|
384
|
+
@staticmethod
|
|
385
|
+
def _impact_item(item: Any) -> dict[str, Any]:
|
|
386
|
+
return {
|
|
387
|
+
"node": item.node.model_dump(mode="json"),
|
|
388
|
+
"depth": item.depth,
|
|
389
|
+
"via": item.via,
|
|
390
|
+
"risk": item.risk,
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
class MCPServer:
|
|
395
|
+
def __init__(self, application: MCPApplication) -> None:
|
|
396
|
+
self.application = application
|
|
397
|
+
|
|
398
|
+
async def dispatch(self, message: dict[str, Any]) -> dict[str, Any] | None:
|
|
399
|
+
request_id = message.get("id")
|
|
400
|
+
method = message.get("method")
|
|
401
|
+
if request_id is None:
|
|
402
|
+
return None
|
|
403
|
+
try:
|
|
404
|
+
if method in {"server/discover", "initialize"}:
|
|
405
|
+
return self._result(request_id, self._discovery())
|
|
406
|
+
if method == "ping":
|
|
407
|
+
return self._result(request_id, {})
|
|
408
|
+
if method == "tools/list":
|
|
409
|
+
return self._result(
|
|
410
|
+
request_id,
|
|
411
|
+
{"tools": self.application.tools(), "ttlMs": 300000},
|
|
412
|
+
)
|
|
413
|
+
if method == "tools/call":
|
|
414
|
+
params = message.get("params") or {}
|
|
415
|
+
payload = await self.application.call(
|
|
416
|
+
str(params.get("name", "")),
|
|
417
|
+
dict(params.get("arguments") or {}),
|
|
418
|
+
)
|
|
419
|
+
return self._result(request_id, self._tool_result(payload))
|
|
420
|
+
return self._error(request_id, -32601, f"Method not found: {method}")
|
|
421
|
+
except (KeyError, TypeError, ValueError) as exc:
|
|
422
|
+
return self._error(request_id, -32602, str(exc))
|
|
423
|
+
except Exception as exc: # pragma: no cover
|
|
424
|
+
return self._error(request_id, -32603, f"Internal error: {exc}")
|
|
425
|
+
|
|
426
|
+
def _discovery(self) -> dict[str, Any]:
|
|
427
|
+
return {
|
|
428
|
+
"protocolVersion": PROTOCOL_VERSION,
|
|
429
|
+
"serverInfo": SERVER_INFO,
|
|
430
|
+
"capabilities": {"tools": {"listChanged": False}},
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
@staticmethod
|
|
434
|
+
def _tool_result(payload: dict[str, Any]) -> dict[str, Any]:
|
|
435
|
+
text = json.dumps(payload, ensure_ascii=False, separators=(",", ":"))
|
|
436
|
+
return {
|
|
437
|
+
"content": [{"type": "text", "text": text}],
|
|
438
|
+
"structuredContent": payload,
|
|
439
|
+
"isError": False,
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
@staticmethod
|
|
443
|
+
def _result(request_id: Any, result: dict[str, Any]) -> dict[str, Any]:
|
|
444
|
+
return {"jsonrpc": "2.0", "id": request_id, "result": result}
|
|
445
|
+
|
|
446
|
+
@staticmethod
|
|
447
|
+
def _error(request_id: Any, code: int, message: str) -> dict[str, Any]:
|
|
448
|
+
return {
|
|
449
|
+
"jsonrpc": "2.0",
|
|
450
|
+
"id": request_id,
|
|
451
|
+
"error": {"code": code, "message": message},
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
async def serve_stdio(self) -> None:
|
|
455
|
+
loop = asyncio.get_running_loop()
|
|
456
|
+
while True:
|
|
457
|
+
line = await loop.run_in_executor(None, sys.stdin.readline)
|
|
458
|
+
if not line:
|
|
459
|
+
return
|
|
460
|
+
try:
|
|
461
|
+
message = json.loads(line)
|
|
462
|
+
except json.JSONDecodeError:
|
|
463
|
+
response = self._error(None, -32700, "Parse error")
|
|
464
|
+
else:
|
|
465
|
+
response = await self.dispatch(message)
|
|
466
|
+
if response is not None:
|
|
467
|
+
sys.stdout.write(json.dumps(response, ensure_ascii=False) + "\n")
|
|
468
|
+
sys.stdout.flush()
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def run_stdio(project_root: Path | None = None) -> None:
|
|
472
|
+
runtime = build_runtime(project_root)
|
|
473
|
+
asyncio.run(MCPServer(MCPApplication(runtime)).serve_stdio())
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Project and team-scoped persistent memory."""
|
|
2
|
+
|
|
3
|
+
from codecortex.memory.json_store import JsonMemoryStore
|
|
4
|
+
from codecortex.memory.team_store import RevisionConflict, TeamMemoryEntry, TeamMemoryStore
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"JsonMemoryStore",
|
|
8
|
+
"RevisionConflict",
|
|
9
|
+
"TeamMemoryEntry",
|
|
10
|
+
"TeamMemoryStore",
|
|
11
|
+
]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Simple local JSON memory backend."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from codecortex.core.contracts import MemoryStore
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class JsonMemoryStore(MemoryStore):
|
|
13
|
+
def __init__(self, root: Path) -> None:
|
|
14
|
+
self.root = root
|
|
15
|
+
self.root.mkdir(parents=True, exist_ok=True)
|
|
16
|
+
|
|
17
|
+
def _path(self, namespace: str) -> Path:
|
|
18
|
+
safe = re.sub(r"[^a-zA-Z0-9_.-]+", "_", namespace)
|
|
19
|
+
return self.root / f"{safe}.json"
|
|
20
|
+
|
|
21
|
+
def _load(self, namespace: str) -> dict[str, str]:
|
|
22
|
+
path = self._path(namespace)
|
|
23
|
+
if not path.exists():
|
|
24
|
+
return {}
|
|
25
|
+
try:
|
|
26
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
27
|
+
except (json.JSONDecodeError, OSError):
|
|
28
|
+
return {}
|
|
29
|
+
return {str(key): str(value) for key, value in data.items()}
|
|
30
|
+
|
|
31
|
+
async def put(self, namespace: str, key: str, value: str) -> None:
|
|
32
|
+
data = self._load(namespace)
|
|
33
|
+
data[key] = value
|
|
34
|
+
path = self._path(namespace)
|
|
35
|
+
temp = path.with_suffix(".tmp")
|
|
36
|
+
temp.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")
|
|
37
|
+
temp.replace(path)
|
|
38
|
+
|
|
39
|
+
async def get(self, namespace: str, key: str) -> str | None:
|
|
40
|
+
return self._load(namespace).get(key)
|
|
41
|
+
|
|
42
|
+
async def search(self, namespace: str, query: str, limit: int = 10) -> list[str]:
|
|
43
|
+
terms = {term.lower() for term in query.split() if term.strip()}
|
|
44
|
+
data = self._load(namespace)
|
|
45
|
+
scored: list[tuple[int, str]] = []
|
|
46
|
+
for key, value in data.items():
|
|
47
|
+
haystack = f"{key} {value}".lower()
|
|
48
|
+
score = sum(1 for term in terms if term in haystack)
|
|
49
|
+
if score:
|
|
50
|
+
scored.append((score, value))
|
|
51
|
+
scored.sort(key=lambda item: item[0], reverse=True)
|
|
52
|
+
return [value for _, value in scored[:limit]]
|