kotadb 2.2.0-next.20260204230500 → 2.2.0-next.20260205005118
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/package.json +1 -1
- package/src/api/queries.ts +989 -812
- package/src/mcp/server.ts +2 -0
- package/src/mcp/tools.ts +230 -9
package/src/mcp/server.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { Sentry } from "../instrument.js";
|
|
|
16
16
|
import {
|
|
17
17
|
ANALYZE_CHANGE_IMPACT_TOOL,
|
|
18
18
|
GENERATE_TASK_CONTEXT_TOOL,
|
|
19
|
+
GET_INDEX_STATISTICS_TOOL,
|
|
19
20
|
INDEX_REPOSITORY_TOOL,
|
|
20
21
|
LIST_RECENT_FILES_TOOL,
|
|
21
22
|
SEARCH_TOOL,
|
|
@@ -35,6 +36,7 @@ import {
|
|
|
35
36
|
// Execute functions
|
|
36
37
|
executeAnalyzeChangeImpact,
|
|
37
38
|
executeGenerateTaskContext,
|
|
39
|
+
executeGetIndexStatistics,
|
|
38
40
|
executeIndexRepository,
|
|
39
41
|
executeListRecentFiles,
|
|
40
42
|
executeSearch,
|
package/src/mcp/tools.ts
CHANGED
|
@@ -6,12 +6,14 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
|
+
getIndexStatistics,
|
|
9
10
|
listRecentFiles,
|
|
10
11
|
queryDependencies,
|
|
11
12
|
queryDependents,
|
|
12
13
|
resolveFilePath,
|
|
13
14
|
runIndexingWorkflow,
|
|
14
15
|
searchFiles,
|
|
16
|
+
extractLineSnippets,
|
|
15
17
|
} from "@api/queries";
|
|
16
18
|
import { getDomainKeyFiles } from "@api/expertise-queries.js";
|
|
17
19
|
import { getGlobalDatabase } from "@db/sqlite/index.js";
|
|
@@ -111,8 +113,20 @@ export function isValidToolset(value: string): value is ToolsetTier {
|
|
|
111
113
|
export const SEARCH_TOOL: ToolDefinition = {
|
|
112
114
|
tier: "core",
|
|
113
115
|
name: "search",
|
|
114
|
-
description:
|
|
115
|
-
|
|
116
|
+
description: `Search indexed code, symbols, decisions, patterns, and failures.
|
|
117
|
+
|
|
118
|
+
OUTPUT MODES:
|
|
119
|
+
- 'paths': File paths only (~100 bytes/result)
|
|
120
|
+
- 'compact': Summary info (~200 bytes/result) - DEFAULT for code scope
|
|
121
|
+
- 'snippet': Matching lines with context (~2KB/result)
|
|
122
|
+
- 'full': Complete content (~100KB/result) - Use with caution for code scope
|
|
123
|
+
|
|
124
|
+
TIPS:
|
|
125
|
+
- Use 'snippet' for code exploration (shows matches in context)
|
|
126
|
+
- Use 'compact' for quick file discovery
|
|
127
|
+
- Use 'full' only for small result sets (symbols, decisions, etc.)
|
|
128
|
+
|
|
129
|
+
Supports multiple search scopes simultaneously with scope-specific filters.`,
|
|
116
130
|
inputSchema: {
|
|
117
131
|
type: "object",
|
|
118
132
|
properties: {
|
|
@@ -196,8 +210,14 @@ export const SEARCH_TOOL: ToolDefinition = {
|
|
|
196
210
|
},
|
|
197
211
|
output: {
|
|
198
212
|
type: "string",
|
|
199
|
-
enum: ["full", "paths", "compact"],
|
|
200
|
-
description: "Output format (
|
|
213
|
+
enum: ["full", "paths", "compact", "snippet"],
|
|
214
|
+
description: "Output format: 'paths' (file paths only), 'compact' (summary), 'snippet' (matches with context), 'full' (complete content). Default varies by scope: code='compact', others='full'. WARNING: 'full' + code scope = large results.",
|
|
215
|
+
},
|
|
216
|
+
context_lines: {
|
|
217
|
+
type: "number",
|
|
218
|
+
description: "Lines of context before/after matches (snippet mode only, default: 3, max: 10)",
|
|
219
|
+
minimum: 0,
|
|
220
|
+
maximum: 10,
|
|
201
221
|
},
|
|
202
222
|
},
|
|
203
223
|
required: ["query"],
|
|
@@ -353,6 +373,21 @@ export const ANALYZE_CHANGE_IMPACT_TOOL: ToolDefinition = {
|
|
|
353
373
|
},
|
|
354
374
|
};
|
|
355
375
|
|
|
376
|
+
/**
|
|
377
|
+
* Tool: get_index_statistics
|
|
378
|
+
*/
|
|
379
|
+
export const GET_INDEX_STATISTICS_TOOL: ToolDefinition = {
|
|
380
|
+
tier: "core",
|
|
381
|
+
name: "get_index_statistics",
|
|
382
|
+
description:
|
|
383
|
+
"Get statistics about indexed data (files, symbols, references, decisions, patterns, failures). Useful for understanding what data is available for search.",
|
|
384
|
+
inputSchema: {
|
|
385
|
+
type: "object",
|
|
386
|
+
properties: {},
|
|
387
|
+
required: [],
|
|
388
|
+
},
|
|
389
|
+
};
|
|
390
|
+
|
|
356
391
|
/**
|
|
357
392
|
* Tool: validate_implementation_spec
|
|
358
393
|
*/
|
|
@@ -770,6 +805,7 @@ export function getToolDefinitions(): ToolDefinition[] {
|
|
|
770
805
|
LIST_RECENT_FILES_TOOL,
|
|
771
806
|
SEARCH_DEPENDENCIES_TOOL,
|
|
772
807
|
ANALYZE_CHANGE_IMPACT_TOOL,
|
|
808
|
+
GET_INDEX_STATISTICS_TOOL,
|
|
773
809
|
VALIDATE_IMPLEMENTATION_SPEC_TOOL,
|
|
774
810
|
SYNC_EXPORT_TOOL,
|
|
775
811
|
SYNC_IMPORT_TOOL,
|
|
@@ -958,11 +994,122 @@ async function searchSymbols(
|
|
|
958
994
|
}));
|
|
959
995
|
}
|
|
960
996
|
|
|
997
|
+
/**
|
|
998
|
+
* Generate contextual tips based on search query and parameters.
|
|
999
|
+
* Uses static pattern matching (no NLP) to detect suboptimal usage patterns.
|
|
1000
|
+
*
|
|
1001
|
+
* Tip frequency: MODERATE - show tips frequently including "nice to know" suggestions.
|
|
1002
|
+
*
|
|
1003
|
+
* @param query - Search query string
|
|
1004
|
+
* @param scopes - Search scopes used
|
|
1005
|
+
* @param filters - Normalized filters applied
|
|
1006
|
+
* @param scopeResults - Results by scope
|
|
1007
|
+
* @returns Array of tip strings (empty if search is optimal)
|
|
1008
|
+
*/
|
|
1009
|
+
function generateSearchTips(
|
|
1010
|
+
query: string,
|
|
1011
|
+
scopes: string[],
|
|
1012
|
+
filters: NormalizedFilters,
|
|
1013
|
+
scopeResults: Record<string, unknown[]>
|
|
1014
|
+
): string[] {
|
|
1015
|
+
const tips: string[] = [];
|
|
1016
|
+
const queryLower = query.toLowerCase();
|
|
1017
|
+
|
|
1018
|
+
// Pattern 1: Query contains structural keywords but not using symbols scope
|
|
1019
|
+
const structuralKeywords = ['function', 'class', 'interface', 'type', 'method', 'component'];
|
|
1020
|
+
const hasStructuralKeyword = structuralKeywords.some(kw => queryLower.includes(kw));
|
|
1021
|
+
|
|
1022
|
+
if (hasStructuralKeyword && !scopes.includes('symbols')) {
|
|
1023
|
+
const matchedKeyword = structuralKeywords.find(kw => queryLower.includes(kw)) || 'function';
|
|
1024
|
+
tips.push(
|
|
1025
|
+
`You searched for "${query}" in code. Try scope: ['symbols'] with filters: {symbol_kind: ['${matchedKeyword}']} for precise structural discovery.`
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
// Pattern 2: Query looks like a file path but using code search
|
|
1030
|
+
const looksLikeFilePath = /^[\w\-./]+\.(ts|tsx|js|jsx|py|rs|go|java)$/i.test(query);
|
|
1031
|
+
if (looksLikeFilePath && scopes.includes('code')) {
|
|
1032
|
+
tips.push(
|
|
1033
|
+
`Query "${query}" looks like a file path. Consider using search_dependencies tool to find files that depend on this file or its dependencies.`
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// Pattern 3: Symbol search without exported_only filter
|
|
1038
|
+
if (scopes.includes('symbols') && filters.exported_only === undefined) {
|
|
1039
|
+
const symbolCount = scopeResults['symbols']?.length || 0;
|
|
1040
|
+
if (symbolCount > 10) {
|
|
1041
|
+
tips.push(
|
|
1042
|
+
`Found ${symbolCount} symbols. Add filters: {exported_only: true} to narrow to public API only.`
|
|
1043
|
+
);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// Pattern 4: No repository filter with large result set
|
|
1048
|
+
if (!filters.repositoryId) {
|
|
1049
|
+
const totalResults = Object.values(scopeResults).reduce((sum, arr) => sum + arr.length, 0);
|
|
1050
|
+
if (totalResults > 20) {
|
|
1051
|
+
tips.push(
|
|
1052
|
+
`Found ${totalResults} results across all repositories. Add filters: {repository: "owner/repo"} to narrow to a specific repository.`
|
|
1053
|
+
);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// Pattern 5: Code search without glob/language filters
|
|
1058
|
+
if (scopes.includes('code') && !filters.glob && !filters.language) {
|
|
1059
|
+
const codeCount = scopeResults['code']?.length || 0;
|
|
1060
|
+
if (codeCount > 15) {
|
|
1061
|
+
tips.push(
|
|
1062
|
+
`Found ${codeCount} code results. Try filters: {glob: "**/*.ts"} or {language: "typescript"} to narrow file types.`
|
|
1063
|
+
);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// Pattern 6: Suggest decisions scope for "why" questions
|
|
1068
|
+
if (/\b(why|reason|decision|chose|choice)\b/i.test(query) && !scopes.includes('decisions')) {
|
|
1069
|
+
tips.push(
|
|
1070
|
+
`Query contains "why/reason/decision". Try scope: ['decisions'] to search architectural decisions and rationale.`
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// Pattern 7: Suggest patterns scope for "how" questions
|
|
1075
|
+
if (/\b(how|pattern|best practice|convention)\b/i.test(query) && !scopes.includes('patterns')) {
|
|
1076
|
+
tips.push(
|
|
1077
|
+
`Query asks "how to". Try scope: ['patterns'] to search coding patterns and conventions from this codebase.`
|
|
1078
|
+
);
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// Pattern 8: Suggest failures scope for error-related queries
|
|
1082
|
+
if (/\b(error|bug|fail|issue|problem|fix)\b/i.test(query) && !scopes.includes('failures')) {
|
|
1083
|
+
tips.push(
|
|
1084
|
+
`Query mentions errors/issues. Try scope: ['failures'] to learn from past mistakes and avoid repeated failures.`
|
|
1085
|
+
);
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// Pattern 9: Single scope when multi-scope could be useful
|
|
1089
|
+
if (scopes.length === 1 && scopes[0] === 'code') {
|
|
1090
|
+
tips.push(
|
|
1091
|
+
`Tip: You can search multiple scopes simultaneously. Try scope: ['code', 'symbols'] for broader discovery.`
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// Pattern 10: Suggest compact format for large result sets
|
|
1096
|
+
const totalResults = Object.values(scopeResults).reduce((sum, arr) => sum + arr.length, 0);
|
|
1097
|
+
if (totalResults > 30 && !tips.some(t => t.includes('output: "compact"'))) {
|
|
1098
|
+
tips.push(
|
|
1099
|
+
`Returning ${totalResults} full results. Use output: "compact" for summary view or output: "paths" for file paths only.`
|
|
1100
|
+
);
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
return tips;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
961
1106
|
function formatSearchResults(
|
|
962
1107
|
query: string,
|
|
963
1108
|
scopes: string[],
|
|
964
1109
|
scopeResults: Record<string, unknown[]>,
|
|
965
|
-
format: string
|
|
1110
|
+
format: string,
|
|
1111
|
+
filters: NormalizedFilters,
|
|
1112
|
+
contextLines?: number
|
|
966
1113
|
): Record<string, unknown> {
|
|
967
1114
|
const response: Record<string, unknown> = {
|
|
968
1115
|
query,
|
|
@@ -998,6 +1145,36 @@ function formatSearchResults(
|
|
|
998
1145
|
}
|
|
999
1146
|
return item;
|
|
1000
1147
|
});
|
|
1148
|
+
} else if (format === "snippet") {
|
|
1149
|
+
// Snippet extraction with context
|
|
1150
|
+
if (scope === "code") {
|
|
1151
|
+
(response.results as Record<string, unknown>)[scope] = items.map((item: any) => {
|
|
1152
|
+
const matches = extractLineSnippets(
|
|
1153
|
+
item.content || "",
|
|
1154
|
+
query,
|
|
1155
|
+
contextLines || 3
|
|
1156
|
+
);
|
|
1157
|
+
return {
|
|
1158
|
+
path: item.path,
|
|
1159
|
+
matches: matches
|
|
1160
|
+
};
|
|
1161
|
+
});
|
|
1162
|
+
} else {
|
|
1163
|
+
// For non-code scopes, fall back to compact format
|
|
1164
|
+
// (snippets only meaningful for code files)
|
|
1165
|
+
(response.results as Record<string, unknown>)[scope] = items.map((item: any) => {
|
|
1166
|
+
if (scope === "symbols") {
|
|
1167
|
+
return { name: item.name, kind: item.kind, file: item.location.file };
|
|
1168
|
+
} else if (scope === "decisions") {
|
|
1169
|
+
return { title: item.title, scope: item.scope };
|
|
1170
|
+
} else if (scope === "patterns") {
|
|
1171
|
+
return { pattern_type: item.pattern_type, file_path: item.file_path };
|
|
1172
|
+
} else if (scope === "failures") {
|
|
1173
|
+
return { title: item.title, problem: item.problem };
|
|
1174
|
+
}
|
|
1175
|
+
return item;
|
|
1176
|
+
});
|
|
1177
|
+
}
|
|
1001
1178
|
} else {
|
|
1002
1179
|
// Full details
|
|
1003
1180
|
(response.results as Record<string, unknown>)[scope] = items;
|
|
@@ -1007,6 +1184,13 @@ function formatSearchResults(
|
|
|
1007
1184
|
(response.counts as Record<string, unknown>).total = ((response.counts as Record<string, unknown>).total as number) + items.length;
|
|
1008
1185
|
}
|
|
1009
1186
|
|
|
1187
|
+
|
|
1188
|
+
// Generate and add tips if applicable
|
|
1189
|
+
const tips = generateSearchTips(query, scopes, filters, scopeResults);
|
|
1190
|
+
if (tips.length > 0) {
|
|
1191
|
+
response.tips = tips;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1010
1194
|
return response;
|
|
1011
1195
|
}
|
|
1012
1196
|
|
|
@@ -1057,13 +1241,30 @@ export async function executeSearch(
|
|
|
1057
1241
|
}
|
|
1058
1242
|
|
|
1059
1243
|
if (p.output !== undefined) {
|
|
1060
|
-
if (typeof p.output !== "string" || !["full", "paths", "compact"].includes(p.output)) {
|
|
1061
|
-
throw new Error("Parameter 'output' must be one of: full, paths, compact");
|
|
1244
|
+
if (typeof p.output !== "string" || !["full", "paths", "compact", "snippet"].includes(p.output)) {
|
|
1245
|
+
throw new Error("Parameter 'output' must be one of: full, paths, compact, snippet");
|
|
1062
1246
|
}
|
|
1063
1247
|
}
|
|
1064
1248
|
|
|
1249
|
+
if (p.context_lines !== undefined && typeof p.context_lines !== "number") {
|
|
1250
|
+
throw new Error("Parameter 'context_lines' must be a number");
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
if (p.context_lines !== undefined && (p.context_lines < 0 || p.context_lines > 10)) {
|
|
1254
|
+
throw new Error("Parameter 'context_lines' must be between 0 and 10");
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1065
1257
|
const limit = Math.min(Math.max((p.limit as number) || 20, 1), 100);
|
|
1066
|
-
|
|
1258
|
+
// Determine default output based on scopes
|
|
1259
|
+
let defaultOutput = "full";
|
|
1260
|
+
if (scopes.length === 1 && scopes[0] === "code") {
|
|
1261
|
+
defaultOutput = "compact"; // Code-only searches default to compact
|
|
1262
|
+
} else if (scopes.includes("code") && scopes.length > 1) {
|
|
1263
|
+
defaultOutput = "compact"; // Multi-scope including code defaults to compact
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
const output = (p.output as string) || defaultOutput;
|
|
1267
|
+
const contextLines = Math.min(Math.max((p.context_lines as number) || 3, 0), 10);
|
|
1067
1268
|
const filters = normalizeFilters(p.filters);
|
|
1068
1269
|
|
|
1069
1270
|
// Route to scope handlers in parallel
|
|
@@ -1142,7 +1343,7 @@ export async function executeSearch(
|
|
|
1142
1343
|
await Promise.all(searchPromises);
|
|
1143
1344
|
|
|
1144
1345
|
// Format output
|
|
1145
|
-
const response = formatSearchResults(p.query as string, scopes, results, output);
|
|
1346
|
+
const response = formatSearchResults(p.query as string, scopes, results, output, filters, contextLines);
|
|
1146
1347
|
|
|
1147
1348
|
logger.info("Unified search completed", {
|
|
1148
1349
|
query: p.query,
|
|
@@ -1634,6 +1835,26 @@ export async function executeAnalyzeChangeImpact(
|
|
|
1634
1835
|
return result;
|
|
1635
1836
|
}
|
|
1636
1837
|
|
|
1838
|
+
/**
|
|
1839
|
+
* Execute get_index_statistics tool
|
|
1840
|
+
*/
|
|
1841
|
+
export async function executeGetIndexStatistics(
|
|
1842
|
+
params: unknown,
|
|
1843
|
+
requestId: string | number,
|
|
1844
|
+
userId: string,
|
|
1845
|
+
): Promise<unknown> {
|
|
1846
|
+
// No parameters to validate
|
|
1847
|
+
|
|
1848
|
+
logger.info("Getting index statistics", { request_id: String(requestId), user_id: userId });
|
|
1849
|
+
|
|
1850
|
+
const stats = getIndexStatistics();
|
|
1851
|
+
|
|
1852
|
+
return {
|
|
1853
|
+
...stats,
|
|
1854
|
+
summary: `${stats.symbols.toLocaleString()} symbols, ${stats.files.toLocaleString()} files, ${stats.repositories} repositories indexed`,
|
|
1855
|
+
};
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1637
1858
|
/**
|
|
1638
1859
|
|
|
1639
1860
|
/**
|