context-mode 0.4.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.
@@ -0,0 +1,457 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { z } from "zod";
5
+ import { PolyglotExecutor } from "./executor.js";
6
+ import { ContentStore } from "./store.js";
7
+ import { detectRuntimes, getRuntimeSummary, getAvailableLanguages, hasBunRuntime, } from "./runtime.js";
8
+ const runtimes = detectRuntimes();
9
+ const available = getAvailableLanguages(runtimes);
10
+ const server = new McpServer({
11
+ name: "context-mode",
12
+ version: "0.4.0",
13
+ });
14
+ const executor = new PolyglotExecutor({ runtimes });
15
+ // Lazy singleton — no DB overhead unless index/search is used
16
+ let _store = null;
17
+ function getStore() {
18
+ if (!_store)
19
+ _store = new ContentStore();
20
+ return _store;
21
+ }
22
+ // Build description dynamically based on detected runtimes
23
+ const langList = available.join(", ");
24
+ const bunNote = hasBunRuntime()
25
+ ? " (Bun detected — JS/TS runs 3-5x faster)"
26
+ : "";
27
+ // ─────────────────────────────────────────────────────────
28
+ // Tool: execute
29
+ // ─────────────────────────────────────────────────────────
30
+ server.registerTool("execute", {
31
+ title: "Execute Code",
32
+ description: `Execute code in a sandboxed subprocess. Only stdout enters context — raw data stays in the subprocess. Use instead of bash/cat when output would exceed 20 lines.${bunNote} Available: ${langList}.`,
33
+ inputSchema: z.object({
34
+ language: z
35
+ .enum([
36
+ "javascript",
37
+ "typescript",
38
+ "python",
39
+ "shell",
40
+ "ruby",
41
+ "go",
42
+ "rust",
43
+ "php",
44
+ "perl",
45
+ "r",
46
+ ])
47
+ .describe("Runtime language"),
48
+ code: z
49
+ .string()
50
+ .describe("Source code to execute. Use console.log (JS/TS), print (Python/Ruby/Perl/R), echo (Shell), echo (PHP), or fmt.Println (Go) to output a summary to context."),
51
+ timeout: z
52
+ .number()
53
+ .optional()
54
+ .default(30000)
55
+ .describe("Max execution time in ms"),
56
+ }),
57
+ }, async ({ language, code, timeout }) => {
58
+ try {
59
+ const result = await executor.execute({ language, code, timeout });
60
+ if (result.timedOut) {
61
+ return {
62
+ content: [
63
+ {
64
+ type: "text",
65
+ text: `Execution timed out after ${timeout}ms\n\nPartial stdout:\n${result.stdout}\n\nstderr:\n${result.stderr}`,
66
+ },
67
+ ],
68
+ isError: true,
69
+ };
70
+ }
71
+ if (result.exitCode !== 0) {
72
+ return {
73
+ content: [
74
+ {
75
+ type: "text",
76
+ text: `Exit code: ${result.exitCode}\n\nstdout:\n${result.stdout}\n\nstderr:\n${result.stderr}`,
77
+ },
78
+ ],
79
+ isError: true,
80
+ };
81
+ }
82
+ return {
83
+ content: [
84
+ { type: "text", text: result.stdout || "(no output)" },
85
+ ],
86
+ };
87
+ }
88
+ catch (err) {
89
+ const message = err instanceof Error ? err.message : String(err);
90
+ return {
91
+ content: [
92
+ { type: "text", text: `Runtime error: ${message}` },
93
+ ],
94
+ isError: true,
95
+ };
96
+ }
97
+ });
98
+ // ─────────────────────────────────────────────────────────
99
+ // Helper: index stdout into FTS5 knowledge base
100
+ // ─────────────────────────────────────────────────────────
101
+ function indexStdout(stdout, source) {
102
+ const store = getStore();
103
+ const indexed = store.index({ content: stdout, source });
104
+ return {
105
+ content: [
106
+ {
107
+ type: "text",
108
+ text: `Indexed ${indexed.totalChunks} sections (${indexed.codeChunks} with code) from: ${indexed.label}\nUse search() to query this content.`,
109
+ },
110
+ ],
111
+ };
112
+ }
113
+ // ─────────────────────────────────────────────────────────
114
+ // Tool: execute_file
115
+ // ─────────────────────────────────────────────────────────
116
+ server.registerTool("execute_file", {
117
+ title: "Execute File Processing",
118
+ description: "Read a file and process it without loading contents into context. The file is read into a FILE_CONTENT variable inside the sandbox. Only your printed summary enters context.",
119
+ inputSchema: z.object({
120
+ path: z
121
+ .string()
122
+ .describe("Absolute file path or relative to project root"),
123
+ language: z
124
+ .enum([
125
+ "javascript",
126
+ "typescript",
127
+ "python",
128
+ "shell",
129
+ "ruby",
130
+ "go",
131
+ "rust",
132
+ "php",
133
+ "perl",
134
+ "r",
135
+ ])
136
+ .describe("Runtime language"),
137
+ code: z
138
+ .string()
139
+ .describe("Code to process FILE_CONTENT. Print summary via console.log/print/echo."),
140
+ timeout: z
141
+ .number()
142
+ .optional()
143
+ .default(30000)
144
+ .describe("Max execution time in ms"),
145
+ }),
146
+ }, async ({ path, language, code, timeout }) => {
147
+ try {
148
+ const result = await executor.executeFile({
149
+ path,
150
+ language,
151
+ code,
152
+ timeout,
153
+ });
154
+ if (result.timedOut) {
155
+ return {
156
+ content: [
157
+ {
158
+ type: "text",
159
+ text: `Timed out processing ${path} after ${timeout}ms`,
160
+ },
161
+ ],
162
+ isError: true,
163
+ };
164
+ }
165
+ if (result.exitCode !== 0) {
166
+ return {
167
+ content: [
168
+ {
169
+ type: "text",
170
+ text: `Error processing ${path} (exit ${result.exitCode}):\n${result.stderr || result.stdout}`,
171
+ },
172
+ ],
173
+ isError: true,
174
+ };
175
+ }
176
+ return {
177
+ content: [
178
+ { type: "text", text: result.stdout || "(no output)" },
179
+ ],
180
+ };
181
+ }
182
+ catch (err) {
183
+ const message = err instanceof Error ? err.message : String(err);
184
+ return {
185
+ content: [
186
+ { type: "text", text: `Runtime error: ${message}` },
187
+ ],
188
+ isError: true,
189
+ };
190
+ }
191
+ });
192
+ // ─────────────────────────────────────────────────────────
193
+ // Tool: index
194
+ // ─────────────────────────────────────────────────────────
195
+ server.registerTool("index", {
196
+ title: "Index Content",
197
+ description: "Index documentation or knowledge content into a searchable BM25 knowledge base. " +
198
+ "Chunks markdown by headings (keeping code blocks intact) and stores in ephemeral FTS5 database. " +
199
+ "The full content does NOT stay in context — only a brief summary is returned.\n\n" +
200
+ "WHEN TO USE:\n" +
201
+ "- Documentation from Context7, Skills, or MCP tools (API docs, framework guides, code examples)\n" +
202
+ "- API references (endpoint details, parameter specs, response schemas)\n" +
203
+ "- MCP tools/list output (exact tool signatures and descriptions)\n" +
204
+ "- Skill prompts and instructions that are too large for context\n" +
205
+ "- README files, migration guides, changelog entries\n" +
206
+ "- Any content with code examples you may need to reference precisely\n\n" +
207
+ "After indexing, use 'search' to retrieve specific sections on-demand.\n" +
208
+ "Do NOT use for: log files, test output, CSV, build output — use 'execute_file' for those.",
209
+ inputSchema: z.object({
210
+ content: z
211
+ .string()
212
+ .optional()
213
+ .describe("Raw text/markdown to index. Provide this OR path, not both."),
214
+ path: z
215
+ .string()
216
+ .optional()
217
+ .describe("File path to read and index (content never enters context). Provide this OR content."),
218
+ source: z
219
+ .string()
220
+ .optional()
221
+ .describe("Label for the indexed content (e.g., 'Context7: React useEffect', 'Skill: frontend-design')"),
222
+ }),
223
+ }, async ({ content, path, source }) => {
224
+ if (!content && !path) {
225
+ return {
226
+ content: [
227
+ {
228
+ type: "text",
229
+ text: "Error: Either content or path must be provided",
230
+ },
231
+ ],
232
+ isError: true,
233
+ };
234
+ }
235
+ try {
236
+ const store = getStore();
237
+ const result = store.index({ content, path, source });
238
+ return {
239
+ content: [
240
+ {
241
+ type: "text",
242
+ text: `Indexed ${result.totalChunks} sections (${result.codeChunks} with code) from: ${result.label}\nUse search() to query this content.`,
243
+ },
244
+ ],
245
+ };
246
+ }
247
+ catch (err) {
248
+ const message = err instanceof Error ? err.message : String(err);
249
+ return {
250
+ content: [
251
+ { type: "text", text: `Index error: ${message}` },
252
+ ],
253
+ isError: true,
254
+ };
255
+ }
256
+ });
257
+ // ─────────────────────────────────────────────────────────
258
+ // Tool: search
259
+ // ─────────────────────────────────────────────────────────
260
+ server.registerTool("search", {
261
+ title: "Search Indexed Content",
262
+ description: "Search previously indexed content using BM25 full-text search. " +
263
+ "Returns the top matching chunks with heading context and full content. " +
264
+ "Use after 'index' to retrieve specific documentation sections, code examples, or API details on demand.\n\n" +
265
+ "WHEN TO USE:\n" +
266
+ "- Find specific code examples ('useEffect cleanup pattern')\n" +
267
+ "- Look up API signatures ('Supabase RLS policy syntax')\n" +
268
+ "- Get configuration details ('Tailwind responsive breakpoints')\n" +
269
+ "- Find migration steps ('App Router data fetching')\n\n" +
270
+ "Returns exact content — not summaries. Each result includes heading hierarchy and full section text.",
271
+ inputSchema: z.object({
272
+ query: z.string().describe("Natural language search query"),
273
+ limit: z
274
+ .number()
275
+ .optional()
276
+ .default(3)
277
+ .describe("Maximum results to return (default: 3)"),
278
+ }),
279
+ }, async ({ query, limit }) => {
280
+ try {
281
+ const store = getStore();
282
+ const results = store.search(query, limit);
283
+ if (results.length === 0) {
284
+ return {
285
+ content: [
286
+ {
287
+ type: "text",
288
+ text: `No results found for: "${query}". Make sure content has been indexed first.`,
289
+ },
290
+ ],
291
+ };
292
+ }
293
+ const formatted = results
294
+ .map((r, i) => {
295
+ const header = `--- Result ${i + 1} [${r.source}] (${r.contentType}) ---`;
296
+ const heading = `## ${r.title}`;
297
+ return `${header}\n${heading}\n\n${r.content}`;
298
+ })
299
+ .join("\n\n");
300
+ return {
301
+ content: [{ type: "text", text: formatted }],
302
+ };
303
+ }
304
+ catch (err) {
305
+ const message = err instanceof Error ? err.message : String(err);
306
+ return {
307
+ content: [
308
+ { type: "text", text: `Search error: ${message}` },
309
+ ],
310
+ isError: true,
311
+ };
312
+ }
313
+ });
314
+ // ─────────────────────────────────────────────────────────
315
+ // Tool: fetch_and_index
316
+ // ─────────────────────────────────────────────────────────
317
+ const HTML_TO_MARKDOWN_CODE = `
318
+ const url = process.argv[1];
319
+ if (!url) { console.error("No URL provided"); process.exit(1); }
320
+
321
+ async function main() {
322
+ const resp = await fetch(url);
323
+ if (!resp.ok) { console.error("HTTP " + resp.status); process.exit(1); }
324
+
325
+ let html = await resp.text();
326
+
327
+ // Strip script, style, nav, header, footer tags with content
328
+ html = html.replace(/<script[^>]*>[\\s\\S]*?<\\/script>/gi, "");
329
+ html = html.replace(/<style[^>]*>[\\s\\S]*?<\\/style>/gi, "");
330
+ html = html.replace(/<nav[^>]*>[\\s\\S]*?<\\/nav>/gi, "");
331
+ html = html.replace(/<header[^>]*>[\\s\\S]*?<\\/header>/gi, "");
332
+ html = html.replace(/<footer[^>]*>[\\s\\S]*?<\\/footer>/gi, "");
333
+
334
+ // Convert headings to markdown
335
+ html = html.replace(/<h1[^>]*>(.*?)<\\/h1>/gi, "\\n# $1\\n");
336
+ html = html.replace(/<h2[^>]*>(.*?)<\\/h2>/gi, "\\n## $1\\n");
337
+ html = html.replace(/<h3[^>]*>(.*?)<\\/h3>/gi, "\\n### $1\\n");
338
+ html = html.replace(/<h4[^>]*>(.*?)<\\/h4>/gi, "\\n#### $1\\n");
339
+
340
+ // Convert code blocks
341
+ html = html.replace(/<pre[^>]*><code[^>]*class="[^"]*language-(\\w+)"[^>]*>([\\s\\S]*?)<\\/code><\\/pre>/gi,
342
+ (_, lang, code) => "\\n\\\`\\\`\\\`" + lang + "\\n" + decodeEntities(code) + "\\n\\\`\\\`\\\`\\n");
343
+ html = html.replace(/<pre[^>]*><code[^>]*>([\\s\\S]*?)<\\/code><\\/pre>/gi,
344
+ (_, code) => "\\n\\\`\\\`\\\`\\n" + decodeEntities(code) + "\\n\\\`\\\`\\\`\\n");
345
+ html = html.replace(/<code[^>]*>([^<]*)<\\/code>/gi, "\\\`$1\\\`");
346
+
347
+ // Convert links
348
+ html = html.replace(/<a[^>]*href="([^"]*)"[^>]*>(.*?)<\\/a>/gi, "[$2]($1)");
349
+
350
+ // Convert lists
351
+ html = html.replace(/<li[^>]*>(.*?)<\\/li>/gi, "- $1\\n");
352
+
353
+ // Convert paragraphs and line breaks
354
+ html = html.replace(/<p[^>]*>(.*?)<\\/p>/gi, "\\n$1\\n");
355
+ html = html.replace(/<br\\s*\\/?>/gi, "\\n");
356
+ html = html.replace(/<hr\\s*\\/?>/gi, "\\n---\\n");
357
+
358
+ // Strip remaining HTML tags
359
+ html = html.replace(/<[^>]+>/g, "");
360
+
361
+ // Decode HTML entities
362
+ html = decodeEntities(html);
363
+
364
+ // Clean up whitespace
365
+ html = html.replace(/\\n{3,}/g, "\\n\\n").trim();
366
+
367
+ console.log(html);
368
+ }
369
+
370
+ function decodeEntities(s) {
371
+ return s
372
+ .replace(/&amp;/g, "&")
373
+ .replace(/&lt;/g, "<")
374
+ .replace(/&gt;/g, ">")
375
+ .replace(/&quot;/g, '"')
376
+ .replace(/&#39;/g, "'")
377
+ .replace(/&#x27;/g, "'")
378
+ .replace(/&#x2F;/g, "/")
379
+ .replace(/&nbsp;/g, " ");
380
+ }
381
+
382
+ main();
383
+ `;
384
+ server.registerTool("fetch_and_index", {
385
+ title: "Fetch & Index URL",
386
+ description: "Fetches URL content, converts HTML to markdown, and indexes into the searchable knowledge base. " +
387
+ "Raw content never enters context — only a brief confirmation is returned.\n\n" +
388
+ "Use INSTEAD of WebFetch/Context7 when you need to reference web documentation later via search.\n\n" +
389
+ "After fetching, use 'search' to retrieve specific sections on-demand.",
390
+ inputSchema: z.object({
391
+ url: z.string().describe("The URL to fetch and index"),
392
+ source: z
393
+ .string()
394
+ .optional()
395
+ .describe("Label for the indexed content (e.g., 'React useEffect docs', 'Supabase Auth API')"),
396
+ }),
397
+ }, async ({ url, source }) => {
398
+ try {
399
+ // Execute fetch inside subprocess — raw HTML never enters context
400
+ const fetchCode = `process.argv[1] = ${JSON.stringify(url)};\n${HTML_TO_MARKDOWN_CODE}`;
401
+ const result = await executor.execute({
402
+ language: "javascript",
403
+ code: fetchCode,
404
+ timeout: 30_000,
405
+ });
406
+ if (result.exitCode !== 0) {
407
+ return {
408
+ content: [
409
+ {
410
+ type: "text",
411
+ text: `Failed to fetch ${url}: ${result.stderr || result.stdout}`,
412
+ },
413
+ ],
414
+ isError: true,
415
+ };
416
+ }
417
+ if (!result.stdout || result.stdout.trim().length === 0) {
418
+ return {
419
+ content: [
420
+ {
421
+ type: "text",
422
+ text: `Fetched ${url} but got empty content after HTML conversion`,
423
+ },
424
+ ],
425
+ isError: true,
426
+ };
427
+ }
428
+ // Index the markdown into FTS5
429
+ return indexStdout(result.stdout, source ?? url);
430
+ }
431
+ catch (err) {
432
+ const message = err instanceof Error ? err.message : String(err);
433
+ return {
434
+ content: [
435
+ { type: "text", text: `Fetch error: ${message}` },
436
+ ],
437
+ isError: true,
438
+ };
439
+ }
440
+ });
441
+ // ─────────────────────────────────────────────────────────
442
+ // Server startup
443
+ // ─────────────────────────────────────────────────────────
444
+ async function main() {
445
+ const transport = new StdioServerTransport();
446
+ await server.connect(transport);
447
+ console.error("Context Mode MCP server v0.4.0 running on stdio");
448
+ console.error(`Detected runtimes:\n${getRuntimeSummary(runtimes)}`);
449
+ if (!hasBunRuntime()) {
450
+ console.error("\nPerformance tip: Install Bun for 3-5x faster JS/TS execution");
451
+ console.error(" curl -fsSL https://bun.sh/install | bash");
452
+ }
453
+ }
454
+ main().catch((err) => {
455
+ console.error("Fatal:", err);
456
+ process.exit(1);
457
+ });
@@ -0,0 +1,39 @@
1
+ /**
2
+ * ContentStore — FTS5 BM25-based knowledge base for context-mode.
3
+ *
4
+ * Chunks markdown content by headings (keeping code blocks intact),
5
+ * stores in SQLite FTS5, and retrieves via BM25-ranked search.
6
+ *
7
+ * Use for documentation, API references, and any content where
8
+ * you need EXACT text later — not summaries.
9
+ */
10
+ export interface IndexResult {
11
+ sourceId: number;
12
+ label: string;
13
+ totalChunks: number;
14
+ codeChunks: number;
15
+ }
16
+ export interface SearchResult {
17
+ title: string;
18
+ content: string;
19
+ source: string;
20
+ rank: number;
21
+ contentType: "code" | "prose";
22
+ }
23
+ export interface StoreStats {
24
+ sources: number;
25
+ chunks: number;
26
+ codeChunks: number;
27
+ }
28
+ export declare class ContentStore {
29
+ #private;
30
+ constructor(dbPath?: string);
31
+ index(options: {
32
+ content?: string;
33
+ path?: string;
34
+ source?: string;
35
+ }): IndexResult;
36
+ search(query: string, limit?: number): SearchResult[];
37
+ getStats(): StoreStats;
38
+ close(): void;
39
+ }