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/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
- "Search indexed code, symbols, decisions, patterns, and failures. Supports multiple search scopes simultaneously with scope-specific filters and output formats.",
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 (default: 'full')",
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
- const output = (p.output as string) || "full";
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
  /**