smart-context-mcp 1.14.0 → 1.16.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.14.0 (or later)
59
+ # Should show: smart-context-mcp@1.16.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.14.0",
4
+ "version": "1.16.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",
@@ -69,6 +69,8 @@
69
69
  "eval:context": "node ./evals/harness.js --tool=context",
70
70
  "eval:both": "node ./evals/harness.js --tool=both",
71
71
  "eval:self": "node ./evals/harness.js --root=../.. --corpus=./evals/corpus/self-tasks.json",
72
+ "eval:realworld": "node ./evals/realworld-eval.js",
73
+ "eval:realworld:json": "node ./evals/realworld-eval.js --json",
72
74
  "eval:report": "node ./evals/report.js",
73
75
  "report:metrics": "node ./scripts/report-metrics.js",
74
76
  "report:workflows": "node ./scripts/report-workflow-metrics.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.14.0",
9
+ "version": "1.16.0",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "smart-context-mcp",
14
- "version": "1.14.0",
14
+ "version": "1.16.0",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },
package/src/server.js CHANGED
@@ -158,7 +158,7 @@ export const createDevctxServer = () => {
158
158
 
159
159
  server.tool(
160
160
  'smart_context',
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. 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.',
162
162
  {
163
163
  task: z.string(),
164
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') {
@@ -278,6 +275,7 @@ const shouldReadContentForItem = (item, payload, detailMode, includeSet, intent)
278
275
  }
279
276
 
280
277
  if (item.role === 'dependency') {
278
+ if ((item.matchedSymbols?.length ?? 0) > 0) return true;
281
279
  return !strongIndexSignal && (payload.symbols?.length ?? 0) === 0;
282
280
  }
283
281
 
@@ -353,7 +351,7 @@ const DEFAULT_INCLUDE = ['content', 'graph', 'hints', 'symbolDetail'];
353
351
  export const smartContext = async ({
354
352
  task,
355
353
  intent,
356
- maxTokens = 8000,
354
+ maxTokens = 12000,
357
355
  entryFile,
358
356
  diff,
359
357
  detail = 'balanced',
@@ -518,7 +516,7 @@ export const smartContext = async ({
518
516
  primarySeeds.unshift({ rel, absPath: abs, evidence: [{ type: 'entryFile' }] });
519
517
  }
520
518
  }
521
- } catch { /* invalid path skip */ }
519
+ } catch (err) { process.stderr.write(`[devctx] smart_context: entryFile "${entryFile}" skipped: ${err.message}\n`); }
522
520
  }
523
521
 
524
522
  await ensureIndexReady({ root });
@@ -549,7 +547,7 @@ export const smartContext = async ({
549
547
  });
550
548
  }
551
549
  }
552
- } catch {}
550
+ } catch (err) { process.stderr.write(`[devctx] smart_context: prefetch path "${predicted.path}" skipped: ${err.message}\n`); }
553
551
  }
554
552
  }
555
553
  } catch (error) {
@@ -759,7 +757,7 @@ export const smartContext = async ({
759
757
  order: idx
760
758
  }))
761
759
  });
762
- } catch {}
760
+ } catch (err) { process.stderr.write(`[devctx] smart_context: recordContextAccess failed: ${err.message}\n`); }
763
761
  }
764
762
 
765
763
  const COVERAGE_RANK = { full: 2, partial: 1, none: 0 };
@@ -114,6 +114,18 @@ const getFunctionSignature = (statement, sourceFile) => {
114
114
  return sig.length > 120 ? `${sig.slice(0, 120)}...` : sig;
115
115
  };
116
116
 
117
+ const getVariableFunctionSignature = (declaration, sourceFile) => {
118
+ const init = declaration.initializer;
119
+ if (!init) return null;
120
+ if (!ts.isArrowFunction(init) && !ts.isFunctionExpression(init)) return null;
121
+ const body = init.body;
122
+ if (!body) return null;
123
+ const fullText = declaration.getText(sourceFile);
124
+ const bodyOffset = body.getStart(sourceFile) - declaration.getStart(sourceFile);
125
+ const sig = fullText.slice(0, bodyOffset).replace(/\s+$/, '');
126
+ return sig.length > 120 ? `${sig.slice(0, 120)}...` : sig;
127
+ };
128
+
117
129
  const formatTopLevelStatement = (statement, sourceFile, mode = 'outline') => {
118
130
  const exported = statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) ?? false;
119
131
  const prefix = exported ? 'export ' : '';
@@ -149,6 +161,12 @@ const formatTopLevelStatement = (statement, sourceFile, mode = 'outline') => {
149
161
  : statement.declarationList.flags & ts.NodeFlags.Let
150
162
  ? 'let'
151
163
  : 'var';
164
+
165
+ if (mode === 'signatures' && statement.declarationList.declarations.length === 1) {
166
+ const sig = getVariableFunctionSignature(statement.declarationList.declarations[0], sourceFile);
167
+ if (sig) return `${prefix}${declarationKind} ${sig}`;
168
+ }
169
+
152
170
  return `${prefix}${declarationKind} ${collectVariableNames(statement.declarationList).join(', ')}`;
153
171
  }
154
172
 
@@ -533,7 +533,8 @@ export const smartSearch = async ({ query, cwd = '.', intent, maxFiles, _testFor
533
533
  ...(validIntent ? { intent: validIntent } : {}),
534
534
  ...(indexHits ? { indexBoosted: indexHits.size } : {}),
535
535
  totalMatches: dedupedMatches.length,
536
- matchedFiles: groups.length,
536
+ matchedFiles: cappedGroups.length,
537
+ ...(groups.length > cappedGroups.length ? { totalFiles: groups.length } : {}),
537
538
  topFiles: cappedGroups.slice(0, 10).map((group) => ({ file: group.file, count: group.count, score: group.score })),
538
539
  matches: compressedText,
539
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;