@danielblomma/cortex-mcp 0.4.2 → 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 +64 -16
  2. package/bin/cortex.mjs +32 -60
  3. package/package.json +17 -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 +19 -23
  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 +330 -109
  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 +24 -2
  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 +191 -355
  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 +99 -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 +15 -8
  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,13 +38,81 @@ 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";
66
+ relation: RelationType;
45
67
  note: string;
46
68
  };
47
69
 
70
+ export type ChunkRecord = {
71
+ id: string;
72
+ file_id: string;
73
+ name: string;
74
+ kind: string;
75
+ signature: string;
76
+ body: string;
77
+ description: string;
78
+ start_line: number;
79
+ end_line: number;
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;
110
+ updated_at: string;
111
+ source_of_truth: boolean;
112
+ trust_level: number;
113
+ status: string;
114
+ };
115
+
48
116
  export type RankingWeights = {
49
117
  semantic: number;
50
118
  graph: number;
@@ -56,6 +124,9 @@ export type ContextData = {
56
124
  documents: DocumentRecord[];
57
125
  adrs: AdrRecord[];
58
126
  rules: RuleRecord[];
127
+ chunks: ChunkRecord[];
128
+ modules: ModuleRecord[];
129
+ projects: ProjectRecord[];
59
130
  relations: RelationRecord[];
60
131
  ranking: RankingWeights;
61
132
  source: "cache" | "ryu";
@@ -64,7 +135,7 @@ export type ContextData = {
64
135
 
65
136
  export type SearchEntity = {
66
137
  id: string;
67
- entity_type: "File" | "Rule" | "ADR";
138
+ entity_type: "File" | "Rule" | "ADR" | "Chunk" | "Module" | "Project";
68
139
  kind: string;
69
140
  label: string;
70
141
  path: string;
@@ -88,13 +159,38 @@ export type SearchParams = {
88
159
  query: string;
89
160
  top_k: number;
90
161
  include_deprecated: boolean;
91
- include_content: boolean;
162
+ response_preset?: "full" | "compact" | "minimal";
163
+ include_scores?: boolean;
164
+ include_matched_rules?: boolean;
165
+ include_content?: boolean;
92
166
  };
93
167
 
94
168
  export type RelatedParams = {
95
169
  entity_id: string;
96
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;
97
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")[];
98
194
  };
99
195
 
100
196
  export type RulesParams = {