@danielblomma/cortex-mcp 0.4.5 → 0.6.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.
Files changed (61) hide show
  1. package/README.md +38 -42
  2. package/bin/cortex.mjs +32 -60
  3. package/package.json +15 -3
  4. package/scaffold/.context/ontology.cypher +47 -0
  5. package/scaffold/.githooks/post-commit +14 -0
  6. package/scaffold/.githooks/post-rewrite +23 -0
  7. package/scaffold/mcp/package-lock.json +16 -16
  8. package/scaffold/mcp/package.json +3 -1
  9. package/scaffold/mcp/src/contextEntities.ts +311 -0
  10. package/scaffold/mcp/src/defaults.ts +6 -0
  11. package/scaffold/mcp/src/embed.ts +163 -37
  12. package/scaffold/mcp/src/frontmatter.ts +39 -0
  13. package/scaffold/mcp/src/graph.ts +253 -130
  14. package/scaffold/mcp/src/graphMetrics.ts +12 -0
  15. package/scaffold/mcp/src/impactPresentation.ts +202 -0
  16. package/scaffold/mcp/src/impactRanking.ts +237 -0
  17. package/scaffold/mcp/src/impactResponse.ts +47 -0
  18. package/scaffold/mcp/src/impactResults.ts +173 -0
  19. package/scaffold/mcp/src/impactSeed.ts +33 -0
  20. package/scaffold/mcp/src/impactTraversal.ts +83 -0
  21. package/scaffold/mcp/src/jsonl.ts +34 -0
  22. package/scaffold/mcp/src/loadGraph.ts +345 -86
  23. package/scaffold/mcp/src/paths.ts +17 -1
  24. package/scaffold/mcp/src/presets.ts +137 -0
  25. package/scaffold/mcp/src/relatedResponse.ts +30 -0
  26. package/scaffold/mcp/src/relatedTraversal.ts +101 -0
  27. package/scaffold/mcp/src/rules.ts +27 -0
  28. package/scaffold/mcp/src/search.ts +186 -455
  29. package/scaffold/mcp/src/searchCore.ts +274 -0
  30. package/scaffold/mcp/src/searchResults.ts +133 -0
  31. package/scaffold/mcp/src/server.ts +95 -3
  32. package/scaffold/mcp/src/types.ts +82 -3
  33. package/scaffold/scripts/context.sh +12 -46
  34. package/scaffold/scripts/dashboard.mjs +797 -0
  35. package/scaffold/scripts/dashboard.sh +13 -0
  36. package/scaffold/scripts/ingest.mjs +2219 -59
  37. package/scaffold/scripts/install-git-hooks.sh +3 -1
  38. package/scaffold/scripts/memory-compile.mjs +232 -0
  39. package/scaffold/scripts/memory-compile.sh +20 -0
  40. package/scaffold/scripts/memory-lint.mjs +375 -0
  41. package/scaffold/scripts/memory-lint.sh +20 -0
  42. package/scaffold/scripts/parsers/config.mjs +178 -0
  43. package/scaffold/scripts/parsers/cpp.mjs +316 -0
  44. package/scaffold/scripts/parsers/dotnet/VbNetParser/Program.cs +374 -0
  45. package/scaffold/scripts/parsers/dotnet/VbNetParser/VbNetParser.csproj +13 -0
  46. package/scaffold/scripts/parsers/javascript/ast.mjs +61 -0
  47. package/scaffold/scripts/parsers/javascript/calls.mjs +53 -0
  48. package/scaffold/scripts/parsers/javascript/chunks.mjs +388 -0
  49. package/scaffold/scripts/parsers/javascript/imports.mjs +162 -0
  50. package/scaffold/scripts/parsers/javascript/patterns.mjs +82 -0
  51. package/scaffold/scripts/parsers/javascript/scope-analysis.mjs +3 -0
  52. package/scaffold/scripts/parsers/javascript/scope-builder.mjs +305 -0
  53. package/scaffold/scripts/parsers/javascript/scope-resolver.mjs +82 -0
  54. package/scaffold/scripts/parsers/javascript.mjs +27 -350
  55. package/scaffold/scripts/parsers/resources.mjs +166 -0
  56. package/scaffold/scripts/parsers/sql.mjs +137 -0
  57. package/scaffold/scripts/parsers/vbnet.mjs +143 -0
  58. package/scaffold/scripts/status.sh +0 -7
  59. package/scaffold/scripts/capture-note.sh +0 -55
  60. package/scaffold/scripts/plan-state-engine.cjs +0 -310
  61. package/scaffold/scripts/plan-state.sh +0 -71
@@ -0,0 +1,274 @@
1
+ import type { SearchEntity } from "./types.js";
2
+
3
+ const SQL_ENTITY_KINDS = new Set(["procedure", "view", "function", "table", "trigger"]);
4
+ const SQL_LIKE_EXTENSIONS = [".sql"];
5
+ const CONFIG_LIKE_EXTENSIONS = [".config"];
6
+ const RESOURCE_LIKE_EXTENSIONS = [".resx"];
7
+ const SETTINGS_LIKE_EXTENSIONS = [".settings"];
8
+ const CONFIG_ENVIRONMENT_TOKENS = [
9
+ "release",
10
+ "debug",
11
+ "prod",
12
+ "production",
13
+ "staging",
14
+ "stage",
15
+ "dev",
16
+ "development",
17
+ "test",
18
+ "qa",
19
+ "uat"
20
+ ];
21
+
22
+ const QUERY_TOKEN_EXPANSIONS: Record<string, string[]> = {
23
+ semantisk: ["semantic"],
24
+ sökning: ["search"],
25
+ sokning: ["search"],
26
+ regel: ["rule"],
27
+ regler: ["rules"],
28
+ relaterad: ["related"],
29
+ meddelande: ["message"],
30
+ avvikelse: ["deviation"]
31
+ };
32
+
33
+ export function normalizeText(value: string): string {
34
+ return value.normalize("NFKC").toLowerCase();
35
+ }
36
+
37
+ export function tokenize(value: string): string[] {
38
+ return normalizeText(value)
39
+ .split(/[^\p{L}\p{N}]+/gu)
40
+ .map((part) => part.trim())
41
+ .filter((part) => part.length >= 2);
42
+ }
43
+
44
+ export function expandQueryTokens(tokens: string[]): string[] {
45
+ const expanded = new Set<string>(tokens);
46
+ for (const token of tokens) {
47
+ const aliases = QUERY_TOKEN_EXPANSIONS[token];
48
+ if (!aliases) {
49
+ continue;
50
+ }
51
+ for (const alias of aliases) {
52
+ expanded.add(alias);
53
+ }
54
+ }
55
+ return Array.from(expanded);
56
+ }
57
+
58
+ function daysSince(isoDate: string): number {
59
+ const timestamp = Date.parse(isoDate);
60
+ if (Number.isNaN(timestamp)) {
61
+ return 3650;
62
+ }
63
+
64
+ const now = Date.now();
65
+ return Math.max(0, (now - timestamp) / (1000 * 60 * 60 * 24));
66
+ }
67
+
68
+ export function recencyScore(isoDate: string): number {
69
+ const days = daysSince(isoDate);
70
+ return 1 / (1 + days / 30);
71
+ }
72
+
73
+ export function semanticScore(queryTokens: string[], queryPhrase: string, text: string): number {
74
+ if (queryTokens.length === 0) {
75
+ return 0;
76
+ }
77
+
78
+ const textTokenSet = new Set(tokenize(text));
79
+ if (textTokenSet.size === 0) {
80
+ return 0;
81
+ }
82
+
83
+ let matched = 0;
84
+ for (const token of queryTokens) {
85
+ if (textTokenSet.has(token)) {
86
+ matched += 1;
87
+ }
88
+ }
89
+
90
+ const overlap = matched / queryTokens.length;
91
+ if (overlap <= 0) {
92
+ return 0;
93
+ }
94
+
95
+ const normalizedText = normalizeText(text);
96
+ const phraseBonus = queryPhrase && normalizedText.includes(queryPhrase) ? 0.15 : 0;
97
+ return Math.min(1, overlap * 0.85 + phraseBonus);
98
+ }
99
+
100
+ function queryHasAnyToken(queryTokens: string[], candidates: string[]): boolean {
101
+ return candidates.some((candidate) => queryTokens.includes(candidate));
102
+ }
103
+
104
+ function pathHasExtension(pathValue: string, extensions: string[]): boolean {
105
+ const normalizedPath = normalizeText(pathValue);
106
+ return extensions.some((extension) => normalizedPath.endsWith(extension));
107
+ }
108
+
109
+ export function legacyDataAccessBoost(entity: SearchEntity, queryTokens: string[], queryPhrase: string): number {
110
+ const normalizedKind = normalizeText(entity.kind);
111
+ const wantsSql =
112
+ queryHasAnyToken(queryTokens, [
113
+ "sql",
114
+ "database",
115
+ "db",
116
+ "provider",
117
+ "providername",
118
+ "sqlclient",
119
+ "sqlserver",
120
+ "oracle",
121
+ "postgres",
122
+ "postgresql",
123
+ "pgsql",
124
+ "mysql",
125
+ "sqlite",
126
+ "stored",
127
+ "procedure",
128
+ "proc",
129
+ "query",
130
+ "queries",
131
+ "view",
132
+ "table",
133
+ "trigger",
134
+ "report",
135
+ "reporting",
136
+ "data",
137
+ "dataflow"
138
+ ]) || queryPhrase.includes("stored procedure");
139
+ const wantsConfig =
140
+ queryHasAnyToken(queryTokens, [
141
+ "config",
142
+ "configuration",
143
+ "connection",
144
+ "connectionstring",
145
+ "connectionstrings",
146
+ "appsettings",
147
+ "setting",
148
+ "settings"
149
+ ]) || queryPhrase.includes("connection string");
150
+ const wantsResource = queryHasAnyToken(queryTokens, ["resource", "resources", "resx"]);
151
+ const wantsSettings = queryHasAnyToken(queryTokens, ["setting", "settings", "appsettings"]);
152
+ const wantsConfigTransform =
153
+ queryHasAnyToken(queryTokens, [...CONFIG_ENVIRONMENT_TOKENS, "transform", "xdt", "override"]) ||
154
+ queryPhrase.includes("web.release.config") ||
155
+ queryPhrase.includes("web.debug.config");
156
+ const wantsMachineConfig = queryHasAnyToken(queryTokens, ["machine", "machineconfig"]);
157
+ const wantsImpact = queryHasAnyToken(queryTokens, [
158
+ "impact",
159
+ "affect",
160
+ "affected",
161
+ "affects",
162
+ "change",
163
+ "changes",
164
+ "changing",
165
+ "override",
166
+ "overrides"
167
+ ]);
168
+
169
+ let boost = 0;
170
+
171
+ if (entity.entity_type === "Chunk") {
172
+ if (normalizedKind === "connection_string" && (wantsConfig || wantsSql)) {
173
+ boost += 0.16;
174
+ } else if (
175
+ normalizedKind === "database_target" &&
176
+ (wantsConfig ||
177
+ wantsSql ||
178
+ queryHasAnyToken(queryTokens, [
179
+ "database",
180
+ "server",
181
+ "catalog",
182
+ "provider",
183
+ "providername",
184
+ "sqlclient",
185
+ "sqlserver",
186
+ "oracle",
187
+ "postgres",
188
+ "postgresql",
189
+ "pgsql",
190
+ "mysql",
191
+ "sqlite"
192
+ ]))
193
+ ) {
194
+ boost += 0.18;
195
+ } else if (normalizedKind === "app_setting" && (wantsConfig || wantsSettings)) {
196
+ boost += 0.12;
197
+ } else if (normalizedKind === "resource_entry" && (wantsResource || wantsSql)) {
198
+ boost += 0.1;
199
+ } else if (normalizedKind === "setting_entry" && (wantsSettings || wantsConfig || wantsSql)) {
200
+ boost += 0.1;
201
+ } else if (SQL_ENTITY_KINDS.has(normalizedKind) && wantsSql) {
202
+ boost += 0.12;
203
+ }
204
+ if (
205
+ wantsImpact &&
206
+ (normalizedKind === "connection_string" ||
207
+ normalizedKind === "database_target" ||
208
+ normalizedKind === "app_setting" ||
209
+ SQL_ENTITY_KINDS.has(normalizedKind))
210
+ ) {
211
+ boost += 0.08;
212
+ }
213
+ }
214
+
215
+ if (entity.entity_type === "File") {
216
+ if (pathHasExtension(entity.path, SQL_LIKE_EXTENSIONS) && wantsSql) {
217
+ boost += 0.04;
218
+ }
219
+ if (pathHasExtension(entity.path, CONFIG_LIKE_EXTENSIONS) && wantsConfig) {
220
+ boost += 0.06;
221
+ }
222
+ if (
223
+ pathHasExtension(entity.path, CONFIG_LIKE_EXTENSIONS) &&
224
+ wantsConfigTransform &&
225
+ CONFIG_ENVIRONMENT_TOKENS.some((token) => normalizeText(entity.path).includes(`.${token}.config`))
226
+ ) {
227
+ boost += 0.12;
228
+ }
229
+ if (
230
+ pathHasExtension(entity.path, CONFIG_LIKE_EXTENSIONS) &&
231
+ wantsMachineConfig &&
232
+ normalizeText(entity.path).endsWith("machine.config")
233
+ ) {
234
+ boost += 0.12;
235
+ }
236
+ if (
237
+ pathHasExtension(entity.path, CONFIG_LIKE_EXTENSIONS) &&
238
+ wantsImpact &&
239
+ (wantsConfig || wantsConfigTransform || wantsSql)
240
+ ) {
241
+ boost += 0.08;
242
+ }
243
+ if (pathHasExtension(entity.path, RESOURCE_LIKE_EXTENSIONS) && (wantsResource || wantsSql)) {
244
+ boost += 0.05;
245
+ }
246
+ if (pathHasExtension(entity.path, SETTINGS_LIKE_EXTENSIONS) && (wantsSettings || wantsConfig)) {
247
+ boost += 0.05;
248
+ }
249
+ }
250
+
251
+ return boost;
252
+ }
253
+
254
+ export function cosineSimilarity(a: number[], b: number[]): number {
255
+ if (a.length === 0 || b.length === 0 || a.length !== b.length) {
256
+ return 0;
257
+ }
258
+
259
+ let dot = 0;
260
+ let normA = 0;
261
+ let normB = 0;
262
+ for (let index = 0; index < a.length; index += 1) {
263
+ const av = a[index];
264
+ const bv = b[index];
265
+ dot += av * bv;
266
+ normA += av * av;
267
+ normB += bv * bv;
268
+ }
269
+
270
+ if (normA === 0 || normB === 0) {
271
+ return 0;
272
+ }
273
+ return dot / (Math.sqrt(normA) * Math.sqrt(normB));
274
+ }
@@ -0,0 +1,133 @@
1
+ import type { RankingWeights, SearchEntity } from "./types.js";
2
+
3
+ function isWindowChunkId(id: string): boolean {
4
+ return id.includes(":window:");
5
+ }
6
+
7
+ function baseChunkId(id: string): string {
8
+ const markerIndex = id.indexOf(":window:");
9
+ return markerIndex === -1 ? id : id.slice(0, markerIndex);
10
+ }
11
+
12
+ function baseChunkLabel(label: string): string {
13
+ const markerIndex = label.indexOf("#window");
14
+ return markerIndex === -1 ? label : label.slice(0, markerIndex);
15
+ }
16
+
17
+ export function buildSearchResults(params: {
18
+ candidates: SearchEntity[];
19
+ degreeByEntity: Map<string, number>;
20
+ queryTokens: string[];
21
+ queryPhrase: string;
22
+ ranking: RankingWeights;
23
+ includeScores: boolean;
24
+ includeMatchedRules: boolean;
25
+ includeContent: boolean;
26
+ queryVector: number[] | null;
27
+ embeddingVectors: Map<string, number[]>;
28
+ topK: number;
29
+ minLexicalRelevance: number;
30
+ minVectorRelevance: number;
31
+ semanticScorer: (queryTokens: string[], queryPhrase: string, text: string) => number;
32
+ vectorScorer: (a: number[], b: number[]) => number;
33
+ recencyScorer: (isoDate: string) => number;
34
+ legacyDataAccessBooster: (entity: SearchEntity, queryTokens: string[], queryPhrase: string) => number;
35
+ }): Record<string, unknown>[] {
36
+ const rawResults = params.candidates
37
+ .map((entity) => {
38
+ const lexicalSemantic = params.semanticScorer(params.queryTokens, params.queryPhrase, entity.text);
39
+ const entityVector = params.embeddingVectors.get(entity.id);
40
+ const vectorSemantic =
41
+ params.queryVector && entityVector
42
+ ? Math.max(0, Math.min(1, params.vectorScorer(params.queryVector, entityVector)))
43
+ : 0;
44
+ const hasRelevanceSignal =
45
+ lexicalSemantic >= params.minLexicalRelevance || vectorSemantic >= params.minVectorRelevance;
46
+ if (!hasRelevanceSignal) {
47
+ return null;
48
+ }
49
+
50
+ const semantic =
51
+ vectorSemantic > 0 ? vectorSemantic * 0.75 + lexicalSemantic * 0.25 : lexicalSemantic;
52
+ const graphScore = Math.min(1, (params.degreeByEntity.get(entity.id) ?? 0) / 4);
53
+ const trustScore = Math.max(0, Math.min(1, entity.trust_level / 100));
54
+ const dateScore = params.recencyScorer(entity.updated_at);
55
+
56
+ let score = 0;
57
+ score += params.ranking.semantic * semantic;
58
+ score += params.ranking.graph * graphScore;
59
+ score += params.ranking.trust * trustScore;
60
+ score += params.ranking.recency * dateScore;
61
+ score += params.legacyDataAccessBooster(entity, params.queryTokens, params.queryPhrase);
62
+
63
+ if (entity.source_of_truth) {
64
+ score += 0.1 * semantic;
65
+ }
66
+
67
+ return {
68
+ id: entity.id,
69
+ entity_type: entity.entity_type,
70
+ kind: entity.kind,
71
+ title: entity.label,
72
+ path: entity.path || undefined,
73
+ source_of_truth: entity.source_of_truth,
74
+ status: entity.status,
75
+ updated_at: entity.updated_at,
76
+ excerpt: entity.snippet,
77
+ ...(params.includeScores
78
+ ? {
79
+ score: Number(score.toFixed(4)),
80
+ semantic_score: Number(semantic.toFixed(4)),
81
+ embedding_score: Number(vectorSemantic.toFixed(4)),
82
+ lexical_score: Number(lexicalSemantic.toFixed(4)),
83
+ graph_score: Number(graphScore.toFixed(4))
84
+ }
85
+ : {}),
86
+ ...(params.includeMatchedRules
87
+ ? {
88
+ matched_rules: entity.matched_rules
89
+ }
90
+ : {}),
91
+ ...(params.includeContent
92
+ ? {
93
+ content: entity.content
94
+ }
95
+ : {})
96
+ };
97
+ })
98
+ .filter((result): result is NonNullable<typeof result> => result !== null)
99
+ .sort((a, b) => Number(b.score ?? 0) - Number(a.score ?? 0));
100
+
101
+ const chunkCandidatesById = new Map(
102
+ params.candidates.filter((entity) => entity.entity_type === "Chunk").map((entity) => [entity.id, entity])
103
+ );
104
+ const normalizedById = new Map<string, (typeof rawResults)[number]>();
105
+
106
+ for (const result of rawResults) {
107
+ if (result.entity_type !== "Chunk" || !isWindowChunkId(String(result.id))) {
108
+ if (!normalizedById.has(String(result.id))) {
109
+ normalizedById.set(String(result.id), result);
110
+ }
111
+ continue;
112
+ }
113
+
114
+ const canonicalId = baseChunkId(String(result.id));
115
+ const baseChunk = chunkCandidatesById.get(canonicalId);
116
+ const normalizedResult = baseChunk
117
+ ? {
118
+ ...result,
119
+ id: canonicalId,
120
+ title: baseChunkLabel(baseChunk.label),
121
+ path: baseChunk.path || undefined
122
+ }
123
+ : result;
124
+ const existing = normalizedById.get(String(normalizedResult.id));
125
+ if (!existing || Number(normalizedResult.score ?? 0) > Number(existing.score ?? 0)) {
126
+ normalizedById.set(String(normalizedResult.id), normalizedResult);
127
+ }
128
+ }
129
+
130
+ return [...normalizedById.values()]
131
+ .sort((a, b) => Number(b.score ?? 0) - Number(a.score ?? 0))
132
+ .slice(0, params.topK);
133
+ }
@@ -2,7 +2,8 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
3
  import { z } from "zod";
4
4
  import { reloadContextGraph } from "./graph.js";
5
- import { runContextRelated, runContextRules, runContextSearch } from "./search.js";
5
+ import { runContextRules } from "./rules.js";
6
+ import { runContextImpact, runContextRelated, runContextSearch } from "./search.js";
6
7
 
7
8
  type ToolPayload = Record<string, unknown>;
8
9
 
@@ -10,15 +11,97 @@ const SearchInput = z.object({
10
11
  query: z.string().min(1),
11
12
  top_k: z.number().int().positive().max(20).default(5),
12
13
  include_deprecated: z.boolean().default(false),
13
- include_content: z.boolean().default(false)
14
+ response_preset: z.enum(["full", "compact", "minimal"]).optional(),
15
+ include_scores: z.boolean().optional(),
16
+ include_matched_rules: z.boolean().optional(),
17
+ include_content: z.boolean().optional()
14
18
  });
15
19
 
16
20
  const RelatedInput = z.object({
17
21
  entity_id: z.string().min(1),
18
22
  depth: z.number().int().positive().max(3).default(1),
19
- include_edges: z.boolean().default(true)
23
+ include_edges: z.boolean().optional(),
24
+ response_preset: z.enum(["full", "compact", "minimal"]).optional(),
25
+ include_entity_metadata: z.boolean().optional()
20
26
  });
21
27
 
28
+ const ImpactInput = z
29
+ .object({
30
+ entity_id: z.string().min(1).optional(),
31
+ query: z.string().min(1).optional(),
32
+ depth: z.number().int().positive().max(4).default(2),
33
+ top_k: z.number().int().positive().max(20).default(8),
34
+ include_edges: z.boolean().default(true),
35
+ response_preset: z.enum(["full", "compact", "minimal"]).optional(),
36
+ include_scores: z.boolean().optional(),
37
+ include_reasons: z.boolean().optional(),
38
+ verbose_paths: z.boolean().optional(),
39
+ max_path_hops_shown: z.number().int().positive().max(8).optional(),
40
+ profile: z.enum(["all", "config_only", "config_to_sql", "code_only", "sql_only"]).default("all"),
41
+ sort_by: z
42
+ .enum(["impact_score", "shortest_path", "semantic_score", "graph_score", "trust_score"])
43
+ .default("impact_score"),
44
+ relation_types: z
45
+ .array(
46
+ z.enum([
47
+ "CALLS",
48
+ "CALLS_SQL",
49
+ "IMPORTS",
50
+ "USES_CONFIG_KEY",
51
+ "USES_RESOURCE_KEY",
52
+ "USES_SETTING_KEY",
53
+ "USES_CONFIG",
54
+ "TRANSFORMS_CONFIG",
55
+ "PART_OF"
56
+ ])
57
+ )
58
+ .max(9)
59
+ .optional(),
60
+ path_must_include: z
61
+ .array(
62
+ 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
+ )
74
+ .max(9)
75
+ .optional(),
76
+ path_must_exclude: z
77
+ .array(
78
+ z.enum([
79
+ "CALLS",
80
+ "CALLS_SQL",
81
+ "IMPORTS",
82
+ "USES_CONFIG_KEY",
83
+ "USES_RESOURCE_KEY",
84
+ "USES_SETTING_KEY",
85
+ "USES_CONFIG",
86
+ "TRANSFORMS_CONFIG",
87
+ "PART_OF"
88
+ ])
89
+ )
90
+ .max(9)
91
+ .optional(),
92
+ result_domains: z
93
+ .array(z.enum(["code", "config", "resource", "settings", "sql", "project"]))
94
+ .max(6)
95
+ .optional(),
96
+ result_entity_types: z
97
+ .array(z.enum(["File", "Chunk", "Module", "Project", "ADR", "Rule"]))
98
+ .max(6)
99
+ .optional()
100
+ })
101
+ .refine((value) => Boolean(value.entity_id || value.query), {
102
+ message: "Either entity_id or query is required."
103
+ });
104
+
22
105
  const RulesInput = z.object({
23
106
  scope: z.string().optional(),
24
107
  include_inactive: z.boolean().default(false)
@@ -59,6 +142,15 @@ function registerTools(server: McpServer): void {
59
142
  async (input) => buildToolResult(await runContextRelated(RelatedInput.parse(input ?? {})))
60
143
  );
61
144
 
145
+ server.registerTool(
146
+ "context.impact",
147
+ {
148
+ description: "Traverse likely impact paths across config, code and SQL starting from an entity id or query.",
149
+ inputSchema: ImpactInput
150
+ },
151
+ async (input) => buildToolResult(await runContextImpact(ImpactInput.parse(input ?? {})))
152
+ );
153
+
62
154
  server.registerTool(
63
155
  "context.get_rules",
64
156
  {
@@ -38,10 +38,32 @@ export type AdrRecord = {
38
38
  status: string;
39
39
  };
40
40
 
41
+ export type RelationType =
42
+ | "CONSTRAINS"
43
+ | "IMPLEMENTS"
44
+ | "SUPERSEDES"
45
+ | "DEFINES"
46
+ | "CALLS"
47
+ | "IMPORTS"
48
+ | "CALLS_SQL"
49
+ | "USES_CONFIG_KEY"
50
+ | "USES_RESOURCE_KEY"
51
+ | "USES_SETTING_KEY"
52
+ | "PART_OF"
53
+ | "CONTAINS"
54
+ | "CONTAINS_MODULE"
55
+ | "EXPORTS"
56
+ | "INCLUDES_FILE"
57
+ | "REFERENCES_PROJECT"
58
+ | "USES_RESOURCE"
59
+ | "USES_SETTING"
60
+ | "USES_CONFIG"
61
+ | "TRANSFORMS_CONFIG";
62
+
41
63
  export type RelationRecord = {
42
64
  from: string;
43
65
  to: string;
44
- relation: "CONSTRAINS" | "IMPLEMENTS" | "SUPERSEDES" | "CALLS" | "IMPORTS" | "PART_OF";
66
+ relation: RelationType;
45
67
  note: string;
46
68
  };
47
69
 
@@ -52,9 +74,39 @@ export type ChunkRecord = {
52
74
  kind: string;
53
75
  signature: string;
54
76
  body: string;
77
+ description: string;
55
78
  start_line: number;
56
79
  end_line: number;
57
80
  language: string;
81
+ exported: boolean;
82
+ updated_at: string;
83
+ source_of_truth: boolean;
84
+ trust_level: number;
85
+ status: string;
86
+ };
87
+
88
+ export type ModuleRecord = {
89
+ id: string;
90
+ path: string;
91
+ name: string;
92
+ summary: string;
93
+ file_count: number;
94
+ exported_symbols: string;
95
+ updated_at: string;
96
+ source_of_truth: boolean;
97
+ trust_level: number;
98
+ status: string;
99
+ };
100
+
101
+ export type ProjectRecord = {
102
+ id: string;
103
+ path: string;
104
+ name: string;
105
+ kind: string;
106
+ language: string;
107
+ target_framework: string;
108
+ summary: string;
109
+ file_count: number;
58
110
  updated_at: string;
59
111
  source_of_truth: boolean;
60
112
  trust_level: number;
@@ -73,6 +125,8 @@ export type ContextData = {
73
125
  adrs: AdrRecord[];
74
126
  rules: RuleRecord[];
75
127
  chunks: ChunkRecord[];
128
+ modules: ModuleRecord[];
129
+ projects: ProjectRecord[];
76
130
  relations: RelationRecord[];
77
131
  ranking: RankingWeights;
78
132
  source: "cache" | "ryu";
@@ -81,7 +135,7 @@ export type ContextData = {
81
135
 
82
136
  export type SearchEntity = {
83
137
  id: string;
84
- entity_type: "File" | "Rule" | "ADR" | "Chunk";
138
+ entity_type: "File" | "Rule" | "ADR" | "Chunk" | "Module" | "Project";
85
139
  kind: string;
86
140
  label: string;
87
141
  path: string;
@@ -105,13 +159,38 @@ export type SearchParams = {
105
159
  query: string;
106
160
  top_k: number;
107
161
  include_deprecated: boolean;
108
- include_content: boolean;
162
+ response_preset?: "full" | "compact" | "minimal";
163
+ include_scores?: boolean;
164
+ include_matched_rules?: boolean;
165
+ include_content?: boolean;
109
166
  };
110
167
 
111
168
  export type RelatedParams = {
112
169
  entity_id: string;
113
170
  depth: number;
171
+ include_edges?: boolean;
172
+ response_preset?: "full" | "compact" | "minimal";
173
+ include_entity_metadata?: boolean;
174
+ };
175
+
176
+ export type ImpactParams = {
177
+ entity_id?: string;
178
+ query?: string;
179
+ depth: number;
180
+ top_k: number;
114
181
  include_edges: boolean;
182
+ response_preset?: "full" | "compact" | "minimal";
183
+ include_scores?: boolean;
184
+ include_reasons?: boolean;
185
+ verbose_paths?: boolean;
186
+ max_path_hops_shown?: number;
187
+ profile?: "all" | "config_only" | "config_to_sql" | "code_only" | "sql_only";
188
+ sort_by?: "impact_score" | "shortest_path" | "semantic_score" | "graph_score" | "trust_score";
189
+ relation_types?: RelationType[];
190
+ path_must_include?: RelationType[];
191
+ path_must_exclude?: RelationType[];
192
+ result_domains?: ("code" | "config" | "resource" | "settings" | "sql" | "project")[];
193
+ result_entity_types?: ("File" | "Chunk" | "Module" | "Project" | "ADR" | "Rule")[];
115
194
  };
116
195
 
117
196
  export type RulesParams = {