smart-context-mcp 1.9.0 → 1.10.0
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/README.md +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
- package/src/tools/smart-search.js +85 -21
package/README.md
CHANGED
|
@@ -56,7 +56,7 @@ Restart your AI client. Done.
|
|
|
56
56
|
# Check installed version
|
|
57
57
|
npm list -g smart-context-mcp
|
|
58
58
|
|
|
59
|
-
# Should show: smart-context-mcp@1.
|
|
59
|
+
# Should show: smart-context-mcp@1.10.0 (or later)
|
|
60
60
|
|
|
61
61
|
# Update to latest version
|
|
62
62
|
npm update -g smart-context-mcp
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "smart-context-mcp",
|
|
3
3
|
"mcpName": "io.github.Arrayo/smart-context-mcp",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.10.0",
|
|
5
5
|
"description": "MCP server that reduces agent token usage by 90% with intelligent context compression, task checkpoint persistence, and workflow-aware agent guidance.",
|
|
6
6
|
"author": "Francisco Caballero Portero <fcp1978@hotmail.com>",
|
|
7
7
|
"type": "module",
|
package/server.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"url": "https://github.com/Arrayo/smart-context-mcp",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "1.
|
|
9
|
+
"version": "1.10.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "smart-context-mcp",
|
|
14
|
-
"version": "1.
|
|
14
|
+
"version": "1.10.0",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|
|
@@ -99,39 +99,35 @@ const parseRgLine = (line, root) => {
|
|
|
99
99
|
|
|
100
100
|
const MAX_FILE_SIZE = '1M';
|
|
101
101
|
|
|
102
|
-
const
|
|
102
|
+
const buildRgBaseArgs = () => {
|
|
103
103
|
const args = [
|
|
104
104
|
'--line-number',
|
|
105
105
|
'--no-heading',
|
|
106
|
-
'--color',
|
|
107
|
-
'never',
|
|
106
|
+
'--color', 'never',
|
|
108
107
|
'--smart-case',
|
|
109
|
-
'--fixed-strings',
|
|
110
108
|
'--max-filesize', MAX_FILE_SIZE,
|
|
111
109
|
];
|
|
112
|
-
|
|
113
110
|
for (const dir of ignoredDirs) {
|
|
114
111
|
args.push('--glob', `!${dir}/**`);
|
|
115
112
|
args.push('--glob', `!**/${dir}/**`);
|
|
116
113
|
}
|
|
117
|
-
|
|
118
114
|
for (const fileName of ignoredFileNames) {
|
|
119
115
|
args.push('--glob', `!${fileName}`);
|
|
120
116
|
}
|
|
121
|
-
|
|
122
117
|
for (const extension of supportedGlobs) {
|
|
123
118
|
args.push('--glob', extension);
|
|
124
119
|
}
|
|
120
|
+
return args;
|
|
121
|
+
};
|
|
125
122
|
|
|
126
|
-
|
|
127
|
-
|
|
123
|
+
const runRg = async (root, pattern, extraArgs = []) => {
|
|
124
|
+
const args = [...buildRgBaseArgs(), ...extraArgs, pattern, '.'];
|
|
128
125
|
try {
|
|
129
126
|
const { stdout } = await execFile(rgPath, args, {
|
|
130
127
|
cwd: root,
|
|
131
128
|
maxBuffer: 1024 * 1024 * 10,
|
|
132
129
|
timeout: 15000,
|
|
133
130
|
});
|
|
134
|
-
|
|
135
131
|
return stdout
|
|
136
132
|
.split('\n')
|
|
137
133
|
.filter(Boolean)
|
|
@@ -140,11 +136,47 @@ const searchWithRipgrep = async (root, query) => {
|
|
|
140
136
|
.filter((match) => !shouldIgnoreFile(match.file));
|
|
141
137
|
} catch (error) {
|
|
142
138
|
if (error.code === 1) return [];
|
|
143
|
-
|
|
139
|
+
process.stderr.write(`[smart-search] ripgrep failed: ${error.message}\n`);
|
|
144
140
|
return null;
|
|
145
141
|
}
|
|
146
142
|
};
|
|
147
143
|
|
|
144
|
+
const extractTerms = (query) =>
|
|
145
|
+
query
|
|
146
|
+
.split(/[\s,;|/\\]+/)
|
|
147
|
+
.map((t) => t.trim())
|
|
148
|
+
.filter((t) => t.length >= 3);
|
|
149
|
+
|
|
150
|
+
const searchWithRipgrep = async (root, query) => {
|
|
151
|
+
// Pass 1: exact literal match
|
|
152
|
+
const exact = await runRg(root, query, ['--fixed-strings']);
|
|
153
|
+
if (exact === null) return null;
|
|
154
|
+
if (exact.length > 0) return { matches: exact, searchMode: 'exact' };
|
|
155
|
+
|
|
156
|
+
// Pass 2: regex (handles partial words, snake_case, camelCase fragments)
|
|
157
|
+
const escaped = query.replace(/[$()*+.?[\\\]^{|}]/g, '\\$&');
|
|
158
|
+
const regex = await runRg(root, escaped);
|
|
159
|
+
if (regex === null) return null;
|
|
160
|
+
if (regex.length > 0) return { matches: regex, searchMode: 'regex' };
|
|
161
|
+
|
|
162
|
+
// Pass 3: term expansion — search each significant word independently and merge
|
|
163
|
+
const terms = extractTerms(query);
|
|
164
|
+
if (terms.length < 2) return { matches: [], searchMode: 'exact', zeroReason: 'no_matches' };
|
|
165
|
+
|
|
166
|
+
const seen = new Set();
|
|
167
|
+
const merged = [];
|
|
168
|
+
for (const term of terms) {
|
|
169
|
+
const hits = await runRg(root, term, ['--fixed-strings']);
|
|
170
|
+
if (!hits) continue;
|
|
171
|
+
for (const hit of hits) {
|
|
172
|
+
const key = `${hit.file}:${hit.lineNumber}`;
|
|
173
|
+
if (!seen.has(key)) { seen.add(key); merged.push({ ...hit, matchedTerm: term }); }
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return { matches: merged, searchMode: 'terms', terms };
|
|
178
|
+
};
|
|
179
|
+
|
|
148
180
|
const MAX_FALLBACK_FILE_BYTES = 1024 * 1024;
|
|
149
181
|
|
|
150
182
|
export const isSmartCaseSensitive = (query) => query !== query.toLowerCase();
|
|
@@ -273,16 +305,43 @@ const groupMatches = (matches, query, intent, indexHits, graphHits) => {
|
|
|
273
305
|
return { groups: sorted, breakdown };
|
|
274
306
|
};
|
|
275
307
|
|
|
276
|
-
const
|
|
308
|
+
const buildZeroResultsMessage = (query, searchMode, provenance) => {
|
|
309
|
+
const lines = [`No matches found for: "${query}"`];
|
|
310
|
+
|
|
311
|
+
if (searchMode === 'exact') {
|
|
312
|
+
lines.push('• Tried: exact literal match (--fixed-strings)');
|
|
313
|
+
lines.push('• Tried: regex match');
|
|
314
|
+
} else if (searchMode === 'terms') {
|
|
315
|
+
const terms = provenance?.expandedTerms ?? [];
|
|
316
|
+
lines.push(`• Tried: exact, regex, and term expansion (${terms.join(', ')})`);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
lines.push('');
|
|
320
|
+
lines.push('Suggestions:');
|
|
321
|
+
lines.push(' – Use a shorter, more specific term (e.g. a function name, not a phrase)');
|
|
322
|
+
lines.push(' – Try Grep for raw text: the query may be in a file type not indexed by smart_search');
|
|
323
|
+
lines.push(' – Run build_index to enable symbol-level search if the codebase is new');
|
|
324
|
+
|
|
325
|
+
return lines.join('\n');
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
const buildCompactResult = (groups, totalMatches, query, root, searchMode, provenance) => {
|
|
329
|
+
if (totalMatches === 0) {
|
|
330
|
+
return buildZeroResultsMessage(query, searchMode, provenance);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const modeLabel = searchMode === 'exact' ? '' : searchMode === 'regex' ? ' [regex fallback]' : ` [term expansion: ${(provenance?.expandedTerms ?? []).join(', ')}]`;
|
|
334
|
+
|
|
277
335
|
if (totalMatches <= 20) {
|
|
278
|
-
|
|
336
|
+
const header = modeLabel ? `# Search mode:${modeLabel}\n\n` : '';
|
|
337
|
+
return header + groups
|
|
279
338
|
.flatMap((group) => group.matches)
|
|
280
339
|
.map(formatMatch)
|
|
281
340
|
.join('\n');
|
|
282
341
|
}
|
|
283
342
|
|
|
284
343
|
const lines = [
|
|
285
|
-
`query: ${query}`,
|
|
344
|
+
`query: ${query}${modeLabel}`,
|
|
286
345
|
`root: ${root}`,
|
|
287
346
|
`total matches: ${totalMatches}`,
|
|
288
347
|
`matched files: ${groups.length}`,
|
|
@@ -314,12 +373,13 @@ export const smartSearch = async ({ query, cwd = '.', intent, _testForceWalk = f
|
|
|
314
373
|
}
|
|
315
374
|
|
|
316
375
|
const root = resolveSafePath(cwd);
|
|
317
|
-
const
|
|
318
|
-
const usedFallback =
|
|
376
|
+
const rgResult = _testForceWalk ? null : await searchWithRipgrep(root, query);
|
|
377
|
+
const usedFallback = rgResult === null;
|
|
319
378
|
const engine = usedFallback ? 'walk' : 'rg';
|
|
320
379
|
|
|
321
380
|
let rawMatches;
|
|
322
381
|
let provenance;
|
|
382
|
+
let searchMode = 'exact';
|
|
323
383
|
|
|
324
384
|
if (usedFallback) {
|
|
325
385
|
const fallback = searchWithFallback(root, query);
|
|
@@ -340,7 +400,9 @@ export const smartSearch = async ({ query, cwd = '.', intent, _testForceWalk = f
|
|
|
340
400
|
warnings,
|
|
341
401
|
};
|
|
342
402
|
} else {
|
|
343
|
-
rawMatches =
|
|
403
|
+
rawMatches = rgResult.matches;
|
|
404
|
+
searchMode = rgResult.searchMode;
|
|
405
|
+
if (rgResult.terms) provenance = { expandedTerms: rgResult.terms };
|
|
344
406
|
}
|
|
345
407
|
|
|
346
408
|
rawMatches = rawMatches.filter((match) => !shouldIgnoreFile(match.file));
|
|
@@ -403,7 +465,7 @@ export const smartSearch = async ({ query, cwd = '.', intent, _testForceWalk = f
|
|
|
403
465
|
}
|
|
404
466
|
|
|
405
467
|
const rawText = dedupedMatches.map(formatMatch).join('\n');
|
|
406
|
-
const compressedText = truncate(buildCompactResult(groups, dedupedMatches.length, query, root), 5000);
|
|
468
|
+
const compressedText = truncate(buildCompactResult(groups, dedupedMatches.length, query, root, searchMode, provenance), 5000);
|
|
407
469
|
const metrics = buildMetrics({
|
|
408
470
|
tool: 'smart_search',
|
|
409
471
|
target: `${root} :: ${query}`,
|
|
@@ -442,9 +504,11 @@ export const smartSearch = async ({ query, cwd = '.', intent, _testForceWalk = f
|
|
|
442
504
|
});
|
|
443
505
|
|
|
444
506
|
let retrievalConfidence = 'high';
|
|
445
|
-
if (
|
|
446
|
-
|
|
447
|
-
|
|
507
|
+
if (dedupedMatches.length === 0) retrievalConfidence = 'none';
|
|
508
|
+
else if (searchMode === 'terms') retrievalConfidence = 'low';
|
|
509
|
+
else if (searchMode === 'regex') retrievalConfidence = 'medium';
|
|
510
|
+
else if (usedFallback) retrievalConfidence = provenance?.skippedItemsTotal > 0 ? 'low' : 'medium';
|
|
511
|
+
else if (provenance?.skippedItemsTotal > 0) retrievalConfidence = 'low';
|
|
448
512
|
|
|
449
513
|
const confidence = { level: retrievalConfidence, indexFreshness };
|
|
450
514
|
|