smart-context-mcp 1.18.1 → 1.20.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.18.1 (or later)
59
+ # Should show: smart-context-mcp@1.20.0 (or later)
60
60
 
61
61
  # Update to latest version
62
62
  npm update -g smart-context-mcp
@@ -278,22 +278,30 @@ Check actual usage:
278
278
 
279
279
  Provides **two key components**:
280
280
 
281
- ### 1. Specialized Tools (12 tools)
281
+ ### 1. Specialized Tools (20 tools)
282
282
 
283
283
  | Tool | Purpose | Savings |
284
284
  |------|---------|---------|
285
- | `smart_read` | Read files in outline/signatures mode | 90% |
285
+ | `smart_read` | Read files in outline / signatures / symbol / explain mode | 90% |
286
286
  | `smart_read_batch` | Read multiple files in one call | 90% |
287
- | `smart_search` | Intent-aware code search with ranking | 95% |
288
- | `smart_context` | One-call context builder | 85% |
289
- | `smart_summary` | Task checkpoint management | 98% |
290
- | `smart_turn` | Task recovery orchestration | - |
291
- | `smart_metrics` | Token usage inspection | - |
292
- | `smart_shell` | Safe command execution | 94% |
293
- | `build_index` | Symbol index builder | - |
287
+ | `smart_search` | Intent-aware code search with ranking and `kinds` filter (incl. ADRs) | 95% |
288
+ | `smart_context` | One-call context builder; `paths: { from, to }` mode traverses the import graph | 85% |
289
+ | `smart_shell` | Safe command execution (TAP / git-log / diff compression) | 94% |
290
+ | `smart_test` | Affected tests via graph + sandboxed runner + persisted `last_failure` | - |
291
+ | `smart_review` | One-call review preflight: diff + callers + tests + heuristic findings | - |
292
+ | `build_index` | Symbol index builder (incremental) | - |
294
293
  | `warm_cache` | File preloading (5x faster cold start) | - |
295
294
  | `git_blame` | Function-level code attribution | - |
296
295
  | `cross_project` | Multi-project context | - |
296
+ | `smart_summary` | Task checkpoint management with rolling window | 98% |
297
+ | `smart_status` | Quick session / project state inspection | - |
298
+ | `smart_doctor` | Health checks for storage, index, hooks | - |
299
+ | `smart_edit` | Targeted symbol-aware edits | - |
300
+ | `smart_turn` | Turn boundary + `nextActions[]` machine-readable plan | - |
301
+ | `smart_resume` | Lightweight alias for `smart_turn(phase: 'start', verbosity: 'minimal')` | - |
302
+ | `smart_metrics` | Token usage inspection | - |
303
+ | `smart_playbook` | Declarative workflows: composes `smart_*` tools in one call (preflight-merge, debug-flake, refactor-safe, doc-sync, ramp-up) | - |
304
+ | `global_memory` | Cross-project memory in `~/.devctx/global.db` (opt-in, scrubbed for secrets, semantic recall) | - |
297
305
 
298
306
  ### 2. Agent Rules (Task-Specific Guidance)
299
307
 
@@ -646,9 +654,12 @@ npm run verify
646
654
  ## Storage
647
655
 
648
656
  Data stored in `.devctx/`:
649
- - `index.json` - Symbol index
650
- - `state.sqlite` - Task checkpoints, metrics, patterns (Node 22+)
651
- - `metrics.jsonl` - Legacy fallback (Node 18-20)
657
+ - `index.json` - Symbol index (`INDEX_VERSION 7`: ADR + ADR sections, richer Python/Go)
658
+ - `state.sqlite` - Sessions, metrics, patterns, task handoffs, test failures, explain cache (Node 22+)
659
+ - `metrics.jsonl` - Opt-in legacy file, only when `DEVCTX_METRICS_FILE=path.jsonl` is set
660
+
661
+ Cross-project (opt-in via `DEVCTX_GLOBAL_MEMORY=true`):
662
+ - `~/.devctx/global.db` - Scrubbed decisions, patterns, playbooks, notes with semantic recall
652
663
 
653
664
  Add to `.gitignore`:
654
665
  ```
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.18.1",
4
+ "version": "1.20.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,8 +69,10 @@
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:self:json": "node ./evals/harness.js --root=../.. --corpus=./evals/corpus/self-tasks.json --json",
72
73
  "eval:realworld": "node ./evals/realworld-eval.js",
73
74
  "eval:realworld:json": "node ./evals/realworld-eval.js --json",
75
+ "eval:kpi:baseline": "node ./evals/kpi-baseline.js",
74
76
  "eval:report": "node ./evals/report.js",
75
77
  "report:metrics": "node ./scripts/report-metrics.js",
76
78
  "report:workflows": "node ./scripts/report-workflow-metrics.js",
@@ -83,5 +85,6 @@
83
85
  "js-tiktoken": "^1.0.21",
84
86
  "typescript": "^6.0.2",
85
87
  "zod": "^4.1.5"
86
- }
88
+ },
89
+ "packageManager": "pnpm@10.33.3+sha512.a19744364a7e248b92657a4ca5973f9354d21caf982579674b1c539f32c7420c47138ad8b1254df07aba9bc782d9b3029e3db34d5dbff974326eb74dac8ff489"
87
90
  }
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.18.1",
9
+ "version": "1.20.0",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "smart-context-mcp",
14
- "version": "1.18.1",
14
+ "version": "1.20.0",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },
@@ -0,0 +1,28 @@
1
+ import { embed as hashingEmbed, cosineSimilarity, buildCorpusIdf, DEFAULT_DIMENSIONS } from './hashing.js';
2
+
3
+ const HASHING_EMBEDDER = {
4
+ id: 'hashing-v1',
5
+ dimensions: DEFAULT_DIMENSIONS,
6
+ embed: (text, options = {}) => hashingEmbed(text, options),
7
+ similarity: cosineSimilarity,
8
+ buildCorpusIdf,
9
+ };
10
+
11
+ let activeEmbedder = HASHING_EMBEDDER;
12
+
13
+ export const getEmbedder = () => activeEmbedder;
14
+
15
+ export const setEmbedder = (embedder) => {
16
+ if (!embedder || typeof embedder.embed !== 'function' || typeof embedder.similarity !== 'function') {
17
+ throw new Error('Embedder must implement embed(text, opts) and similarity(a, b)');
18
+ }
19
+ activeEmbedder = {
20
+ id: embedder.id ?? 'custom',
21
+ dimensions: embedder.dimensions ?? DEFAULT_DIMENSIONS,
22
+ embed: embedder.embed,
23
+ similarity: embedder.similarity,
24
+ buildCorpusIdf: embedder.buildCorpusIdf ?? buildCorpusIdf,
25
+ };
26
+ };
27
+
28
+ export const resetEmbedder = () => { activeEmbedder = HASHING_EMBEDDER; };
@@ -0,0 +1,77 @@
1
+ import { tokenize } from './tokenize.js';
2
+
3
+ export const DEFAULT_DIMENSIONS = 256;
4
+
5
+ const fnv1a = (str) => {
6
+ let hash = 0x811c9dc5;
7
+ for (let i = 0; i < str.length; i += 1) {
8
+ hash ^= str.charCodeAt(i);
9
+ hash = Math.imul(hash, 0x01000193) >>> 0;
10
+ }
11
+ return hash;
12
+ };
13
+
14
+ const signedBucket = (token, dims) => {
15
+ const h = fnv1a(token);
16
+ const bucket = h % dims;
17
+ const sign = ((h >>> 16) & 1) === 0 ? 1 : -1;
18
+ return { bucket, sign };
19
+ };
20
+
21
+ const l2Normalize = (vector) => {
22
+ let sumSq = 0;
23
+ for (let i = 0; i < vector.length; i += 1) sumSq += vector[i] * vector[i];
24
+ if (sumSq === 0) return vector;
25
+ const norm = Math.sqrt(sumSq);
26
+ for (let i = 0; i < vector.length; i += 1) vector[i] /= norm;
27
+ return vector;
28
+ };
29
+
30
+ export const embed = (text, { dimensions = DEFAULT_DIMENSIONS, idf = null } = {}) => {
31
+ const tokens = tokenize(text);
32
+ const vector = new Float32Array(dimensions);
33
+ if (tokens.length === 0) return vector;
34
+
35
+ const counts = new Map();
36
+ for (const token of tokens) {
37
+ counts.set(token, (counts.get(token) ?? 0) + 1);
38
+ }
39
+
40
+ for (const [token, count] of counts) {
41
+ const tf = 1 + Math.log(count);
42
+ const weight = idf ? tf * (idf.get(token) ?? 1) : tf;
43
+ const { bucket, sign } = signedBucket(token, dimensions);
44
+ vector[bucket] += sign * weight;
45
+ }
46
+
47
+ return l2Normalize(vector);
48
+ };
49
+
50
+ export const cosineSimilarity = (a, b) => {
51
+ if (!a || !b || a.length !== b.length) return 0;
52
+ let dot = 0;
53
+ for (let i = 0; i < a.length; i += 1) dot += a[i] * b[i];
54
+ return dot;
55
+ };
56
+
57
+ export const buildCorpusIdf = (documents) => {
58
+ const df = new Map();
59
+ let docCount = 0;
60
+ for (const doc of documents) {
61
+ docCount += 1;
62
+ const seen = new Set();
63
+ for (const token of tokenize(doc)) {
64
+ if (!seen.has(token)) {
65
+ seen.add(token);
66
+ df.set(token, (df.get(token) ?? 0) + 1);
67
+ }
68
+ }
69
+ }
70
+ const idf = new Map();
71
+ for (const [token, freq] of df) {
72
+ idf.set(token, Math.log((docCount + 1) / (freq + 1)) + 1);
73
+ }
74
+ return idf;
75
+ };
76
+
77
+ export const _internal = { fnv1a, signedBucket, l2Normalize };
@@ -0,0 +1,91 @@
1
+ import { getEmbedder } from './embedder.js';
2
+
3
+ const symbolToText = (symbol, filePath) => {
4
+ const parts = [];
5
+ parts.push(symbol.name);
6
+ if (symbol.parent) parts.push(symbol.parent);
7
+ if (symbol.kind) parts.push(symbol.kind);
8
+ if (symbol.signature) parts.push(symbol.signature);
9
+ if (symbol.snippet) parts.push(symbol.snippet);
10
+ if (Array.isArray(symbol.decorators)) parts.push(...symbol.decorators);
11
+ if (filePath) {
12
+ const segments = filePath.replace(/\\/g, '/').split('/');
13
+ parts.push(...segments);
14
+ }
15
+ return parts.join(' ');
16
+ };
17
+
18
+ const fileToText = (relPath, fileInfo) => {
19
+ const parts = [relPath];
20
+ for (const symbol of fileInfo?.symbols ?? []) {
21
+ parts.push(symbol.name);
22
+ if (symbol.signature) parts.push(symbol.signature);
23
+ }
24
+ return parts.join(' ');
25
+ };
26
+
27
+ export const buildIndexCorpusIdf = (index) => {
28
+ const embedder = getEmbedder();
29
+ const docs = [];
30
+ for (const [relPath, fileInfo] of Object.entries(index?.files ?? {})) {
31
+ docs.push(fileToText(relPath, fileInfo));
32
+ for (const symbol of fileInfo.symbols ?? []) {
33
+ docs.push(symbolToText(symbol, relPath));
34
+ }
35
+ }
36
+ return embedder.buildCorpusIdf(docs);
37
+ };
38
+
39
+ export const embedQuery = (query, options = {}) => {
40
+ const embedder = getEmbedder();
41
+ return embedder.embed(query, options);
42
+ };
43
+
44
+ export const embedFile = (relPath, fileInfo, options = {}) => {
45
+ const embedder = getEmbedder();
46
+ return embedder.embed(fileToText(relPath, fileInfo), options);
47
+ };
48
+
49
+ export const embedSymbol = (symbol, relPath, options = {}) => {
50
+ const embedder = getEmbedder();
51
+ return embedder.embed(symbolToText(symbol, relPath), options);
52
+ };
53
+
54
+ export const semanticRankSymbols = ({ query, index, limit = 10, idf = null }) => {
55
+ if (!query || !index) return [];
56
+ const embedder = getEmbedder();
57
+ const queryVec = embedder.embed(query, { idf });
58
+ const results = [];
59
+ for (const [relPath, fileInfo] of Object.entries(index.files ?? {})) {
60
+ for (const symbol of fileInfo.symbols ?? []) {
61
+ const vec = embedder.embed(symbolToText(symbol, relPath), { idf });
62
+ const score = embedder.similarity(queryVec, vec);
63
+ if (score > 0) {
64
+ results.push({ score, path: relPath, symbol });
65
+ }
66
+ }
67
+ }
68
+ results.sort((a, b) => b.score - a.score);
69
+ return results.slice(0, limit);
70
+ };
71
+
72
+ export const semanticRankFiles = ({ query, index, limit = 10, idf = null }) => {
73
+ if (!query || !index) return [];
74
+ const embedder = getEmbedder();
75
+ const queryVec = embedder.embed(query, { idf });
76
+ const results = [];
77
+ for (const [relPath, fileInfo] of Object.entries(index.files ?? {})) {
78
+ const vec = embedder.embed(fileToText(relPath, fileInfo), { idf });
79
+ const score = embedder.similarity(queryVec, vec);
80
+ if (score > 0) {
81
+ results.push({ score, path: relPath, symbolCount: fileInfo.symbols?.length ?? 0 });
82
+ }
83
+ }
84
+ results.sort((a, b) => b.score - a.score);
85
+ return results.slice(0, limit);
86
+ };
87
+
88
+ export { getEmbedder, setEmbedder, resetEmbedder } from './embedder.js';
89
+ export { tokenize } from './tokenize.js';
90
+ export { embed, cosineSimilarity, buildCorpusIdf, DEFAULT_DIMENSIONS } from './hashing.js';
91
+ export const _internal = { symbolToText, fileToText };
@@ -0,0 +1,46 @@
1
+ const STOP_WORDS = new Set([
2
+ 'the', 'a', 'an', 'and', 'or', 'but', 'is', 'in', 'on', 'at', 'to', 'for', 'of', 'with',
3
+ 'as', 'by', 'from', 'this', 'that', 'these', 'those', 'it', 'its', 'be', 'been', 'being',
4
+ 'are', 'was', 'were', 'has', 'have', 'had', 'do', 'does', 'did', 'will', 'would', 'should',
5
+ 'can', 'could', 'may', 'might', 'must', 'shall', 'not', 'no', 'yes', 'if', 'else', 'then',
6
+ 'return', 'true', 'false', 'null', 'undefined', 'self', 'this', 'use', 'using', 'used', 'via',
7
+ ]);
8
+
9
+ const CAMEL_RE = /([a-z0-9])([A-Z])/g;
10
+ const SNAKE_RE = /[_-]+/g;
11
+ const NON_WORD_RE = /[^A-Za-z0-9_]+/g;
12
+ const NUMBER_RE = /^\d+$/;
13
+
14
+ const splitIdentifier = (token) => {
15
+ if (!token) return [];
16
+ const camelExpanded = token.replace(CAMEL_RE, '$1 $2');
17
+ const parts = camelExpanded.replace(SNAKE_RE, ' ').split(/\s+/).filter(Boolean);
18
+ const out = new Set();
19
+ out.add(token.toLowerCase());
20
+ for (const part of parts) {
21
+ const lower = part.toLowerCase();
22
+ if (!lower || NUMBER_RE.test(lower)) continue;
23
+ if (STOP_WORDS.has(lower)) continue;
24
+ if (lower.length < 2) continue;
25
+ out.add(lower);
26
+ }
27
+ return [...out];
28
+ };
29
+
30
+ export const tokenize = (text) => {
31
+ if (!text || typeof text !== 'string') return [];
32
+ const raw = text.replace(NON_WORD_RE, ' ').split(/\s+/).filter(Boolean);
33
+ const out = [];
34
+ for (const token of raw) {
35
+ if (NUMBER_RE.test(token)) continue;
36
+ const expanded = splitIdentifier(token);
37
+ for (const part of expanded) {
38
+ if (!STOP_WORDS.has(part) && part.length >= 2) {
39
+ out.push(part);
40
+ }
41
+ }
42
+ }
43
+ return out;
44
+ };
45
+
46
+ export const _internal = { STOP_WORDS, splitIdentifier };
@@ -0,0 +1,46 @@
1
+ const SECRET_PATTERNS = [
2
+ /(api[_-]?key|secret|token|password|passwd|pwd|bearer|authorization)\s*[:=]\s*['"]?([A-Za-z0-9_\-+/=]{8,})['"]?/gi,
3
+ /(aws_access_key_id|aws_secret_access_key)\s*[:=]\s*['"]?([A-Za-z0-9_\-+/=]{8,})['"]?/gi,
4
+ /sk-[A-Za-z0-9]{20,}/g,
5
+ /ghp_[A-Za-z0-9]{20,}/g,
6
+ /xoxb-[A-Za-z0-9-]{20,}/g,
7
+ /AIza[A-Za-z0-9_-]{30,}/g,
8
+ /eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g,
9
+ /-----BEGIN [A-Z ]+PRIVATE KEY-----[\s\S]+?-----END [A-Z ]+PRIVATE KEY-----/g,
10
+ /(?:postgres|postgresql|mysql|mongodb|redis):\/\/[^\s'"]+/gi,
11
+ /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,
12
+ ];
13
+
14
+ const HOME_PATH_RE = /\/home\/[^/\s'"]+/g;
15
+ const USER_PATH_RE = /\/Users\/[^/\s'"]+/g;
16
+ const WIN_USER_RE = /C:\\Users\\[^\\/\s'"]+/g;
17
+
18
+ const REDACTION = '[REDACTED]';
19
+
20
+ export const scrubContent = (content) => {
21
+ if (typeof content !== 'string') return content;
22
+ let cleaned = content;
23
+ for (const pattern of SECRET_PATTERNS) {
24
+ cleaned = cleaned.replace(pattern, REDACTION);
25
+ }
26
+ cleaned = cleaned.replace(HOME_PATH_RE, '~').replace(USER_PATH_RE, '~').replace(WIN_USER_RE, '~');
27
+ return cleaned;
28
+ };
29
+
30
+ export const containsLikelySecret = (content) => {
31
+ if (typeof content !== 'string') return false;
32
+ return SECRET_PATTERNS.some((re) => {
33
+ re.lastIndex = 0;
34
+ return re.test(content);
35
+ });
36
+ };
37
+
38
+ export const hashProjectPath = (absolutePath) => {
39
+ if (!absolutePath || typeof absolutePath !== 'string') return null;
40
+ let hash = 0x811c9dc5;
41
+ for (let i = 0; i < absolutePath.length; i += 1) {
42
+ hash ^= absolutePath.charCodeAt(i);
43
+ hash = Math.imul(hash, 0x01000193) >>> 0;
44
+ }
45
+ return `proj-${hash.toString(36)}`;
46
+ };