smart-context-mcp 1.13.0 → 1.15.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 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.13.0 (or later)
59
+ # Should show: smart-context-mcp@1.15.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.13.0",
4
+ "version": "1.15.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",
@@ -60,6 +60,7 @@
60
60
  "init:clients": "node ./scripts/init-clients.js",
61
61
  "smoke:formats": "node ./scripts/format-smoke.js",
62
62
  "test": "node --test --test-concurrency=1 ./tests/*.test.js",
63
+ "test:fast": "node --test --test-concurrency=4 ./tests/*.test.js",
63
64
  "verify": "node ./scripts/verify-features-direct.js",
64
65
  "benchmark": "node ./scripts/run-benchmark.js",
65
66
  "benchmark:orchestration": "node ./evals/orchestration-benchmark.js",
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.13.0",
9
+ "version": "1.15.0",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "smart-context-mcp",
14
- "version": "1.13.0",
14
+ "version": "1.15.0",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },
package/src/server.js CHANGED
@@ -146,18 +146,19 @@ export const createDevctxServer = () => {
146
146
 
147
147
  server.tool(
148
148
  'smart_search',
149
- 'Search code across the project using ripgrep (with filesystem fallback). Returns grouped, ranked results. Optional intent (implementation/debug/tests/config/docs/explore) adjusts ranking. Use instead of native Grep for ranked, deduplicated results with index boosting.',
149
+ 'Search code with ranked, deduplicated results and index boosting. Best for: finding where a symbol is defined/used, understanding call chains, locating implementations. NOT ideal for: exact string matching (use Grep), finding files by name (use Glob), broad multi-word queries (generates noise). Optional intent adjusts ranking. maxFiles caps the number of files returned (default 15). When >30 files match, results include a hint suggesting Grep instead.',
150
150
  {
151
151
  query: z.string(),
152
152
  cwd: z.string().optional(),
153
153
  intent: z.enum(['implementation', 'debug', 'tests', 'config', 'docs', 'explore']).optional(),
154
+ maxFiles: z.number().int().min(1).max(50).optional(),
154
155
  },
155
- async ({ query, cwd = '.', intent }) => asTextResult(await smartSearch({ query, cwd, intent })),
156
+ async ({ query, cwd = '.', intent, maxFiles }) => asTextResult(await smartSearch({ query, cwd, intent, maxFiles })),
156
157
  );
157
158
 
158
159
  server.tool(
159
160
  'smart_context',
160
- 'PREFERRED for multi-file tasks. Gets curated context in one call — replaces the manual search → read → read cycle. Combines search + graph expansion + selective reading. Returns relevant files with symbols and content, optimized for tokens. Options: intent, maxTokens (budget), diff (true for HEAD or branch name), detail (minimal/balanced/deep), include (content/graph/hints/symbolDetail), prefetch (true for predictive loading). Call this FIRST before individual smart_read/smart_search calls.',
161
+ 'PREFERRED for multi-file tasks. Gets curated context in one call — replaces the manual search → read → read cycle. Combines search + graph expansion + selective reading. Primary files always include content (signatures) in balanced mode — reduces follow-up smart_read calls. Options: intent, maxTokens (budget, default 12000), diff (true for HEAD or branch name), detail (minimal/balanced/deep), include (content/graph/hints/symbolDetail), prefetch (true for predictive loading). Call this FIRST before individual smart_read/smart_search calls.',
161
162
  {
162
163
  task: z.string(),
163
164
  intent: z.enum(['implementation', 'debug', 'tests', 'config', 'docs', 'explore']).optional(),
@@ -196,9 +196,7 @@ export const allocateReads = (files, maxTokens, intent, detailMode = 'balanced')
196
196
 
197
197
  const mode = detailMode === 'deep'
198
198
  ? 'full'
199
- : best.role === 'primary' && !tightBudget
200
- ? 'outline'
201
- : 'signatures';
199
+ : 'signatures';
202
200
 
203
201
  roleLimits[best.role]--;
204
202
  selected.push(best);
@@ -269,8 +267,7 @@ const shouldReadContentForItem = (item, payload, detailMode, includeSet, intent)
269
267
  const strongIndexSignal = hasStrongIndexSignal(payload);
270
268
 
271
269
  if (item.role === 'primary') {
272
- if ((item.matchedSymbols?.length ?? 0) > 0) return false;
273
- return !strongIndexSignal;
270
+ return true;
274
271
  }
275
272
 
276
273
  if (item.role === 'test' && intent === 'tests') {
@@ -353,7 +350,7 @@ const DEFAULT_INCLUDE = ['content', 'graph', 'hints', 'symbolDetail'];
353
350
  export const smartContext = async ({
354
351
  task,
355
352
  intent,
356
- maxTokens = 8000,
353
+ maxTokens = 12000,
357
354
  entryFile,
358
355
  diff,
359
356
  detail = 'balanced',
@@ -518,7 +515,7 @@ export const smartContext = async ({
518
515
  primarySeeds.unshift({ rel, absPath: abs, evidence: [{ type: 'entryFile' }] });
519
516
  }
520
517
  }
521
- } catch { /* invalid path skip */ }
518
+ } catch (err) { process.stderr.write(`[devctx] smart_context: entryFile "${entryFile}" skipped: ${err.message}\n`); }
522
519
  }
523
520
 
524
521
  await ensureIndexReady({ root });
@@ -549,7 +546,7 @@ export const smartContext = async ({
549
546
  });
550
547
  }
551
548
  }
552
- } catch {}
549
+ } catch (err) { process.stderr.write(`[devctx] smart_context: prefetch path "${predicted.path}" skipped: ${err.message}\n`); }
553
550
  }
554
551
  }
555
552
  } catch (error) {
@@ -759,7 +756,7 @@ export const smartContext = async ({
759
756
  order: idx
760
757
  }))
761
758
  });
762
- } catch {}
759
+ } catch (err) { process.stderr.write(`[devctx] smart_context: recordContextAccess failed: ${err.message}\n`); }
763
760
  }
764
761
 
765
762
  const COVERAGE_RANK = { full: 2, partial: 1, none: 0 };
@@ -324,16 +324,20 @@ const buildZeroResultsMessage = (query, searchMode, provenance) => {
324
324
  return lines.join('\n');
325
325
  };
326
326
 
327
- const buildCompactResult = (groups, totalMatches, query, root, searchMode, provenance) => {
327
+ const MAX_RESULT_FILES = 15;
328
+
329
+ const buildCompactResult = (groups, totalMatches, query, root, searchMode, provenance, totalFiles) => {
328
330
  if (totalMatches === 0) {
329
331
  return buildZeroResultsMessage(query, searchMode, provenance);
330
332
  }
331
333
 
332
334
  const modeLabel = searchMode === 'exact' ? '' : searchMode === 'regex' ? ' [regex fallback]' : ` [term expansion: ${(provenance?.expandedTerms ?? []).join(', ')}]`;
333
335
 
336
+ const topGroups = groups.slice(0, MAX_RESULT_FILES);
337
+
334
338
  if (totalMatches <= 20) {
335
339
  const header = modeLabel ? `# Search mode:${modeLabel}\n\n` : '';
336
- return header + groups
340
+ return header + topGroups
337
341
  .flatMap((group) => group.matches)
338
342
  .map(formatMatch)
339
343
  .join('\n');
@@ -341,29 +345,34 @@ const buildCompactResult = (groups, totalMatches, query, root, searchMode, prove
341
345
 
342
346
  const lines = [
343
347
  `query: ${query}${modeLabel}`,
344
- `root: ${root}`,
345
- `total matches: ${totalMatches}`,
346
- `matched files: ${groups.length}`,
348
+ `total: ${totalMatches} matches in ${totalFiles ?? groups.length} files${totalFiles && totalFiles > groups.length ? ` (showing top ${groups.length})` : ''}`,
347
349
  '',
348
350
  '# Top files',
349
351
  ];
350
352
 
351
- for (const group of groups.slice(0, 10)) {
353
+ for (const group of topGroups.slice(0, 10)) {
352
354
  lines.push(`${group.count} match(es), score ${group.score} :: ${group.file}`);
353
355
  }
354
356
 
355
357
  lines.push('', '# Sample matches');
356
358
 
357
- for (const group of groups.slice(0, 5)) {
358
- for (const match of group.matches.slice(0, 3)) {
359
+ const topScore = topGroups[0]?.score ?? 0;
360
+ for (const group of topGroups.slice(0, 5)) {
361
+ const linesPerFile = group.score >= topScore * 0.7 ? 5 : 2;
362
+ for (const match of group.matches.slice(0, linesPerFile)) {
359
363
  lines.push(formatMatch(match));
360
364
  }
361
365
  }
362
366
 
367
+ const fileCount = totalFiles ?? groups.length;
368
+ if (fileCount > 30) {
369
+ lines.push('', `# Note: ${fileCount} files matched — query may be too broad. Use Grep for exact pattern matching.`);
370
+ }
371
+
363
372
  return lines.join('\n');
364
373
  };
365
374
 
366
- export const smartSearch = async ({ query, cwd = '.', intent, _testForceWalk = false, progress: enableProgress = false }) => {
375
+ export const smartSearch = async ({ query, cwd = '.', intent, maxFiles, _testForceWalk = false, progress: enableProgress = false }) => {
367
376
  const progress = enableProgress ? createProgressReporter('smart_search') : null;
368
377
  const startTime = Date.now();
369
378
 
@@ -463,8 +472,11 @@ export const smartSearch = async ({ query, cwd = '.', intent, _testForceWalk = f
463
472
  }
464
473
  }
465
474
 
475
+ const effectiveMaxFiles = maxFiles ?? MAX_RESULT_FILES;
476
+ const cappedGroups = groups.slice(0, effectiveMaxFiles);
477
+
466
478
  const rawText = dedupedMatches.map(formatMatch).join('\n');
467
- const compressedText = truncate(buildCompactResult(groups, dedupedMatches.length, query, root, searchMode, provenance), 5000);
479
+ const compressedText = truncate(buildCompactResult(cappedGroups, dedupedMatches.length, query, root, searchMode, provenance, groups.length), 5000);
468
480
  const metrics = buildMetrics({
469
481
  tool: 'smart_search',
470
482
  target: `${root} :: ${query}`,
@@ -522,7 +534,7 @@ export const smartSearch = async ({ query, cwd = '.', intent, _testForceWalk = f
522
534
  ...(indexHits ? { indexBoosted: indexHits.size } : {}),
523
535
  totalMatches: dedupedMatches.length,
524
536
  matchedFiles: groups.length,
525
- topFiles: groups.slice(0, 10).map((group) => ({ file: group.file, count: group.count, score: group.score })),
537
+ topFiles: cappedGroups.slice(0, 10).map((group) => ({ file: group.file, count: group.count, score: group.score })),
526
538
  matches: compressedText,
527
539
  };
528
540
 
@@ -277,6 +277,7 @@ export const scorePrimarySeed = (seed, task, intent) => {
277
277
  let score = 0;
278
278
 
279
279
  for (const evidence of seed.evidence ?? []) {
280
+ if (evidence.type === 'entryFile') { score += 100; continue; }
280
281
  if (evidence.type !== 'searchHit') continue;
281
282
  score += Math.max(0, 40 - ((evidence.rank ?? 1) - 1) * 8);
282
283
  if (!evidence.query) continue;