context-mode 0.5.0 → 0.5.3
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +2 -2
- package/README.md +70 -72
- package/build/executor.js +9 -9
- package/build/server.js +82 -27
- package/build/store.d.ts +1 -0
- package/build/store.js +68 -5
- package/package.json +2 -2
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
{
|
|
13
13
|
"name": "context-mode",
|
|
14
14
|
"source": "./",
|
|
15
|
-
"description": "Claude Code MCP plugin that saves
|
|
16
|
-
"version": "0.5.
|
|
15
|
+
"description": "Claude Code MCP plugin that saves 98% of your context window. Sandboxed code execution in 10 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
|
|
16
|
+
"version": "0.5.3",
|
|
17
17
|
"author": {
|
|
18
18
|
"name": "Mert Koseoğlu"
|
|
19
19
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-mode",
|
|
3
|
-
"version": "0.5.
|
|
4
|
-
"description": "Claude Code MCP plugin that saves
|
|
3
|
+
"version": "0.5.3",
|
|
4
|
+
"description": "Claude Code MCP plugin that saves 98% of your context window. Sandboxed code execution in 10 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Mert Koseoğlu",
|
|
7
7
|
"url": "https://github.com/mksglu"
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Context Mode
|
|
2
2
|
|
|
3
|
-
**Claude Code MCP plugin that saves
|
|
3
|
+
**Claude Code MCP plugin that saves 98% of your context window.**
|
|
4
4
|
|
|
5
5
|
Every tool call in Claude Code consumes context tokens. A single Playwright snapshot burns 10K-135K tokens. A Context7 docs lookup dumps 4K-10K tokens. GitHub's `list_commits` with 30 results costs 29K-64K tokens. With 5+ MCP servers active, you lose ~55K tokens before your first message — and after 30 minutes of real debugging, responses slow to a crawl.
|
|
6
6
|
|
|
@@ -12,14 +12,14 @@ Claude Code has a 200K token context window. Here's how fast popular MCP servers
|
|
|
12
12
|
|
|
13
13
|
| MCP Server | Tool | Without Context Mode | With Context Mode | Savings | Source |
|
|
14
14
|
|---|---|---|---|---|---|
|
|
15
|
-
| **Playwright** | `browser_snapshot` | 10K-135K tokens | ~
|
|
16
|
-
| **Context7** | `query-docs` | 4K-10K tokens | ~
|
|
17
|
-
| **GitHub** | `list_commits` (30) | 29K-64K tokens | ~
|
|
18
|
-
| **Sentry** | issue analysis | 5K-30K tokens | ~
|
|
19
|
-
| **Supabase** | schema queries | 2K-30K tokens | ~
|
|
20
|
-
| **Firecrawl** | `scrape` / `crawl` | 5K-50K+ tokens | ~
|
|
21
|
-
| **Chrome DevTools** | DOM / network | 5K-50K+ tokens | ~
|
|
22
|
-
| **Fetch** | `fetch` | 5K-50K tokens | ~
|
|
15
|
+
| **Playwright** | `browser_snapshot` | 10K-135K tokens | ~75 tokens | **99%** | [playwright-mcp#1233](https://github.com/microsoft/playwright-mcp/issues/1233) |
|
|
16
|
+
| **Context7** | `query-docs` | 4K-10K tokens | ~65 tokens | **98%** | [upstash/context7](https://github.com/upstash/context7) |
|
|
17
|
+
| **GitHub** | `list_commits` (30) | 29K-64K tokens | ~180 tokens | **99%** | [github-mcp-server#142](https://github.com/github/github-mcp-server/issues/142) |
|
|
18
|
+
| **Sentry** | issue analysis | 5K-30K tokens | ~85 tokens | **99%** | [getsentry/sentry-mcp](https://github.com/getsentry/sentry-mcp) |
|
|
19
|
+
| **Supabase** | schema queries | 2K-30K tokens | ~80 tokens | **99%** | [supabase-community/supabase-mcp](https://github.com/supabase-community/supabase-mcp) |
|
|
20
|
+
| **Firecrawl** | `scrape` / `crawl` | 5K-50K+ tokens | ~65 tokens | **99%** | [firecrawl](https://github.com/mendableai/firecrawl) |
|
|
21
|
+
| **Chrome DevTools** | DOM / network | 5K-50K+ tokens | ~75 tokens | **99%** | Community benchmark |
|
|
22
|
+
| **Fetch** | `fetch` | 5K-50K tokens | ~65 tokens | **99%** | Official reference server |
|
|
23
23
|
|
|
24
24
|
**Real measurement** ([Scott Spence, 2025](https://scottspence.com/posts/optimising-mcp-server-context-usage-in-claude-code)): With 81+ MCP tools enabled across multiple servers, **143K of 200K tokens (72%) consumed** — 82K tokens just for MCP tool definitions. Only 28% left for actual work.
|
|
25
25
|
|
|
@@ -29,15 +29,15 @@ Claude Code has a 200K token context window. Here's how fast popular MCP servers
|
|
|
29
29
|
|
|
30
30
|
| What you're doing | Without Context Mode | With Context Mode | Savings |
|
|
31
31
|
|---|---|---|---|
|
|
32
|
-
| Playwright `browser_snapshot` |
|
|
33
|
-
| Context7 `query-docs` (React) |
|
|
34
|
-
|
|
|
35
|
-
| Read `access.log` (500 req) | 45 KB raw log |
|
|
36
|
-
| `
|
|
37
|
-
| Git log (153 commits) | 12 KB raw log |
|
|
38
|
-
|
|
|
32
|
+
| Playwright `browser_snapshot` | 56 KB into context | 299 B summary | **99%** |
|
|
33
|
+
| Context7 `query-docs` (React) | 5.9 KB raw docs | 261 B summary | **96%** |
|
|
34
|
+
| GitHub issues (20) | 59 KB JSON response | 1.1 KB summary | **98%** |
|
|
35
|
+
| Read `access.log` (500 req) | 45 KB raw log | 155 B status breakdown | **100%** |
|
|
36
|
+
| `vitest` (30 suites) | 6 KB raw output | 337 B pass/fail | **95%** |
|
|
37
|
+
| Git log (153 commits) | 12 KB raw log | 107 B summary | **99%** |
|
|
38
|
+
| Analytics CSV (500 rows) | 86 KB raw data | 222 B summary | **100%** |
|
|
39
39
|
|
|
40
|
-
**Real aggregate across
|
|
40
|
+
**Real aggregate across 14 scenarios: 315 KB raw → 5.4 KB context (98% savings)**
|
|
41
41
|
|
|
42
42
|
## Quick Start
|
|
43
43
|
|
|
@@ -75,7 +75,7 @@ Claude calls: execute({ language: "shell", code: "gh pr list --json title,state
|
|
|
75
75
|
Returns: "3" ← 2 bytes instead of 8KB JSON
|
|
76
76
|
```
|
|
77
77
|
|
|
78
|
-
**Intent-driven search** (v0.5.
|
|
78
|
+
**Intent-driven search** (v0.5.2): When you provide an `intent` parameter and output exceeds 5KB, Context Mode uses score-based BM25 search to return only the relevant sections matching your intent.
|
|
79
79
|
|
|
80
80
|
```
|
|
81
81
|
Claude calls: execute({
|
|
@@ -83,9 +83,12 @@ Claude calls: execute({
|
|
|
83
83
|
code: "cat /var/log/app.log",
|
|
84
84
|
intent: "connection refused database error"
|
|
85
85
|
})
|
|
86
|
-
Returns:
|
|
86
|
+
Returns: section titles + searchable terms (500B) ← instead of 100KB raw log
|
|
87
87
|
```
|
|
88
88
|
|
|
89
|
+
When intent search runs, the response includes `Searchable terms` — distinctive vocabulary
|
|
90
|
+
extracted from the output via IDF scoring. Use these terms for targeted follow-up `search()` calls.
|
|
91
|
+
|
|
89
92
|
Authenticated CLIs work out of the box — `gh`, `aws`, `gcloud`, `kubectl`, `docker` credentials are passed through securely. Bun auto-detected for 3-5x faster JS/TS.
|
|
90
93
|
|
|
91
94
|
### `execute_file` — Process Files Without Loading
|
|
@@ -132,12 +135,12 @@ Use instead of WebFetch or Context7 when you need documentation — index once,
|
|
|
132
135
|
┌──────────────────────────────────────────────────────────────────┐
|
|
133
136
|
│ Without Context Mode │
|
|
134
137
|
│ │
|
|
135
|
-
│ Claude Code → Playwright snapshot →
|
|
136
|
-
│ Claude Code → Context7 docs →
|
|
137
|
-
│ Claude Code → gh pr list →
|
|
138
|
+
│ Claude Code → Playwright snapshot → 56KB into context │
|
|
139
|
+
│ Claude Code → Context7 docs → 6KB into context │
|
|
140
|
+
│ Claude Code → gh pr list → 6KB into context │
|
|
138
141
|
│ Claude Code → cat access.log → 45KB into context │
|
|
139
142
|
│ │
|
|
140
|
-
│ Total:
|
|
143
|
+
│ Total: 113KB consumed = ~29,000 tokens = 14% of context gone │
|
|
141
144
|
└──────────────────────────────────────────────────────────────────┘
|
|
142
145
|
|
|
143
146
|
┌──────────────────────────────────────────────────────────────────┐
|
|
@@ -145,10 +148,10 @@ Use instead of WebFetch or Context7 when you need documentation — index once,
|
|
|
145
148
|
│ │
|
|
146
149
|
│ Claude Code → fetch_and_index(url) → "Indexed 8 sections" (50B)│
|
|
147
150
|
│ Claude Code → search("snapshot") → exact element (500B) │
|
|
148
|
-
│ Claude Code → execute("gh pr list") → "
|
|
149
|
-
│ Claude Code → execute_file(log) → "500:
|
|
151
|
+
│ Claude Code → execute("gh pr list") → "5 PRs, +59 -0" (719B)│
|
|
152
|
+
│ Claude Code → execute_file(log) → "500:13, 404:13" (155B)│
|
|
150
153
|
│ │
|
|
151
|
-
│ Total:
|
|
154
|
+
│ Total: 1.4KB consumed = ~350 tokens = 0.18% of context │
|
|
152
155
|
└──────────────────────────────────────────────────────────────────┘
|
|
153
156
|
```
|
|
154
157
|
|
|
@@ -163,7 +166,7 @@ Use instead of WebFetch or Context7 when you need documentation — index once,
|
|
|
163
166
|
│ │ • 10 language runtimes │ │
|
|
164
167
|
│ │ • Sandboxed subprocess │ │
|
|
165
168
|
│ │ • Auth passthrough │ │
|
|
166
|
-
│ │ •
|
|
169
|
+
│ │ • Intent-driven search │ │
|
|
167
170
|
│ └────────────────────────┘ │
|
|
168
171
|
│ │
|
|
169
172
|
│ ┌────────────────────────┐ │
|
|
@@ -172,6 +175,7 @@ Use instead of WebFetch or Context7 when you need documentation — index once,
|
|
|
172
175
|
│ │ • BM25 ranking │ │
|
|
173
176
|
│ │ • Porter stemming │ │
|
|
174
177
|
│ │ • Heading-aware chunks │ │
|
|
178
|
+
│ │ • Vocabulary hints │ │
|
|
175
179
|
│ └────────────────────────┘ │
|
|
176
180
|
└──────────────────────────────┘
|
|
177
181
|
```
|
|
@@ -213,42 +217,36 @@ ORDER BY rank LIMIT 3;
|
|
|
213
217
|
|
|
214
218
|
**Lazy singleton:** Database created only when `index` or `search` is first called — zero overhead for sessions that don't use it.
|
|
215
219
|
|
|
216
|
-
###
|
|
217
|
-
|
|
218
|
-
When subprocess output exceeds the 100KB buffer, Context Mode preserves both head and tail:
|
|
219
|
-
|
|
220
|
-
```
|
|
221
|
-
Head (60%): Initial output with context
|
|
222
|
-
... [47 lines / 3.2KB truncated — showing first 12 + last 8 lines] ...
|
|
223
|
-
Tail (40%): Final output with errors/results
|
|
224
|
-
```
|
|
220
|
+
### Intent-Driven Search (v0.5.2)
|
|
225
221
|
|
|
226
|
-
|
|
222
|
+
When `execute` or `execute_file` is called with an `intent` parameter and output exceeds 5KB, Context Mode uses score-based BM25 search to return only the relevant sections:
|
|
227
223
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
224
|
+
- **Score-based search**: Searches ALL intent words independently, ranks chunks by match count
|
|
225
|
+
- **Searchable terms**: Distinctive vocabulary hints extracted via IDF scoring, helping you craft precise follow-up `search()` calls
|
|
226
|
+
- **Smarter chunk titles**: Uses the first content line of each chunk instead of generic "Section N" labels
|
|
231
227
|
|
|
232
228
|
```
|
|
233
|
-
|
|
234
|
-
stdout (100KB) →
|
|
235
|
-
Problem: relevant info in the middle is lost
|
|
229
|
+
Without intent:
|
|
230
|
+
stdout (100KB) → full output enters context
|
|
236
231
|
|
|
237
|
-
|
|
238
|
-
stdout (100KB) → chunk by lines → in-memory FTS5 →
|
|
239
|
-
Result: only what you need enters context
|
|
232
|
+
With intent:
|
|
233
|
+
stdout (100KB) → chunk by lines → in-memory FTS5 → score all intent words → top chunks + searchable terms
|
|
234
|
+
Result: only what you need enters context, plus vocabulary for targeted follow-ups
|
|
240
235
|
```
|
|
241
236
|
|
|
242
|
-
|
|
237
|
+
**31% to 100% recall on real-world CHANGELOG test** — the score-based approach finds every relevant section, not just those matching a single query string.
|
|
243
238
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
|
247
|
-
|
|
248
|
-
|
|
|
249
|
-
|
|
|
239
|
+
Tested across 5 real-world scenarios:
|
|
240
|
+
|
|
241
|
+
| Scenario | Without Intent | With Intent | Size Reduction |
|
|
242
|
+
|---|---|---|---|
|
|
243
|
+
| Server log error (line 347/500) | error lost in output | **found** | 1.5 KB vs 5.0 KB |
|
|
244
|
+
| 3 test failures among 200 tests | only 2/3 visible | **all 3 found** | 2.4 KB vs 5.0 KB |
|
|
245
|
+
| 2 build warnings among 300 lines | both lost in output | **both found** | 2.1 KB vs 5.0 KB |
|
|
246
|
+
| API auth error (line 743/1000) | error lost in output | **found** | 1.2 KB vs 4.9 KB |
|
|
247
|
+
| Semantic gap (CHANGELOG search) | 31% recall | **100% recall** | Full coverage |
|
|
250
248
|
|
|
251
|
-
|
|
249
|
+
Intent search finds the target every time while using 50-75% fewer bytes.
|
|
252
250
|
|
|
253
251
|
### HTML to Markdown Conversion
|
|
254
252
|
|
|
@@ -269,17 +267,17 @@ Tested with tools from popular MCP servers and Claude Code workflows:
|
|
|
269
267
|
|
|
270
268
|
| Scenario | Tool | Raw | Context | Savings |
|
|
271
269
|
|---|---|---|---|---|
|
|
272
|
-
| Playwright page snapshot | `
|
|
273
|
-
| Context7 React docs | `
|
|
274
|
-
| Context7
|
|
275
|
-
| Context7
|
|
276
|
-
|
|
|
277
|
-
| GitHub
|
|
278
|
-
| Access log (500 req) | `execute_file` | 45.1 KB |
|
|
279
|
-
| Analytics CSV (500 rows) | `execute_file` | 85.5 KB |
|
|
280
|
-
| MCP tools manifest (40 tools) | `execute_file` | 17.0 KB |
|
|
281
|
-
|
|
|
282
|
-
| Git log (153 commits) | `execute` | 11.6 KB |
|
|
270
|
+
| Playwright page snapshot | `execute` | 56.2 KB | 299 B | **99%** |
|
|
271
|
+
| Context7 React docs | `execute` | 5.9 KB | 261 B | **96%** |
|
|
272
|
+
| Context7 Next.js docs | `execute` | 6.5 KB | 249 B | **96%** |
|
|
273
|
+
| Context7 Tailwind docs | `execute` | 4.0 KB | 186 B | **95%** |
|
|
274
|
+
| GitHub Issues (20) | `execute` | 58.9 KB | 1.1 KB | **98%** |
|
|
275
|
+
| GitHub PR list (5) | `execute` | 6.4 KB | 719 B | **89%** |
|
|
276
|
+
| Access log (500 req) | `execute_file` | 45.1 KB | 155 B | **100%** |
|
|
277
|
+
| Analytics CSV (500 rows) | `execute_file` | 85.5 KB | 222 B | **100%** |
|
|
278
|
+
| MCP tools manifest (40 tools) | `execute_file` | 17.0 KB | 742 B | **96%** |
|
|
279
|
+
| Test output (30 suites) | `execute` | 6.0 KB | 337 B | **95%** |
|
|
280
|
+
| Git log (153 commits) | `execute` | 11.6 KB | 107 B | **99%** |
|
|
283
281
|
|
|
284
282
|
### Session Impact
|
|
285
283
|
|
|
@@ -287,9 +285,9 @@ Typical 45-minute debugging session:
|
|
|
287
285
|
|
|
288
286
|
| Metric | Without | With | Delta |
|
|
289
287
|
|---|---|---|---|
|
|
290
|
-
| Context consumed |
|
|
291
|
-
| Tokens used | ~
|
|
292
|
-
| Context remaining |
|
|
288
|
+
| Context consumed | 315 KB | 5.4 KB | **-98%** |
|
|
289
|
+
| Tokens used | ~80,600 | ~1,400 | **-98%** |
|
|
290
|
+
| Context remaining | 60% | 99% | **+39pp** |
|
|
293
291
|
| Time before slowdown | ~30 min | ~3 hours | **+6x** |
|
|
294
292
|
|
|
295
293
|
## Tool Decision Matrix
|
|
@@ -388,13 +386,13 @@ Just ask naturally — Claude automatically routes through Context Mode when it
|
|
|
388
386
|
|
|
389
387
|
## Test Suite
|
|
390
388
|
|
|
391
|
-
|
|
389
|
+
100+ tests across 4 suites:
|
|
392
390
|
|
|
393
391
|
| Suite | Tests | Coverage |
|
|
394
392
|
|---|---|---|
|
|
395
|
-
| Executor | 55 | 10 languages, sandbox,
|
|
393
|
+
| Executor | 55 | 10 languages, sandbox, output handling, concurrency, timeouts |
|
|
396
394
|
| ContentStore | 40 | FTS5 schema, BM25 ranking, chunking, stemming, plain text indexing |
|
|
397
|
-
| Intent Search |
|
|
395
|
+
| Intent Search | 5 | Intent-driven search across 5 real-world scenarios (incl. semantic gap) |
|
|
398
396
|
| MCP Integration | 24 | JSON-RPC protocol, all 5 tools, fetch_and_index, errors |
|
|
399
397
|
|
|
400
398
|
## Development
|
|
@@ -406,7 +404,7 @@ npm install
|
|
|
406
404
|
npm run build
|
|
407
405
|
npm test # executor (55 tests)
|
|
408
406
|
npm run test:store # FTS5/BM25 (40 tests)
|
|
409
|
-
npm run test:all # all suites (
|
|
407
|
+
npm run test:all # all suites (100+ tests)
|
|
410
408
|
```
|
|
411
409
|
|
|
412
410
|
## License
|
package/build/executor.js
CHANGED
|
@@ -232,23 +232,23 @@ export class PolyglotExecutor {
|
|
|
232
232
|
switch (language) {
|
|
233
233
|
case "javascript":
|
|
234
234
|
case "typescript":
|
|
235
|
-
return `const FILE_CONTENT = require("fs").readFileSync(
|
|
235
|
+
return `const FILE_CONTENT_PATH = ${escaped};\nconst FILE_CONTENT = require("fs").readFileSync(FILE_CONTENT_PATH, "utf-8");\n${code}`;
|
|
236
236
|
case "python":
|
|
237
|
-
return `
|
|
237
|
+
return `FILE_CONTENT_PATH = ${escaped}\nwith open(FILE_CONTENT_PATH, "r") as _f:\n FILE_CONTENT = _f.read()\n${code}`;
|
|
238
238
|
case "shell":
|
|
239
|
-
return `
|
|
239
|
+
return `FILE_CONTENT_PATH=${escaped}\nFILE_CONTENT=$(cat ${escaped})\n${code}`;
|
|
240
240
|
case "ruby":
|
|
241
|
-
return `
|
|
241
|
+
return `FILE_CONTENT_PATH = ${escaped}\nFILE_CONTENT = File.read(FILE_CONTENT_PATH)\n${code}`;
|
|
242
242
|
case "go":
|
|
243
|
-
return `package main\n\nimport (\n\t"fmt"\n\t"os"\n)\n\nfunc main() {\n\tb, _ := os.ReadFile(
|
|
243
|
+
return `package main\n\nimport (\n\t"fmt"\n\t"os"\n)\n\nvar FILE_CONTENT_PATH = ${escaped}\n\nfunc main() {\n\tb, _ := os.ReadFile(FILE_CONTENT_PATH)\n\tFILE_CONTENT := string(b)\n\t_ = FILE_CONTENT\n\t_ = fmt.Sprint()\n${code}\n}\n`;
|
|
244
244
|
case "rust":
|
|
245
|
-
return `use std::fs;\n\nfn main() {\n let file_content = fs::read_to_string(
|
|
245
|
+
return `use std::fs;\n\nfn main() {\n let file_content_path = ${escaped};\n let file_content = fs::read_to_string(file_content_path).unwrap();\n${code}\n}\n`;
|
|
246
246
|
case "php":
|
|
247
|
-
return `<?php\n$FILE_CONTENT = file_get_contents($
|
|
247
|
+
return `<?php\n$FILE_CONTENT_PATH = ${escaped};\n$FILE_CONTENT = file_get_contents($FILE_CONTENT_PATH);\n${code}`;
|
|
248
248
|
case "perl":
|
|
249
|
-
return `
|
|
249
|
+
return `my $FILE_CONTENT_PATH = ${escaped};\nopen(my $fh, '<', $FILE_CONTENT_PATH) or die "Cannot open: $!";\nmy $FILE_CONTENT = do { local $/; <$fh> };\nclose($fh);\n${code}`;
|
|
250
250
|
case "r":
|
|
251
|
-
return `
|
|
251
|
+
return `FILE_CONTENT_PATH <- ${escaped}\nFILE_CONTENT <- readLines(FILE_CONTENT_PATH, warn=FALSE)\nFILE_CONTENT <- paste(FILE_CONTENT, collapse="\\n")\n${code}`;
|
|
252
252
|
}
|
|
253
253
|
}
|
|
254
254
|
}
|
package/build/server.js
CHANGED
|
@@ -5,11 +5,12 @@ import { z } from "zod";
|
|
|
5
5
|
import { PolyglotExecutor } from "./executor.js";
|
|
6
6
|
import { ContentStore } from "./store.js";
|
|
7
7
|
import { detectRuntimes, getRuntimeSummary, getAvailableLanguages, hasBunRuntime, } from "./runtime.js";
|
|
8
|
+
const VERSION = "0.5.3";
|
|
8
9
|
const runtimes = detectRuntimes();
|
|
9
10
|
const available = getAvailableLanguages(runtimes);
|
|
10
11
|
const server = new McpServer({
|
|
11
12
|
name: "context-mode",
|
|
12
|
-
version:
|
|
13
|
+
version: VERSION,
|
|
13
14
|
});
|
|
14
15
|
const executor = new PolyglotExecutor({ runtimes });
|
|
15
16
|
// Lazy singleton — no DB overhead unless index/search is used
|
|
@@ -57,8 +58,9 @@ server.registerTool("execute", {
|
|
|
57
58
|
.string()
|
|
58
59
|
.optional()
|
|
59
60
|
.describe("What you're looking for in the output. When provided and output is large (>5KB), " +
|
|
60
|
-
"
|
|
61
|
-
"Example: '
|
|
61
|
+
"indexes output into knowledge base and returns section titles + previews — not full content. " +
|
|
62
|
+
"Use search() to retrieve specific sections. Example: 'failing tests', 'HTTP 500 errors'." +
|
|
63
|
+
"\n\nTIP: Use specific technical terms, not just concepts. Check 'Searchable terms' in the response for available vocabulary."),
|
|
62
64
|
}),
|
|
63
65
|
}, async ({ language, code, timeout, intent }) => {
|
|
64
66
|
try {
|
|
@@ -79,7 +81,7 @@ server.registerTool("execute", {
|
|
|
79
81
|
if (intent && intent.trim().length > 0 && Buffer.byteLength(output) > INTENT_SEARCH_THRESHOLD) {
|
|
80
82
|
return {
|
|
81
83
|
content: [
|
|
82
|
-
{ type: "text", text: intentSearch(output, intent) },
|
|
84
|
+
{ type: "text", text: intentSearch(output, intent, `execute:${language}:error`) },
|
|
83
85
|
],
|
|
84
86
|
isError: true,
|
|
85
87
|
};
|
|
@@ -96,7 +98,7 @@ server.registerTool("execute", {
|
|
|
96
98
|
if (intent && intent.trim().length > 0 && Buffer.byteLength(stdout) > INTENT_SEARCH_THRESHOLD) {
|
|
97
99
|
return {
|
|
98
100
|
content: [
|
|
99
|
-
{ type: "text", text: intentSearch(stdout, intent) },
|
|
101
|
+
{ type: "text", text: intentSearch(stdout, intent, `execute:${language}`) },
|
|
100
102
|
],
|
|
101
103
|
};
|
|
102
104
|
}
|
|
@@ -135,30 +137,79 @@ function indexStdout(stdout, source) {
|
|
|
135
137
|
// Helper: intent-driven search on execution output
|
|
136
138
|
// ─────────────────────────────────────────────────────────
|
|
137
139
|
const INTENT_SEARCH_THRESHOLD = 5_000; // bytes — ~80-100 lines
|
|
138
|
-
function intentSearch(stdout, intent, maxResults = 5) {
|
|
139
|
-
const
|
|
140
|
+
function intentSearch(stdout, intent, source, maxResults = 5) {
|
|
141
|
+
const totalLines = stdout.split("\n").length;
|
|
142
|
+
const totalBytes = Buffer.byteLength(stdout);
|
|
143
|
+
// Index into the PERSISTENT store so user can search() later
|
|
144
|
+
const persistent = getStore();
|
|
145
|
+
const indexed = persistent.indexPlainText(stdout, source);
|
|
146
|
+
// Search with an ephemeral store to find matching section titles
|
|
147
|
+
const ephemeral = new ContentStore(":memory:");
|
|
140
148
|
try {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const results = store.search(intent, maxResults);
|
|
149
|
+
ephemeral.indexPlainText(stdout, source);
|
|
150
|
+
let results = ephemeral.search(intent, maxResults);
|
|
151
|
+
// Score-based relaxed search: search ALL words, rank by match count
|
|
145
152
|
if (results.length === 0) {
|
|
146
|
-
|
|
147
|
-
|
|
153
|
+
const words = intent.trim().split(/\s+/).filter(w => w.length > 2).slice(0, 20);
|
|
154
|
+
if (words.length > 0) {
|
|
155
|
+
const sectionScores = new Map();
|
|
156
|
+
for (const word of words) {
|
|
157
|
+
const wordResults = ephemeral.search(word, 10);
|
|
158
|
+
for (const r of wordResults) {
|
|
159
|
+
const existing = sectionScores.get(r.title);
|
|
160
|
+
if (existing) {
|
|
161
|
+
existing.score += 1;
|
|
162
|
+
if (r.rank < existing.bestRank) {
|
|
163
|
+
existing.bestRank = r.rank;
|
|
164
|
+
existing.result = r;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
sectionScores.set(r.title, { result: r, score: 1, bestRank: r.rank });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
results = Array.from(sectionScores.values())
|
|
173
|
+
.sort((a, b) => b.score - a.score || a.bestRank - b.bestRank)
|
|
174
|
+
.slice(0, maxResults)
|
|
175
|
+
.map(s => s.result);
|
|
176
|
+
}
|
|
148
177
|
}
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
.
|
|
157
|
-
|
|
158
|
-
|
|
178
|
+
// Extract distinctive terms as vocabulary hints for the LLM
|
|
179
|
+
const distinctiveTerms = persistent.getDistinctiveTerms(indexed.sourceId);
|
|
180
|
+
if (results.length === 0) {
|
|
181
|
+
const lines = [
|
|
182
|
+
`Indexed ${indexed.totalChunks} sections from "${source}" into knowledge base.`,
|
|
183
|
+
`No sections matched intent "${intent}" in ${totalLines}-line output (${(totalBytes / 1024).toFixed(1)}KB).`,
|
|
184
|
+
];
|
|
185
|
+
if (distinctiveTerms.length > 0) {
|
|
186
|
+
lines.push("");
|
|
187
|
+
lines.push(`Searchable terms: ${distinctiveTerms.join(", ")}`);
|
|
188
|
+
}
|
|
189
|
+
lines.push("");
|
|
190
|
+
lines.push("Use search() to explore the indexed content.");
|
|
191
|
+
return lines.join("\n");
|
|
192
|
+
}
|
|
193
|
+
// Return ONLY titles + first-line previews — not full content
|
|
194
|
+
const lines = [
|
|
195
|
+
`Indexed ${indexed.totalChunks} sections from "${source}" into knowledge base.`,
|
|
196
|
+
`${results.length} sections matched "${intent}" (${totalLines} lines, ${(totalBytes / 1024).toFixed(1)}KB):`,
|
|
197
|
+
"",
|
|
198
|
+
];
|
|
199
|
+
for (const r of results) {
|
|
200
|
+
const preview = r.content.split("\n")[0].slice(0, 120);
|
|
201
|
+
lines.push(` - ${r.title}: ${preview}`);
|
|
202
|
+
}
|
|
203
|
+
if (distinctiveTerms.length > 0) {
|
|
204
|
+
lines.push("");
|
|
205
|
+
lines.push(`Searchable terms: ${distinctiveTerms.join(", ")}`);
|
|
206
|
+
}
|
|
207
|
+
lines.push("");
|
|
208
|
+
lines.push("Use search() to retrieve full content of any section.");
|
|
209
|
+
return lines.join("\n");
|
|
159
210
|
}
|
|
160
211
|
finally {
|
|
161
|
-
|
|
212
|
+
ephemeral.close();
|
|
162
213
|
}
|
|
163
214
|
}
|
|
164
215
|
// ─────────────────────────────────────────────────────────
|
|
@@ -223,7 +274,7 @@ server.registerTool("execute_file", {
|
|
|
223
274
|
if (intent && intent.trim().length > 0 && Buffer.byteLength(output) > INTENT_SEARCH_THRESHOLD) {
|
|
224
275
|
return {
|
|
225
276
|
content: [
|
|
226
|
-
{ type: "text", text: intentSearch(output, intent) },
|
|
277
|
+
{ type: "text", text: intentSearch(output, intent, `file:${path}:error`) },
|
|
227
278
|
],
|
|
228
279
|
isError: true,
|
|
229
280
|
};
|
|
@@ -239,7 +290,7 @@ server.registerTool("execute_file", {
|
|
|
239
290
|
if (intent && intent.trim().length > 0 && Buffer.byteLength(stdout) > INTENT_SEARCH_THRESHOLD) {
|
|
240
291
|
return {
|
|
241
292
|
content: [
|
|
242
|
-
{ type: "text", text: intentSearch(stdout, intent) },
|
|
293
|
+
{ type: "text", text: intentSearch(stdout, intent, `file:${path}`) },
|
|
243
294
|
],
|
|
244
295
|
};
|
|
245
296
|
}
|
|
@@ -337,6 +388,10 @@ server.registerTool("search", {
|
|
|
337
388
|
"- Look up API signatures ('Supabase RLS policy syntax')\n" +
|
|
338
389
|
"- Get configuration details ('Tailwind responsive breakpoints')\n" +
|
|
339
390
|
"- Find migration steps ('App Router data fetching')\n\n" +
|
|
391
|
+
"SEARCH TIPS:\n" +
|
|
392
|
+
"- Use specific technical terms, not concepts ('__proto__' not 'security')\n" +
|
|
393
|
+
"- Check 'Searchable terms' from execute/execute_file results for available vocabulary\n" +
|
|
394
|
+
"- Combine multiple specific terms for better results\n\n" +
|
|
340
395
|
"Returns exact content — not summaries. Each result includes heading hierarchy and full section text.",
|
|
341
396
|
inputSchema: z.object({
|
|
342
397
|
query: z.string().describe("Natural language search query"),
|
|
@@ -514,7 +569,7 @@ server.registerTool("fetch_and_index", {
|
|
|
514
569
|
async function main() {
|
|
515
570
|
const transport = new StdioServerTransport();
|
|
516
571
|
await server.connect(transport);
|
|
517
|
-
console.error(
|
|
572
|
+
console.error(`Context Mode MCP server v${VERSION} running on stdio`);
|
|
518
573
|
console.error(`Detected runtimes:\n${getRuntimeSummary(runtimes)}`);
|
|
519
574
|
if (!hasBunRuntime()) {
|
|
520
575
|
console.error("\nPerformance tip: Install Bun for 3-5x faster JS/TS execution");
|
package/build/store.d.ts
CHANGED
|
@@ -40,6 +40,7 @@ export declare class ContentStore {
|
|
|
40
40
|
*/
|
|
41
41
|
indexPlainText(content: string, source: string, linesPerChunk?: number): IndexResult;
|
|
42
42
|
search(query: string, limit?: number): SearchResult[];
|
|
43
|
+
getDistinctiveTerms(sourceId: number, maxTerms?: number): string[];
|
|
43
44
|
getStats(): StoreStats;
|
|
44
45
|
close(): void;
|
|
45
46
|
}
|
package/build/store.js
CHANGED
|
@@ -12,6 +12,24 @@ import { readFileSync } from "node:fs";
|
|
|
12
12
|
import { tmpdir } from "node:os";
|
|
13
13
|
import { join } from "node:path";
|
|
14
14
|
// ─────────────────────────────────────────────────────────
|
|
15
|
+
// Constants
|
|
16
|
+
// ─────────────────────────────────────────────────────────
|
|
17
|
+
const STOPWORDS = new Set([
|
|
18
|
+
"the", "and", "for", "are", "but", "not", "you", "all", "can", "had",
|
|
19
|
+
"her", "was", "one", "our", "out", "has", "his", "how", "its", "may",
|
|
20
|
+
"new", "now", "old", "see", "way", "who", "did", "get", "got", "let",
|
|
21
|
+
"say", "she", "too", "use", "will", "with", "this", "that", "from",
|
|
22
|
+
"they", "been", "have", "many", "some", "them", "than", "each", "make",
|
|
23
|
+
"like", "just", "over", "such", "take", "into", "year", "your", "good",
|
|
24
|
+
"could", "would", "about", "which", "their", "there", "other", "after",
|
|
25
|
+
"should", "through", "also", "more", "most", "only", "very", "when",
|
|
26
|
+
"what", "then", "these", "those", "being", "does", "done", "both",
|
|
27
|
+
"same", "still", "while", "where", "here", "were", "much",
|
|
28
|
+
// Common in code/changelogs
|
|
29
|
+
"update", "updates", "updated", "deps", "dev", "tests", "test",
|
|
30
|
+
"add", "added", "fix", "fixed", "run", "running", "using",
|
|
31
|
+
]);
|
|
32
|
+
// ─────────────────────────────────────────────────────────
|
|
15
33
|
// Helpers
|
|
16
34
|
// ─────────────────────────────────────────────────────────
|
|
17
35
|
function sanitizeQuery(query) {
|
|
@@ -155,6 +173,46 @@ export class ContentStore {
|
|
|
155
173
|
contentType: r.content_type,
|
|
156
174
|
}));
|
|
157
175
|
}
|
|
176
|
+
// ── Vocabulary ──
|
|
177
|
+
getDistinctiveTerms(sourceId, maxTerms = 40) {
|
|
178
|
+
const stats = this.#db
|
|
179
|
+
.prepare("SELECT chunk_count FROM sources WHERE id = ?")
|
|
180
|
+
.get(sourceId);
|
|
181
|
+
if (!stats || stats.chunk_count < 3)
|
|
182
|
+
return [];
|
|
183
|
+
const totalChunks = stats.chunk_count;
|
|
184
|
+
const minAppearances = 2;
|
|
185
|
+
const maxAppearances = Math.max(3, Math.ceil(totalChunks * 0.4));
|
|
186
|
+
const rows = this.#db
|
|
187
|
+
.prepare("SELECT content FROM chunks WHERE source_id = ?")
|
|
188
|
+
.all(sourceId);
|
|
189
|
+
// Count document frequency (how many sections contain each word)
|
|
190
|
+
const docFreq = new Map();
|
|
191
|
+
for (const row of rows) {
|
|
192
|
+
const words = new Set(row.content
|
|
193
|
+
.toLowerCase()
|
|
194
|
+
.split(/[^\p{L}\p{N}_-]+/u)
|
|
195
|
+
.filter((w) => w.length >= 3 && !STOPWORDS.has(w)));
|
|
196
|
+
for (const word of words) {
|
|
197
|
+
docFreq.set(word, (docFreq.get(word) ?? 0) + 1);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const filtered = Array.from(docFreq.entries())
|
|
201
|
+
.filter(([, count]) => count >= minAppearances && count <= maxAppearances);
|
|
202
|
+
// Score: IDF (rarity) + length bonus + identifier bonus (underscore/camelCase)
|
|
203
|
+
const scored = filtered.map(([word, count]) => {
|
|
204
|
+
const idf = Math.log(totalChunks / count);
|
|
205
|
+
const lenBonus = Math.min(word.length / 20, 0.5);
|
|
206
|
+
const hasSpecialChars = /[_]/.test(word);
|
|
207
|
+
const isCamelOrLong = word.length >= 12;
|
|
208
|
+
const identifierBonus = hasSpecialChars ? 1.5 : isCamelOrLong ? 0.8 : 0;
|
|
209
|
+
return { word, score: idf + lenBonus + identifierBonus };
|
|
210
|
+
});
|
|
211
|
+
return scored
|
|
212
|
+
.sort((a, b) => b.score - a.score)
|
|
213
|
+
.slice(0, maxTerms)
|
|
214
|
+
.map((s) => s.word);
|
|
215
|
+
}
|
|
158
216
|
// ── Stats ──
|
|
159
217
|
getStats() {
|
|
160
218
|
const sources = this.#db.prepare("SELECT COUNT(*) as c FROM sources").get()?.c ?? 0;
|
|
@@ -246,10 +304,14 @@ export class ContentStore {
|
|
|
246
304
|
sections.length <= 200 &&
|
|
247
305
|
sections.every((s) => Buffer.byteLength(s) < 5000)) {
|
|
248
306
|
return sections
|
|
249
|
-
.map((section, i) =>
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
307
|
+
.map((section, i) => {
|
|
308
|
+
const trimmed = section.trim();
|
|
309
|
+
const firstLine = trimmed.split("\n")[0].slice(0, 80);
|
|
310
|
+
return {
|
|
311
|
+
title: firstLine || `Section ${i + 1}`,
|
|
312
|
+
content: trimmed,
|
|
313
|
+
};
|
|
314
|
+
})
|
|
253
315
|
.filter((s) => s.content.length > 0);
|
|
254
316
|
}
|
|
255
317
|
const lines = text.split("\n");
|
|
@@ -267,8 +329,9 @@ export class ContentStore {
|
|
|
267
329
|
break;
|
|
268
330
|
const startLine = i + 1;
|
|
269
331
|
const endLine = Math.min(i + slice.length, lines.length);
|
|
332
|
+
const firstLine = slice[0]?.trim().slice(0, 80);
|
|
270
333
|
chunks.push({
|
|
271
|
-
title: `Lines ${startLine}-${endLine}`,
|
|
334
|
+
title: firstLine || `Lines ${startLine}-${endLine}`,
|
|
272
335
|
content: slice.join("\n"),
|
|
273
336
|
});
|
|
274
337
|
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-mode",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Claude Code MCP plugin that saves
|
|
5
|
+
"description": "Claude Code MCP plugin that saves 98% of your context window. Sandboxed code execution, FTS5 knowledge base, and intent-driven search.",
|
|
6
6
|
"author": "Mert Koseoğlu",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"keywords": [
|