@danielblomma/cortex-mcp 2.4.0 → 2.4.2
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/CHANGELOG.md +226 -0
- package/README.md +24 -3
- package/bin/cli/arguments.mjs +60 -0
- package/bin/cli/context-passthrough.mjs +129 -0
- package/bin/cli/daemon.mjs +157 -0
- package/bin/cli/enterprise.mjs +516 -0
- package/bin/cli/help.mjs +88 -0
- package/bin/cli/hooks.mjs +199 -0
- package/bin/cli/mcp-command.mjs +87 -0
- package/bin/cli/paths.mjs +14 -0
- package/bin/cli/process.mjs +49 -0
- package/bin/cli/project-commands.mjs +237 -0
- package/bin/cli/project-runtime.mjs +34 -0
- package/bin/cli/query-command.mjs +18 -0
- package/bin/cli/router.mjs +66 -0
- package/bin/cli/run-command.mjs +44 -0
- package/bin/cli/scaffold-ownership.mjs +936 -0
- package/bin/cli/scaffold.mjs +687 -0
- package/bin/cli/stage-command.mjs +9 -0
- package/bin/cli/telemetry-command.mjs +18 -0
- package/bin/cli/trusted-runtime.mjs +18 -0
- package/bin/cortex.mjs +12 -1607
- package/bin/daemon-control.mjs +162 -0
- package/mcp-registry-submission.json +1 -1
- package/package.json +8 -3
- package/scaffold/mcp/dist/.cortex-build-hash +1 -0
- package/scaffold/mcp/dist/cli/enterprise-setup.js +134 -0
- package/scaffold/mcp/dist/cli/govern.js +937 -0
- package/scaffold/mcp/dist/cli/query.js +409 -0
- package/scaffold/mcp/dist/cli/run.js +295 -0
- package/scaffold/mcp/dist/cli/stage.js +308 -0
- package/scaffold/mcp/dist/cli/telemetry-test.js +141 -0
- package/scaffold/mcp/dist/cli/ungoverned-detector.js +133 -0
- package/scaffold/mcp/dist/contextEntities.js +282 -0
- package/scaffold/mcp/dist/core/audit/query.js +72 -0
- package/scaffold/mcp/dist/core/audit/writer.js +28 -0
- package/scaffold/mcp/dist/core/config.js +235 -0
- package/scaffold/mcp/dist/core/enterprise-host-identity.js +193 -0
- package/scaffold/mcp/dist/core/enterprise-identity.js +18 -0
- package/scaffold/mcp/dist/core/enterprise-rotation.js +158 -0
- package/scaffold/mcp/dist/core/govern-paths.js +21 -0
- package/scaffold/mcp/dist/core/index.js +14 -0
- package/scaffold/mcp/dist/core/license.js +278 -0
- package/scaffold/mcp/dist/core/policy/enforce.js +66 -0
- package/scaffold/mcp/dist/core/policy/injection.js +172 -0
- package/scaffold/mcp/dist/core/policy/store.js +179 -0
- package/scaffold/mcp/dist/core/rbac/check.js +30 -0
- package/scaffold/mcp/dist/core/secure-endpoint.js +25 -0
- package/scaffold/mcp/dist/core/telemetry/collector.js +285 -0
- package/scaffold/mcp/dist/core/telemetry/state-dir.js +31 -0
- package/scaffold/mcp/dist/core/validators/builtins.js +632 -0
- package/scaffold/mcp/dist/core/validators/config.js +44 -0
- package/scaffold/mcp/dist/core/validators/engine.js +120 -0
- package/scaffold/mcp/dist/core/validators/evaluators/code_comments.js +236 -0
- package/scaffold/mcp/dist/core/validators/evaluators/regex.js +116 -0
- package/scaffold/mcp/dist/core/workflow/artifact-io.js +103 -0
- package/scaffold/mcp/dist/core/workflow/capabilities.js +88 -0
- package/scaffold/mcp/dist/core/workflow/default-workflows.js +102 -0
- package/scaffold/mcp/dist/core/workflow/enforcement.js +164 -0
- package/scaffold/mcp/dist/core/workflow/envelope.js +132 -0
- package/scaffold/mcp/dist/core/workflow/index.js +11 -0
- package/scaffold/mcp/dist/core/workflow/mcp-tools.js +175 -0
- package/scaffold/mcp/dist/core/workflow/resolution.js +69 -0
- package/scaffold/mcp/dist/core/workflow/run-lifecycle.js +123 -0
- package/scaffold/mcp/dist/core/workflow/schemas.js +141 -0
- package/scaffold/mcp/dist/core/workflow/synced-capability-registry.js +60 -0
- package/scaffold/mcp/dist/core/workflow/synced-registry.js +60 -0
- package/scaffold/mcp/dist/daemon/capability-sync-checker.js +242 -0
- package/scaffold/mcp/dist/daemon/client.js +130 -0
- package/scaffold/mcp/dist/daemon/egress-proxy.js +288 -0
- package/scaffold/mcp/dist/daemon/global-host-events.js +222 -0
- package/scaffold/mcp/dist/daemon/heartbeat-pusher.js +116 -0
- package/scaffold/mcp/dist/daemon/heartbeat-tracker.js +165 -0
- package/scaffold/mcp/dist/daemon/host-events-pusher.js +249 -0
- package/scaffold/mcp/dist/daemon/main.js +449 -0
- package/scaffold/mcp/dist/daemon/paths.js +36 -0
- package/scaffold/mcp/dist/daemon/project-service-registry.js +78 -0
- package/scaffold/mcp/dist/daemon/protocol.js +8 -0
- package/scaffold/mcp/dist/daemon/server.js +180 -0
- package/scaffold/mcp/dist/daemon/skill-sync-checker.js +557 -0
- package/scaffold/mcp/dist/daemon/sync-checker.js +165 -0
- package/scaffold/mcp/dist/daemon/ungoverned-scanner.js +136 -0
- package/scaffold/mcp/dist/daemon/workflow-sync-checker.js +249 -0
- package/scaffold/mcp/dist/defaults.js +6 -0
- package/scaffold/mcp/dist/embed.js +627 -0
- package/scaffold/mcp/dist/embedScheduler.js +479 -0
- package/scaffold/mcp/dist/embeddings.js +167 -0
- package/scaffold/mcp/dist/enterprise/audit/push.js +66 -0
- package/scaffold/mcp/dist/enterprise/index.js +327 -0
- package/scaffold/mcp/dist/enterprise/model/deploy.js +27 -0
- package/scaffold/mcp/dist/enterprise/policy/sync.js +129 -0
- package/scaffold/mcp/dist/enterprise/privacy/boundary.js +184 -0
- package/scaffold/mcp/dist/enterprise/reviews/changed-files.js +33 -0
- package/scaffold/mcp/dist/enterprise/reviews/pattern-context.js +202 -0
- package/scaffold/mcp/dist/enterprise/reviews/policy-selection.js +46 -0
- package/scaffold/mcp/dist/enterprise/reviews/push.js +102 -0
- package/scaffold/mcp/dist/enterprise/reviews/trust-state.js +186 -0
- package/scaffold/mcp/dist/enterprise/telemetry/sync.js +57 -0
- package/scaffold/mcp/dist/enterprise/tools/enterprise.js +826 -0
- package/scaffold/mcp/dist/enterprise/tools/harness.js +40 -0
- package/scaffold/mcp/dist/enterprise/tools/walk.js +73 -0
- package/scaffold/mcp/dist/enterprise/violations/push.js +77 -0
- package/scaffold/mcp/dist/enterprise/workflow/push.js +44 -0
- package/scaffold/mcp/dist/enterprise/workflow/state.js +329 -0
- package/scaffold/mcp/dist/frontmatter.js +33 -0
- package/scaffold/mcp/dist/graph.js +769 -0
- package/scaffold/mcp/dist/graphCsv.js +55 -0
- package/scaffold/mcp/dist/graphMetrics.js +8 -0
- package/scaffold/mcp/dist/hooks/permission-request.js +89 -0
- package/scaffold/mcp/dist/hooks/post-tool-use.js +105 -0
- package/scaffold/mcp/dist/hooks/pre-compact.js +29 -0
- package/scaffold/mcp/dist/hooks/pre-tool-use.js +78 -0
- package/scaffold/mcp/dist/hooks/session-end.js +43 -0
- package/scaffold/mcp/dist/hooks/session-start.js +41 -0
- package/scaffold/mcp/dist/hooks/shared.js +194 -0
- package/scaffold/mcp/dist/hooks/stop.js +28 -0
- package/scaffold/mcp/dist/hooks/user-prompt-submit.js +33 -0
- package/scaffold/mcp/dist/impactPresentation.js +137 -0
- package/scaffold/mcp/dist/impactRanking.js +191 -0
- package/scaffold/mcp/dist/impactResponse.js +30 -0
- package/scaffold/mcp/dist/impactResults.js +105 -0
- package/scaffold/mcp/dist/impactSeed.js +20 -0
- package/scaffold/mcp/dist/impactTraversal.js +64 -0
- package/scaffold/mcp/dist/jsonl.js +77 -0
- package/scaffold/mcp/dist/loadGraph.js +759 -0
- package/scaffold/mcp/dist/lruCache.js +38 -0
- package/scaffold/mcp/dist/paths.js +97 -0
- package/scaffold/mcp/dist/patternEvidence.js +272 -0
- package/scaffold/mcp/dist/plugin.js +81 -0
- package/scaffold/mcp/dist/presets.js +78 -0
- package/scaffold/mcp/dist/relatedResponse.js +18 -0
- package/scaffold/mcp/dist/relatedTraversal.js +78 -0
- package/scaffold/mcp/dist/rules.js +23 -0
- package/scaffold/mcp/dist/search.js +212 -0
- package/scaffold/mcp/dist/searchCore.js +457 -0
- package/scaffold/mcp/dist/searchResults.js +230 -0
- package/scaffold/mcp/dist/server.js +317 -0
- package/scaffold/mcp/dist/types.js +1 -0
- package/scaffold/mcp/package-lock.json +321 -200
- package/scaffold/mcp/package.json +15 -6
- package/scaffold/mcp/src/cli/enterprise-setup.ts +82 -8
- package/scaffold/mcp/src/cli/govern.ts +137 -52
- package/scaffold/mcp/src/cli/run.ts +53 -19
- package/scaffold/mcp/src/cli/stage.ts +8 -3
- package/scaffold/mcp/src/core/config.ts +9 -2
- package/scaffold/mcp/src/core/enterprise-host-identity.ts +259 -0
- package/scaffold/mcp/src/core/enterprise-identity.ts +20 -0
- package/scaffold/mcp/src/core/enterprise-rotation.ts +185 -0
- package/scaffold/mcp/src/core/govern-paths.ts +30 -0
- package/scaffold/mcp/src/core/license.ts +186 -17
- package/scaffold/mcp/src/core/secure-endpoint.ts +23 -0
- package/scaffold/mcp/src/core/workflow/enforcement.ts +8 -1
- package/scaffold/mcp/src/core/workflow/mcp-tools.ts +3 -0
- package/scaffold/mcp/src/core/workflow/resolution.ts +9 -1
- package/scaffold/mcp/src/core/workflow/synced-capability-registry.ts +15 -0
- package/scaffold/mcp/src/core/workflow/synced-registry.ts +15 -0
- package/scaffold/mcp/src/daemon/capability-sync-checker.ts +38 -2
- package/scaffold/mcp/src/daemon/egress-proxy.ts +14 -8
- package/scaffold/mcp/src/daemon/global-host-events.ts +288 -0
- package/scaffold/mcp/src/daemon/heartbeat-pusher.ts +4 -0
- package/scaffold/mcp/src/daemon/heartbeat-tracker.ts +2 -0
- package/scaffold/mcp/src/daemon/host-events-pusher.ts +5 -0
- package/scaffold/mcp/src/daemon/main.ts +99 -25
- package/scaffold/mcp/src/daemon/project-service-registry.ts +107 -0
- package/scaffold/mcp/src/daemon/skill-sync-checker.ts +368 -31
- package/scaffold/mcp/src/daemon/sync-checker.ts +4 -0
- package/scaffold/mcp/src/daemon/ungoverned-scanner.ts +57 -25
- package/scaffold/mcp/src/daemon/workflow-sync-checker.ts +41 -2
- package/scaffold/mcp/src/enterprise/audit/push.ts +8 -0
- package/scaffold/mcp/src/enterprise/policy/sync.ts +12 -0
- package/scaffold/mcp/src/enterprise/reviews/push.ts +9 -0
- package/scaffold/mcp/src/enterprise/reviews/trust-state.ts +3 -1
- package/scaffold/mcp/src/enterprise/telemetry/sync.ts +9 -0
- package/scaffold/mcp/src/enterprise/violations/push.ts +9 -0
- package/scaffold/mcp/src/enterprise/workflow/push.ts +7 -0
- package/scaffold/mcp/src/plugin.ts +20 -0
- package/scaffold/mcp/tests/copilot-shim.test.mjs +19 -1
- package/scaffold/mcp/tests/egress-proxy.test.mjs +37 -0
- package/scaffold/mcp/tests/enterprise-identity-sync.test.mjs +401 -0
- package/scaffold/mcp/tests/enterprise-setup.test.mjs +189 -0
- package/scaffold/mcp/tests/global-host-events.test.mjs +81 -0
- package/scaffold/mcp/tests/govern-install.test.mjs +95 -2
- package/scaffold/mcp/tests/govern-repair.test.mjs +20 -3
- package/scaffold/mcp/tests/heartbeat-tracker.test.mjs +26 -0
- package/scaffold/mcp/tests/license.test.mjs +367 -0
- package/scaffold/mcp/tests/project-service-registry.test.mjs +172 -0
- package/scaffold/mcp/tests/review-trust-contract.test.mjs +18 -0
- package/scaffold/mcp/tests/secure-enterprise-endpoint.test.mjs +46 -0
- package/scaffold/mcp/tests/skill-sync-checker.test.mjs +446 -2
- package/scaffold/mcp/tests/ungoverned-scanner.test.mjs +104 -22
- package/scaffold/mcp/tests/workflow-cli.test.mjs +14 -1
- package/scaffold/mcp/tests/workflow-synced-capabilities.test.mjs +37 -0
- package/scaffold/mcp/tests/workflow-synced-registry.test.mjs +40 -1
- package/scaffold/ownership/baseline-v2.4.1.json +22 -0
- package/scaffold/ownership/current.json +4 -0
- package/scaffold/ownership/v1.json +456 -0
- package/scaffold/scripts/ingest-parsers.mjs +1 -387
- package/scaffold/scripts/ingest-worker.mjs +4 -1
- package/scaffold/scripts/ingest.mjs +5 -3899
- package/scaffold/scripts/lib/ingest/arguments.mjs +78 -0
- package/scaffold/scripts/lib/ingest/chunks.mjs +212 -0
- package/scaffold/scripts/lib/ingest/config.mjs +120 -0
- package/scaffold/scripts/lib/ingest/constants.mjs +222 -0
- package/scaffold/scripts/lib/ingest/files.mjs +387 -0
- package/scaffold/scripts/lib/ingest/incremental-state.mjs +131 -0
- package/scaffold/scripts/lib/ingest/io.mjs +78 -0
- package/scaffold/scripts/lib/ingest/main.mjs +76 -0
- package/scaffold/scripts/lib/ingest/parser-composition.mjs +140 -0
- package/scaffold/scripts/lib/ingest/parser-registry.mjs +387 -0
- package/scaffold/scripts/lib/ingest/pipeline-stages.mjs +1625 -0
- package/scaffold/scripts/lib/ingest/projects.mjs +272 -0
- package/scaffold/scripts/lib/ingest/relations.mjs +860 -0
- package/scaffold/scripts/lib/ingest/runtime-paths.mjs +11 -0
- package/scaffold/scripts/lib/ingest/workers.mjs +264 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
function isWindowChunkId(id) {
|
|
2
|
+
return id.includes(":window:");
|
|
3
|
+
}
|
|
4
|
+
function baseChunkId(id) {
|
|
5
|
+
const markerIndex = id.indexOf(":window:");
|
|
6
|
+
return markerIndex === -1 ? id : id.slice(0, markerIndex);
|
|
7
|
+
}
|
|
8
|
+
function baseChunkLabel(label) {
|
|
9
|
+
const markerIndex = label.indexOf("#window");
|
|
10
|
+
return markerIndex === -1 ? label : label.slice(0, markerIndex);
|
|
11
|
+
}
|
|
12
|
+
export function compareText(a, b) {
|
|
13
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
14
|
+
}
|
|
15
|
+
function rankedResultKey(ranked) {
|
|
16
|
+
return `${String(ranked.result.id ?? "")}\u0000${String(ranked.result.path ?? "")}`;
|
|
17
|
+
}
|
|
18
|
+
function compareRankedResults(a, b) {
|
|
19
|
+
return b.rankScore - a.rankScore || compareText(rankedResultKey(a), rankedResultKey(b));
|
|
20
|
+
}
|
|
21
|
+
function pruneRankedResults(bestById, limit) {
|
|
22
|
+
if (bestById.size <= limit) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const retained = [...bestById.entries()]
|
|
26
|
+
.sort(([, a], [, b]) => compareRankedResults(a, b))
|
|
27
|
+
.slice(0, limit);
|
|
28
|
+
bestById.clear();
|
|
29
|
+
for (const [id, result] of retained) {
|
|
30
|
+
bestById.set(id, result);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function secondarySignalScale(semantic) {
|
|
34
|
+
return Math.min(1, 0.15 + Math.max(0, semantic) * 0.85);
|
|
35
|
+
}
|
|
36
|
+
function isTestEvidenceEntity(entity) {
|
|
37
|
+
return /(^|\/)(tests?|__tests__)\//u.test(entity.path) || /\.(test|spec)\.[^.]+$/u.test(entity.path);
|
|
38
|
+
}
|
|
39
|
+
function testEvidenceBoost(entity, semantic, lexicalSemantic) {
|
|
40
|
+
if (!isTestEvidenceEntity(entity)) {
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
if (semantic >= 0.5 && lexicalSemantic >= 0.4) {
|
|
44
|
+
return 0.07;
|
|
45
|
+
}
|
|
46
|
+
if (semantic >= 0.4 && lexicalSemantic >= 0.25) {
|
|
47
|
+
return 0.04;
|
|
48
|
+
}
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
|
51
|
+
function resultPathKey(result) {
|
|
52
|
+
const path = typeof result.path === "string" ? result.path : "";
|
|
53
|
+
return path || String(result.id ?? "");
|
|
54
|
+
}
|
|
55
|
+
function selectDiverseResults(rankedResults, topK) {
|
|
56
|
+
const remaining = rankedResults
|
|
57
|
+
.map((ranked) => ({ ranked }))
|
|
58
|
+
.sort((a, b) => compareRankedResults(a.ranked, b.ranked));
|
|
59
|
+
const selected = [];
|
|
60
|
+
const selectedByPath = new Map();
|
|
61
|
+
while (selected.length < topK && remaining.length > 0) {
|
|
62
|
+
let bestIndex = 0;
|
|
63
|
+
let bestAdjustedScore = Number.NEGATIVE_INFINITY;
|
|
64
|
+
let bestRankScore = Number.NEGATIVE_INFINITY;
|
|
65
|
+
let bestKey = "";
|
|
66
|
+
for (let index = 0; index < remaining.length; index += 1) {
|
|
67
|
+
const candidate = remaining[index].ranked;
|
|
68
|
+
const pathCount = selectedByPath.get(resultPathKey(candidate.result)) ?? 0;
|
|
69
|
+
const samePathPenalty = pathCount === 0 ? 0 : Math.min(0.12, pathCount * 0.04);
|
|
70
|
+
const adjustedScore = candidate.rankScore - samePathPenalty;
|
|
71
|
+
const candidateKey = rankedResultKey(candidate);
|
|
72
|
+
if (adjustedScore > bestAdjustedScore ||
|
|
73
|
+
(adjustedScore === bestAdjustedScore && candidate.rankScore > bestRankScore) ||
|
|
74
|
+
(adjustedScore === bestAdjustedScore && candidate.rankScore === bestRankScore && candidateKey < bestKey)) {
|
|
75
|
+
bestIndex = index;
|
|
76
|
+
bestAdjustedScore = adjustedScore;
|
|
77
|
+
bestRankScore = candidate.rankScore;
|
|
78
|
+
bestKey = candidateKey;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const [picked] = remaining.splice(bestIndex, 1);
|
|
82
|
+
selected.push(picked.ranked);
|
|
83
|
+
const pathKey = resultPathKey(picked.ranked.result);
|
|
84
|
+
selectedByPath.set(pathKey, (selectedByPath.get(pathKey) ?? 0) + 1);
|
|
85
|
+
}
|
|
86
|
+
return selected;
|
|
87
|
+
}
|
|
88
|
+
function candidatesFrom(source) {
|
|
89
|
+
return typeof source === "function" ? source() : source;
|
|
90
|
+
}
|
|
91
|
+
export function buildSearchResultsWithStats(params) {
|
|
92
|
+
if (params.topK <= 0) {
|
|
93
|
+
return { results: [], totalCandidates: 0 };
|
|
94
|
+
}
|
|
95
|
+
// Graph score = midrank percentile of relation degree within the entity's
|
|
96
|
+
// own type. The previous min(1, degree/4) saturated at degree >= 4, which
|
|
97
|
+
// nearly every entity exceeds, making the graph weight a constant. Per-type
|
|
98
|
+
// percentiles discriminate by connectivity while staying type-neutral
|
|
99
|
+
// (every type averages ~0.5), so hub-heavy types like rules cannot drown
|
|
100
|
+
// out leaf code files or doc sections.
|
|
101
|
+
const sortedDegreesByType = new Map();
|
|
102
|
+
const chunkCandidatesById = new Map();
|
|
103
|
+
let totalCandidates = 0;
|
|
104
|
+
for (const entity of candidatesFrom(params.candidates)) {
|
|
105
|
+
totalCandidates += 1;
|
|
106
|
+
const degree = params.degreeByEntity.get(entity.id) ?? 0;
|
|
107
|
+
const list = sortedDegreesByType.get(entity.entity_type);
|
|
108
|
+
if (list) {
|
|
109
|
+
list.push(degree);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
sortedDegreesByType.set(entity.entity_type, [degree]);
|
|
113
|
+
}
|
|
114
|
+
if (entity.entity_type === "Chunk" && !isWindowChunkId(entity.id)) {
|
|
115
|
+
chunkCandidatesById.set(entity.id, {
|
|
116
|
+
label: entity.label,
|
|
117
|
+
path: entity.path
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
for (const list of sortedDegreesByType.values()) {
|
|
122
|
+
list.sort((a, b) => a - b);
|
|
123
|
+
}
|
|
124
|
+
const midrankPercentile = (sorted, value) => {
|
|
125
|
+
let lo = 0;
|
|
126
|
+
let hi = sorted.length;
|
|
127
|
+
while (lo < hi) {
|
|
128
|
+
const mid = (lo + hi) >> 1;
|
|
129
|
+
if (sorted[mid] < value)
|
|
130
|
+
lo = mid + 1;
|
|
131
|
+
else
|
|
132
|
+
hi = mid;
|
|
133
|
+
}
|
|
134
|
+
let upper = lo;
|
|
135
|
+
while (upper < sorted.length && sorted[upper] === value)
|
|
136
|
+
upper += 1;
|
|
137
|
+
return sorted.length > 0 ? (lo + upper) / (2 * sorted.length) : 0;
|
|
138
|
+
};
|
|
139
|
+
const resultLimit = Math.max(1, params.topK);
|
|
140
|
+
const diversityPoolLimit = Math.max(resultLimit * 3, resultLimit);
|
|
141
|
+
const pruneThreshold = Math.max(diversityPoolLimit * 2, 64);
|
|
142
|
+
const bestById = new Map();
|
|
143
|
+
for (const entity of candidatesFrom(params.candidates)) {
|
|
144
|
+
const lexicalSemantic = params.semanticScorer(params.queryTokens, params.queryPhrase, entity.text);
|
|
145
|
+
const entityVector = params.embeddingVectors.get(entity.id);
|
|
146
|
+
const vectorSemantic = params.queryVector && entityVector
|
|
147
|
+
? Math.max(0, Math.min(1, params.vectorScorer(params.queryVector, entityVector)))
|
|
148
|
+
: 0;
|
|
149
|
+
const hasRelevanceSignal = lexicalSemantic >= params.minLexicalRelevance || vectorSemantic >= params.minVectorRelevance;
|
|
150
|
+
if (!hasRelevanceSignal) {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const semantic = vectorSemantic > 0 ? vectorSemantic * 0.75 + lexicalSemantic * 0.25 : lexicalSemantic;
|
|
154
|
+
const degree = params.degreeByEntity.get(entity.id) ?? 0;
|
|
155
|
+
const graphScore = midrankPercentile(sortedDegreesByType.get(entity.entity_type) ?? [], degree);
|
|
156
|
+
const trustScore = Math.max(0, Math.min(1, entity.trust_level / 100));
|
|
157
|
+
const dateScore = params.recencyScorer(entity.updated_at);
|
|
158
|
+
const structuralBoost = params.structuralSearchBooster?.(entity, params.queryTokens, params.queryPhrase) ?? 0;
|
|
159
|
+
const evidenceBoost = testEvidenceBoost(entity, semantic, lexicalSemantic);
|
|
160
|
+
const secondaryScale = secondarySignalScale(Math.max(semantic, structuralBoost));
|
|
161
|
+
let score = 0;
|
|
162
|
+
score += params.ranking.semantic * semantic;
|
|
163
|
+
score += params.ranking.graph * graphScore * secondaryScale;
|
|
164
|
+
score += params.ranking.trust * trustScore * secondaryScale;
|
|
165
|
+
score += params.ranking.recency * dateScore * secondaryScale;
|
|
166
|
+
score += structuralBoost;
|
|
167
|
+
score += evidenceBoost;
|
|
168
|
+
score += params.legacyDataAccessBooster(entity, params.queryTokens, params.queryPhrase);
|
|
169
|
+
if (entity.source_of_truth) {
|
|
170
|
+
score += 0.1 * semantic;
|
|
171
|
+
}
|
|
172
|
+
const result = {
|
|
173
|
+
id: entity.id,
|
|
174
|
+
entity_type: entity.entity_type,
|
|
175
|
+
kind: entity.kind,
|
|
176
|
+
title: entity.label,
|
|
177
|
+
path: entity.path || undefined,
|
|
178
|
+
source_of_truth: entity.source_of_truth,
|
|
179
|
+
status: entity.status,
|
|
180
|
+
updated_at: entity.updated_at,
|
|
181
|
+
excerpt: entity.snippet,
|
|
182
|
+
...(params.includeScores
|
|
183
|
+
? {
|
|
184
|
+
score: Number(score.toFixed(4)),
|
|
185
|
+
semantic_score: Number(semantic.toFixed(4)),
|
|
186
|
+
embedding_score: Number(vectorSemantic.toFixed(4)),
|
|
187
|
+
lexical_score: Number(lexicalSemantic.toFixed(4)),
|
|
188
|
+
graph_score: Number(graphScore.toFixed(4))
|
|
189
|
+
}
|
|
190
|
+
: {}),
|
|
191
|
+
...(params.includeMatchedRules
|
|
192
|
+
? {
|
|
193
|
+
matched_rules: entity.matched_rules
|
|
194
|
+
}
|
|
195
|
+
: {}),
|
|
196
|
+
...(params.includeContent
|
|
197
|
+
? {
|
|
198
|
+
content: entity.content
|
|
199
|
+
}
|
|
200
|
+
: {})
|
|
201
|
+
};
|
|
202
|
+
if (entity.entity_type === "Chunk" && isWindowChunkId(entity.id)) {
|
|
203
|
+
const canonicalId = baseChunkId(entity.id);
|
|
204
|
+
const baseChunk = chunkCandidatesById.get(canonicalId);
|
|
205
|
+
if (baseChunk) {
|
|
206
|
+
result.id = canonicalId;
|
|
207
|
+
result.title = baseChunkLabel(baseChunk.label);
|
|
208
|
+
result.path = baseChunk.path || undefined;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const resultId = String(result.id);
|
|
212
|
+
const existing = bestById.get(resultId);
|
|
213
|
+
if (!existing || score > existing.rankScore) {
|
|
214
|
+
bestById.set(resultId, { result, rankScore: score });
|
|
215
|
+
}
|
|
216
|
+
if (bestById.size > pruneThreshold) {
|
|
217
|
+
pruneRankedResults(bestById, diversityPoolLimit);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const rankedResults = [...bestById.values()]
|
|
221
|
+
.sort(compareRankedResults)
|
|
222
|
+
.slice(0, diversityPoolLimit);
|
|
223
|
+
const results = selectDiverseResults(rankedResults, params.topK)
|
|
224
|
+
.sort(compareRankedResults)
|
|
225
|
+
.map((ranked) => ranked.result);
|
|
226
|
+
return { results, totalCandidates };
|
|
227
|
+
}
|
|
228
|
+
export function buildSearchResults(params) {
|
|
229
|
+
return buildSearchResultsWithStats(params).results;
|
|
230
|
+
}
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { reloadContextGraph } from "./graph.js";
|
|
5
|
+
import { runContextRules } from "./rules.js";
|
|
6
|
+
import { runContextImpact, runContextRelated, runContextSearch } from "./search.js";
|
|
7
|
+
import { getToolCallHook, getToolEventHook, getSessionEndHook, getSessionEventHook, loadPlugins, } from "./plugin.js";
|
|
8
|
+
const ESTIMATED_TOKENS_SAVED_PER_RESULT = 400;
|
|
9
|
+
const MAX_SESSION_CALLS = 1000;
|
|
10
|
+
const SHUTDOWN_TIMEOUT_MS = 3000;
|
|
11
|
+
const sessionCalls = [];
|
|
12
|
+
const sessionStartedAt = Date.now();
|
|
13
|
+
let successfulToolCalls = 0;
|
|
14
|
+
let failedToolCalls = 0;
|
|
15
|
+
const SearchInput = z.object({
|
|
16
|
+
query: z.string().min(1),
|
|
17
|
+
top_k: z.number().int().positive().max(20).default(5),
|
|
18
|
+
include_deprecated: z.boolean().default(false),
|
|
19
|
+
response_preset: z.enum(["full", "compact", "minimal"]).optional(),
|
|
20
|
+
include_scores: z.boolean().optional(),
|
|
21
|
+
include_matched_rules: z.boolean().optional(),
|
|
22
|
+
include_content: z.boolean().optional()
|
|
23
|
+
});
|
|
24
|
+
const RelatedInput = z.object({
|
|
25
|
+
entity_id: z.string().min(1),
|
|
26
|
+
depth: z.number().int().positive().max(3).default(1),
|
|
27
|
+
include_edges: z.boolean().optional(),
|
|
28
|
+
response_preset: z.enum(["full", "compact", "minimal"]).optional(),
|
|
29
|
+
include_entity_metadata: z.boolean().optional()
|
|
30
|
+
});
|
|
31
|
+
const ImpactInput = z
|
|
32
|
+
.object({
|
|
33
|
+
entity_id: z.string().min(1).optional(),
|
|
34
|
+
query: z.string().min(1).optional(),
|
|
35
|
+
depth: z.number().int().positive().max(4).default(2),
|
|
36
|
+
top_k: z.number().int().positive().max(20).default(8),
|
|
37
|
+
include_edges: z.boolean().default(true),
|
|
38
|
+
response_preset: z.enum(["full", "compact", "minimal"]).optional(),
|
|
39
|
+
include_scores: z.boolean().optional(),
|
|
40
|
+
include_reasons: z.boolean().optional(),
|
|
41
|
+
verbose_paths: z.boolean().optional(),
|
|
42
|
+
max_path_hops_shown: z.number().int().positive().max(8).optional(),
|
|
43
|
+
profile: z.enum(["all", "config_only", "config_to_sql", "code_only", "sql_only"]).default("all"),
|
|
44
|
+
sort_by: z
|
|
45
|
+
.enum(["impact_score", "shortest_path", "semantic_score", "graph_score", "trust_score"])
|
|
46
|
+
.default("impact_score"),
|
|
47
|
+
relation_types: z
|
|
48
|
+
.array(z.enum([
|
|
49
|
+
"CALLS",
|
|
50
|
+
"CALLS_SQL",
|
|
51
|
+
"IMPORTS",
|
|
52
|
+
"USES_CONFIG_KEY",
|
|
53
|
+
"USES_RESOURCE_KEY",
|
|
54
|
+
"USES_SETTING_KEY",
|
|
55
|
+
"USES_CONFIG",
|
|
56
|
+
"TRANSFORMS_CONFIG",
|
|
57
|
+
"PART_OF"
|
|
58
|
+
]))
|
|
59
|
+
.max(9)
|
|
60
|
+
.optional(),
|
|
61
|
+
path_must_include: z
|
|
62
|
+
.array(z.enum([
|
|
63
|
+
"CALLS",
|
|
64
|
+
"CALLS_SQL",
|
|
65
|
+
"IMPORTS",
|
|
66
|
+
"USES_CONFIG_KEY",
|
|
67
|
+
"USES_RESOURCE_KEY",
|
|
68
|
+
"USES_SETTING_KEY",
|
|
69
|
+
"USES_CONFIG",
|
|
70
|
+
"TRANSFORMS_CONFIG",
|
|
71
|
+
"PART_OF"
|
|
72
|
+
]))
|
|
73
|
+
.max(9)
|
|
74
|
+
.optional(),
|
|
75
|
+
path_must_exclude: z
|
|
76
|
+
.array(z.enum([
|
|
77
|
+
"CALLS",
|
|
78
|
+
"CALLS_SQL",
|
|
79
|
+
"IMPORTS",
|
|
80
|
+
"USES_CONFIG_KEY",
|
|
81
|
+
"USES_RESOURCE_KEY",
|
|
82
|
+
"USES_SETTING_KEY",
|
|
83
|
+
"USES_CONFIG",
|
|
84
|
+
"TRANSFORMS_CONFIG",
|
|
85
|
+
"PART_OF"
|
|
86
|
+
]))
|
|
87
|
+
.max(9)
|
|
88
|
+
.optional(),
|
|
89
|
+
result_domains: z
|
|
90
|
+
.array(z.enum(["code", "config", "resource", "settings", "sql", "project"]))
|
|
91
|
+
.max(6)
|
|
92
|
+
.optional(),
|
|
93
|
+
result_entity_types: z
|
|
94
|
+
.array(z.enum(["File", "Chunk", "Module", "Project", "ADR", "Rule"]))
|
|
95
|
+
.max(6)
|
|
96
|
+
.optional()
|
|
97
|
+
})
|
|
98
|
+
.refine((value) => Boolean(value.entity_id || value.query), {
|
|
99
|
+
message: "Either entity_id or query is required."
|
|
100
|
+
});
|
|
101
|
+
const RulesInput = z.object({
|
|
102
|
+
scope: z.string().optional(),
|
|
103
|
+
include_inactive: z.boolean().default(false)
|
|
104
|
+
});
|
|
105
|
+
const ReloadInput = z.object({
|
|
106
|
+
force: z.boolean().default(true)
|
|
107
|
+
});
|
|
108
|
+
function buildToolResult(data) {
|
|
109
|
+
return {
|
|
110
|
+
content: [
|
|
111
|
+
{
|
|
112
|
+
type: "text",
|
|
113
|
+
text: JSON.stringify(data, null, 2)
|
|
114
|
+
}
|
|
115
|
+
],
|
|
116
|
+
structuredContent: data
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function extractQuery(input) {
|
|
120
|
+
if (input && typeof input === "object" && "query" in input) {
|
|
121
|
+
const q = input.query;
|
|
122
|
+
if (typeof q === "string")
|
|
123
|
+
return q;
|
|
124
|
+
}
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
function notifyToolStart(toolName, input) {
|
|
128
|
+
const timestamp = new Date().toISOString();
|
|
129
|
+
const eventHook = getToolEventHook();
|
|
130
|
+
if (eventHook) {
|
|
131
|
+
const query = extractQuery(input);
|
|
132
|
+
void eventHook({
|
|
133
|
+
phase: "start",
|
|
134
|
+
tool: toolName,
|
|
135
|
+
timestamp,
|
|
136
|
+
input: (input ?? {}),
|
|
137
|
+
query,
|
|
138
|
+
query_length: query?.length,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return timestamp;
|
|
142
|
+
}
|
|
143
|
+
function notifyToolCall(toolName, input, result, durationMs, startedAtIso) {
|
|
144
|
+
const resultCount = Array.isArray(result.results)
|
|
145
|
+
? (result.results).length
|
|
146
|
+
: 0;
|
|
147
|
+
const query = extractQuery(input);
|
|
148
|
+
if (sessionCalls.length < MAX_SESSION_CALLS) {
|
|
149
|
+
sessionCalls.push({
|
|
150
|
+
tool: toolName,
|
|
151
|
+
query,
|
|
152
|
+
resultCount,
|
|
153
|
+
time: startedAtIso,
|
|
154
|
+
outcome: "success",
|
|
155
|
+
duration_ms: durationMs,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
successfulToolCalls++;
|
|
159
|
+
const eventHook = getToolEventHook();
|
|
160
|
+
const hook = getToolCallHook();
|
|
161
|
+
if (!eventHook && hook) {
|
|
162
|
+
hook(toolName, resultCount, resultCount * ESTIMATED_TOKENS_SAVED_PER_RESULT);
|
|
163
|
+
}
|
|
164
|
+
if (eventHook) {
|
|
165
|
+
void eventHook({
|
|
166
|
+
phase: "success",
|
|
167
|
+
tool: toolName,
|
|
168
|
+
timestamp: new Date().toISOString(),
|
|
169
|
+
input: (input ?? {}),
|
|
170
|
+
query,
|
|
171
|
+
query_length: query?.length,
|
|
172
|
+
result_count: resultCount,
|
|
173
|
+
estimated_tokens_saved: resultCount * ESTIMATED_TOKENS_SAVED_PER_RESULT,
|
|
174
|
+
duration_ms: durationMs,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function notifyToolError(toolName, input, error, durationMs, startedAtIso) {
|
|
179
|
+
const query = extractQuery(input);
|
|
180
|
+
if (sessionCalls.length < MAX_SESSION_CALLS) {
|
|
181
|
+
sessionCalls.push({
|
|
182
|
+
tool: toolName,
|
|
183
|
+
query,
|
|
184
|
+
resultCount: 0,
|
|
185
|
+
time: startedAtIso,
|
|
186
|
+
outcome: "error",
|
|
187
|
+
duration_ms: durationMs,
|
|
188
|
+
error: error instanceof Error ? error.message : String(error),
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
failedToolCalls++;
|
|
192
|
+
const eventHook = getToolEventHook();
|
|
193
|
+
if (eventHook) {
|
|
194
|
+
void eventHook({
|
|
195
|
+
phase: "error",
|
|
196
|
+
tool: toolName,
|
|
197
|
+
timestamp: new Date().toISOString(),
|
|
198
|
+
input: (input ?? {}),
|
|
199
|
+
query,
|
|
200
|
+
query_length: query?.length,
|
|
201
|
+
duration_ms: durationMs,
|
|
202
|
+
error: error instanceof Error ? error.message : String(error),
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
async function executeInstrumentedTool(toolName, input, run) {
|
|
207
|
+
const startedAt = Date.now();
|
|
208
|
+
const startedAtIso = notifyToolStart(toolName, input);
|
|
209
|
+
try {
|
|
210
|
+
const result = await run();
|
|
211
|
+
notifyToolCall(toolName, input, result, Date.now() - startedAt, startedAtIso);
|
|
212
|
+
return buildToolResult(result);
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
notifyToolError(toolName, input, error, Date.now() - startedAt, startedAtIso);
|
|
216
|
+
throw error;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function registerTools(server) {
|
|
220
|
+
server.registerTool("context.search", {
|
|
221
|
+
description: "Search ranked context documents and code using semantic, graph and trust weighting.",
|
|
222
|
+
inputSchema: SearchInput
|
|
223
|
+
}, async (input) => executeInstrumentedTool("context.search", input, async () => runContextSearch(SearchInput.parse(input ?? {}))));
|
|
224
|
+
server.registerTool("context.get_related", {
|
|
225
|
+
description: "Return related entities and graph edges for a context entity id.",
|
|
226
|
+
inputSchema: RelatedInput
|
|
227
|
+
}, async (input) => executeInstrumentedTool("context.get_related", input, async () => runContextRelated(RelatedInput.parse(input ?? {}))));
|
|
228
|
+
server.registerTool("context.impact", {
|
|
229
|
+
description: "Traverse likely impact paths across config, code and SQL starting from an entity id or query.",
|
|
230
|
+
inputSchema: ImpactInput
|
|
231
|
+
}, async (input) => executeInstrumentedTool("context.impact", input, async () => runContextImpact(ImpactInput.parse(input ?? {}))));
|
|
232
|
+
server.registerTool("context.get_rules", {
|
|
233
|
+
description: "List indexed rules filtered by scope and active status.",
|
|
234
|
+
inputSchema: RulesInput.optional()
|
|
235
|
+
}, async (input) => executeInstrumentedTool("context.get_rules", input, async () => runContextRules(RulesInput.parse(input ?? {}))));
|
|
236
|
+
server.registerTool("context.reload", {
|
|
237
|
+
description: "Reload RyuGraph connection after graph updates or maintenance.",
|
|
238
|
+
inputSchema: ReloadInput.optional()
|
|
239
|
+
}, async (input) => executeInstrumentedTool("context.reload", input, async () => {
|
|
240
|
+
const parsed = ReloadInput.parse(input ?? {});
|
|
241
|
+
return reloadContextGraph(parsed.force);
|
|
242
|
+
}));
|
|
243
|
+
// Note: cortex.workflow.* tools (the Cortex Harness) are enterprise-only
|
|
244
|
+
// and registered by enterprise/index.ts::register() once the license has
|
|
245
|
+
// verified. They intentionally do not appear here so community-mode MCP
|
|
246
|
+
// servers do not surface them at all.
|
|
247
|
+
}
|
|
248
|
+
let shutdownCalled = false;
|
|
249
|
+
async function onShutdown() {
|
|
250
|
+
if (shutdownCalled)
|
|
251
|
+
return;
|
|
252
|
+
shutdownCalled = true;
|
|
253
|
+
const sessionEventHook = getSessionEventHook();
|
|
254
|
+
if (sessionEventHook) {
|
|
255
|
+
try {
|
|
256
|
+
await Promise.race([
|
|
257
|
+
Promise.resolve(sessionEventHook({
|
|
258
|
+
phase: "end",
|
|
259
|
+
timestamp: new Date().toISOString(),
|
|
260
|
+
duration_ms: Date.now() - sessionStartedAt,
|
|
261
|
+
tool_calls: sessionCalls.length,
|
|
262
|
+
successful_tool_calls: successfulToolCalls,
|
|
263
|
+
failed_tool_calls: failedToolCalls,
|
|
264
|
+
calls: [...sessionCalls],
|
|
265
|
+
})),
|
|
266
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("session event hook timeout")), SHUTDOWN_TIMEOUT_MS))
|
|
267
|
+
]);
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
// Best effort — don't block shutdown
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const hook = getSessionEndHook();
|
|
274
|
+
if (hook) {
|
|
275
|
+
try {
|
|
276
|
+
await Promise.race([
|
|
277
|
+
hook([...sessionCalls]),
|
|
278
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("shutdown hook timeout")), SHUTDOWN_TIMEOUT_MS))
|
|
279
|
+
]);
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
// Best effort — don't block shutdown
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
async function main() {
|
|
287
|
+
const server = new McpServer({
|
|
288
|
+
name: "cortex-context",
|
|
289
|
+
version: "0.1.0"
|
|
290
|
+
});
|
|
291
|
+
registerTools(server);
|
|
292
|
+
// v2.0.0: load enterprise plugin in-process if .context/enterprise.yml
|
|
293
|
+
// is present and license validates. Community-mode is a no-op.
|
|
294
|
+
await loadPlugins(server);
|
|
295
|
+
// Notify session start to enterprise (if active).
|
|
296
|
+
const sessionEventHook = getSessionEventHook();
|
|
297
|
+
if (sessionEventHook) {
|
|
298
|
+
void sessionEventHook({
|
|
299
|
+
phase: "start",
|
|
300
|
+
timestamp: new Date(sessionStartedAt).toISOString(),
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
const transport = new StdioServerTransport();
|
|
304
|
+
await server.connect(transport);
|
|
305
|
+
const cleanup = () => {
|
|
306
|
+
void onShutdown().finally(() => process.exit(0));
|
|
307
|
+
};
|
|
308
|
+
process.on("SIGINT", cleanup);
|
|
309
|
+
process.on("SIGTERM", cleanup);
|
|
310
|
+
process.on("beforeExit", () => {
|
|
311
|
+
void onShutdown();
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
main().catch((error) => {
|
|
315
|
+
process.stderr.write(`${error instanceof Error ? error.message : "Fatal error"}\n`);
|
|
316
|
+
process.exit(1);
|
|
317
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|