smart-coding-mcp 2.0.0 → 2.1.1

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/index.js CHANGED
@@ -1,7 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
- import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
4
+ import {
5
+ CallToolRequestSchema,
6
+ ListToolsRequestSchema,
7
+ } from "@modelcontextprotocol/sdk/types.js";
5
8
  import fs from "fs/promises";
6
9
  import { createRequire } from "module";
7
10
 
@@ -10,7 +13,7 @@ const require = createRequire(import.meta.url);
10
13
  const packageJson = require("./package.json");
11
14
 
12
15
  import { loadConfig } from "./lib/config.js";
13
- import { EmbeddingsCache } from "./lib/cache.js";
16
+ import { SQLiteCache } from "./lib/sqlite-cache.js";
14
17
  import { createEmbedder } from "./lib/mrl-embedder.js";
15
18
  import { CodebaseIndexer } from "./features/index-codebase.js";
16
19
  import { HybridSearch } from "./features/hybrid-search.js";
@@ -24,24 +27,30 @@ import * as GetStatusFeature from "./features/get-status.js";
24
27
 
25
28
  // Parse workspace from command line arguments
26
29
  const args = process.argv.slice(2);
27
- const workspaceIndex = args.findIndex(arg => arg.startsWith('--workspace'));
30
+ const workspaceIndex = args.findIndex((arg) => arg.startsWith("--workspace"));
28
31
  let workspaceDir = process.cwd(); // Default to current directory
29
32
 
30
33
  if (workspaceIndex !== -1) {
31
34
  const arg = args[workspaceIndex];
32
35
  let rawWorkspace = null;
33
36
 
34
- if (arg.includes('=')) {
35
- rawWorkspace = arg.split('=')[1];
37
+ if (arg.includes("=")) {
38
+ rawWorkspace = arg.split("=")[1];
36
39
  } else if (workspaceIndex + 1 < args.length) {
37
40
  rawWorkspace = args[workspaceIndex + 1];
38
41
  }
39
42
 
40
43
  // Check if IDE variable wasn't expanded (contains ${})
41
- if (rawWorkspace && rawWorkspace.includes('${')) {
42
- console.error(`[Server] FATAL: Workspace variable "${rawWorkspace}" was not expanded by your IDE.`);
43
- console.error(`[Server] This typically means your MCP client does not support dynamic variables.`);
44
- console.error(`[Server] Please use an absolute path instead: --workspace /path/to/your/project`);
44
+ if (rawWorkspace && rawWorkspace.includes("${")) {
45
+ console.error(
46
+ `[Server] FATAL: Workspace variable "${rawWorkspace}" was not expanded by your IDE.`
47
+ );
48
+ console.error(
49
+ `[Server] This typically means your MCP client does not support dynamic variables.`
50
+ );
51
+ console.error(
52
+ `[Server] Please use an absolute path instead: --workspace /path/to/your/project`
53
+ );
45
54
  process.exit(1);
46
55
  } else if (rawWorkspace) {
47
56
  workspaceDir = rawWorkspace;
@@ -58,110 +67,167 @@ let cache = null;
58
67
  let indexer = null;
59
68
  let hybridSearch = null;
60
69
  let config = null;
70
+ let isInitialized = false;
71
+ let initializationPromise = null;
61
72
 
62
73
  // Feature registry - ordered by priority (semantic_search first as primary tool)
63
74
  const features = [
64
75
  {
65
76
  module: HybridSearchFeature,
66
77
  instance: null,
67
- handler: HybridSearchFeature.handleToolCall
78
+ handler: HybridSearchFeature.handleToolCall,
68
79
  },
69
80
  {
70
81
  module: IndexCodebaseFeature,
71
82
  instance: null,
72
- handler: IndexCodebaseFeature.handleToolCall
83
+ handler: IndexCodebaseFeature.handleToolCall,
73
84
  },
74
85
  {
75
86
  module: ClearCacheFeature,
76
87
  instance: null,
77
- handler: ClearCacheFeature.handleToolCall
88
+ handler: ClearCacheFeature.handleToolCall,
78
89
  },
79
90
  {
80
91
  module: CheckLastVersionFeature,
81
92
  instance: null,
82
- handler: CheckLastVersionFeature.handleToolCall
93
+ handler: CheckLastVersionFeature.handleToolCall,
83
94
  },
84
95
  {
85
96
  module: SetWorkspaceFeature,
86
97
  instance: null,
87
- handler: SetWorkspaceFeature.handleToolCall
98
+ handler: SetWorkspaceFeature.handleToolCall,
88
99
  },
89
100
  {
90
101
  module: GetStatusFeature,
91
102
  instance: null,
92
- handler: GetStatusFeature.handleToolCall
93
- }
103
+ handler: GetStatusFeature.handleToolCall,
104
+ },
94
105
  ];
95
106
 
96
- // Initialize application
107
+ /**
108
+ * Lazy initialization - only loads heavy resources when first needed
109
+ * This prevents IDE blocking on startup
110
+ */
111
+ async function ensureInitialized() {
112
+ // Already initialized
113
+ if (isInitialized) {
114
+ return;
115
+ }
116
+
117
+ // Initialization in progress, wait for it
118
+ if (initializationPromise) {
119
+ return initializationPromise;
120
+ }
121
+
122
+ // Start initialization
123
+ initializationPromise = (async () => {
124
+ console.error("[Server] Loading AI model and cache (first use)...");
125
+
126
+ // Load AI model using MRL embedder factory
127
+ embedder = await createEmbedder(config);
128
+ console.error(
129
+ `[Server] Model: ${embedder.modelName} (${embedder.dimension}d, device: ${embedder.device})`
130
+ );
131
+
132
+ // Initialize cache
133
+ cache = new SQLiteCache(config);
134
+ await cache.load();
135
+
136
+ // Initialize features
137
+ indexer = new CodebaseIndexer(embedder, cache, config, server);
138
+ hybridSearch = new HybridSearch(embedder, cache, config, indexer);
139
+ const cacheClearer = new ClearCacheFeature.CacheClearer(
140
+ embedder,
141
+ cache,
142
+ config,
143
+ indexer
144
+ );
145
+ const versionChecker = new CheckLastVersionFeature.VersionChecker(config);
146
+
147
+ // Store feature instances (matches features array order)
148
+ features[0].instance = hybridSearch;
149
+ features[1].instance = indexer;
150
+ features[2].instance = cacheClearer;
151
+ features[3].instance = versionChecker;
152
+
153
+ // Initialize new tools
154
+ const workspaceManager = new SetWorkspaceFeature.WorkspaceManager(
155
+ config,
156
+ cache,
157
+ indexer
158
+ );
159
+ const statusReporter = new GetStatusFeature.StatusReporter(
160
+ config,
161
+ cache,
162
+ indexer,
163
+ embedder
164
+ );
165
+ features[4].instance = workspaceManager;
166
+ features[5].instance = statusReporter;
167
+
168
+ isInitialized = true;
169
+ console.error("[Server] Model and cache loaded successfully");
170
+ })();
171
+
172
+ await initializationPromise;
173
+ }
174
+
175
+ // Initialize application (lightweight, non-blocking)
97
176
  async function initialize() {
98
177
  // Load configuration with workspace support
99
178
  config = await loadConfig(workspaceDir);
100
-
179
+
101
180
  // Ensure search directory exists
102
181
  try {
103
182
  await fs.access(config.searchDirectory);
104
183
  } catch {
105
- console.error(`[Server] Error: Search directory "${config.searchDirectory}" does not exist`);
184
+ console.error(
185
+ `[Server] Error: Search directory "${config.searchDirectory}" does not exist`
186
+ );
106
187
  process.exit(1);
107
188
  }
108
189
 
109
- // Load AI model using MRL embedder factory
110
- console.error("[Server] Loading AI embedding model (this may take time on first run)...");
111
- embedder = await createEmbedder(config);
112
- console.error(`[Server] Model: ${embedder.modelName} (${embedder.dimension}d, device: ${embedder.device})`);
113
-
114
- // Initialize cache
115
- cache = new EmbeddingsCache(config);
116
- await cache.load();
117
-
118
- // Initialize features
119
- indexer = new CodebaseIndexer(embedder, cache, config, server);
120
- hybridSearch = new HybridSearch(embedder, cache, config);
121
- const cacheClearer = new ClearCacheFeature.CacheClearer(embedder, cache, config, indexer);
122
- const versionChecker = new CheckLastVersionFeature.VersionChecker(config);
123
-
124
- // Store feature instances (matches features array order)
125
- features[0].instance = hybridSearch;
126
- features[1].instance = indexer;
127
- features[2].instance = cacheClearer;
128
- features[3].instance = versionChecker;
129
-
130
- // Initialize new tools
131
- const workspaceManager = new SetWorkspaceFeature.WorkspaceManager(config, cache, indexer);
132
- const statusReporter = new GetStatusFeature.StatusReporter(config, cache, indexer, embedder);
133
- features[4].instance = workspaceManager;
134
- features[5].instance = statusReporter;
135
-
136
- // Start indexing in background (non-blocking)
137
- console.error("[Server] Starting background indexing...");
138
- indexer.indexAll().then(() => {
139
- // Only start file watcher if explicitly enabled in config
140
- if (config.watchFiles) {
141
- indexer.setupFileWatcher();
142
- }
143
- }).catch(err => {
144
- console.error("[Server] Background indexing error:", err.message);
145
- });
190
+ console.error(
191
+ "[Server] Configuration loaded. Model will load on first use (lazy initialization)."
192
+ );
193
+
194
+ // Optional: auto-index after delay
195
+ if (config.autoIndexDelay && config.autoIndexDelay > 0) {
196
+ console.error(
197
+ `[Server] Auto-indexing will start after ${config.autoIndexDelay}ms delay...`
198
+ );
199
+ setTimeout(async () => {
200
+ console.error("[Server] Starting auto-indexing...");
201
+ try {
202
+ await ensureInitialized();
203
+ await indexer.indexAll();
204
+ if (config.watchFiles) {
205
+ indexer.setupFileWatcher();
206
+ }
207
+ } catch (err) {
208
+ console.error("[Server] Auto-indexing error:", err.message);
209
+ }
210
+ }, config.autoIndexDelay);
211
+ }
146
212
  }
147
213
 
148
214
  // Setup MCP server
149
215
  const server = new Server(
150
- {
151
- name: "smart-coding-mcp",
152
- version: packageJson.version
216
+ {
217
+ name: "smart-coding-mcp",
218
+ version: packageJson.version,
153
219
  },
154
- {
155
- capabilities: {
156
- tools: {}
157
- }
220
+ {
221
+ capabilities: {
222
+ tools: {},
223
+ },
158
224
  }
159
225
  );
160
226
 
161
227
  // Register tools from all features
162
228
  server.setRequestHandler(ListToolsRequestSchema, async () => {
163
229
  const tools = [];
164
-
230
+
165
231
  for (const feature of features) {
166
232
  const toolDef = feature.module.getToolDefinition(config);
167
233
  tools.push(toolDef);
@@ -172,6 +238,9 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
172
238
 
173
239
  // Handle tool calls
174
240
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
241
+ // Ensure model and cache are loaded before handling any tool
242
+ await ensureInitialized();
243
+
175
244
  for (const feature of features) {
176
245
  const toolDef = feature.module.getToolDefinition(config);
177
246
 
@@ -191,36 +260,36 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
191
260
  // Main entry point
192
261
  async function main() {
193
262
  await initialize();
194
-
263
+
195
264
  const transport = new StdioServerTransport();
196
265
  await server.connect(transport);
197
-
266
+
198
267
  console.error("[Server] Smart Coding MCP server ready!");
199
268
  }
200
269
 
201
270
  // Graceful shutdown
202
- process.on('SIGINT', async () => {
271
+ process.on("SIGINT", async () => {
203
272
  console.error("\n[Server] Shutting down gracefully...");
204
-
273
+
205
274
  // Stop file watcher
206
275
  if (indexer && indexer.watcher) {
207
276
  await indexer.watcher.close();
208
277
  console.error("[Server] File watcher stopped");
209
278
  }
210
-
279
+
211
280
  // Save cache
212
281
  if (cache) {
213
282
  await cache.save();
214
283
  console.error("[Server] Cache saved");
215
284
  }
216
-
285
+
217
286
  console.error("[Server] Goodbye!");
218
287
  process.exit(0);
219
288
  });
220
289
 
221
- process.on('SIGTERM', async () => {
290
+ process.on("SIGTERM", async () => {
222
291
  console.error("\n[Server] Received SIGTERM, shutting down...");
223
292
  process.exit(0);
224
293
  });
225
294
 
226
- main().catch(console.error);
295
+ main().catch(console.error);
package/lib/config.js CHANGED
@@ -63,12 +63,24 @@ const DEFAULT_CONFIG = {
63
63
  verbose: false,
64
64
  workerThreads: "auto", // "auto" = CPU cores - 1, or set a number
65
65
  embeddingModel: "nomic-ai/nomic-embed-text-v1.5",
66
- embeddingDimension: 256, // MRL dimension: 64, 128, 256, 512, 768
66
+ embeddingDimension: 128, // MRL dimension: 64, 128, 256, 512, 768 (changed from 256 to 128 for better performance)
67
67
  device: "auto", // "cpu", "webgpu", or "auto"
68
68
  chunkingMode: "smart", // "smart", "ast", or "line"
69
69
  semanticWeight: 0.7,
70
70
  exactMatchBoost: 1.5,
71
- smartIndexing: true
71
+ smartIndexing: true,
72
+
73
+ // Resource throttling (prevents CPU exhaustion)
74
+ maxCpuPercent: 25, // Max CPU usage during indexing (default: 25%)
75
+ batchDelay: 250, // Delay between batches in ms (default: 250)
76
+ maxWorkers: 'auto', // Max worker threads ('auto' = 25% of cores, or specific number)
77
+
78
+ // Startup behavior
79
+ autoIndexDelay: null, // Delay before auto-indexing (ms), null = disabled (lazy index on first search)
80
+
81
+ // Progressive indexing
82
+ incrementalSaveInterval: 5, // Save to cache every N batches
83
+ allowPartialSearch: true // Allow searches while indexing is in progress
72
84
  };
73
85
 
74
86
  let config = { ...DEFAULT_CONFIG };
@@ -276,6 +288,44 @@ export async function loadConfig(workspaceDir = null) {
276
288
  }
277
289
  }
278
290
 
291
+ // Resource throttling - Max CPU percent
292
+ if (process.env.SMART_CODING_MAX_CPU_PERCENT !== undefined) {
293
+ const value = parseInt(process.env.SMART_CODING_MAX_CPU_PERCENT, 10);
294
+ if (!isNaN(value) && value >= 10 && value <= 100) {
295
+ config.maxCpuPercent = value;
296
+ console.error(`[Config] Max CPU usage: ${value}%`);
297
+ } else {
298
+ console.error(`[Config] Invalid SMART_CODING_MAX_CPU_PERCENT: ${value}, using default (must be 10-100)`);
299
+ }
300
+ }
301
+
302
+ // Resource throttling - Batch delay
303
+ if (process.env.SMART_CODING_BATCH_DELAY !== undefined) {
304
+ const value = parseInt(process.env.SMART_CODING_BATCH_DELAY, 10);
305
+ if (!isNaN(value) && value >= 0 && value <= 5000) {
306
+ config.batchDelay = value;
307
+ console.error(`[Config] Batch delay: ${value}ms`);
308
+ } else {
309
+ console.error(`[Config] Invalid SMART_CODING_BATCH_DELAY: ${value}, using default (must be 0-5000)`);
310
+ }
311
+ }
312
+
313
+ // Resource throttling - Max workers
314
+ if (process.env.SMART_CODING_MAX_WORKERS !== undefined) {
315
+ const value = process.env.SMART_CODING_MAX_WORKERS.trim().toLowerCase();
316
+ if (value === 'auto') {
317
+ config.maxWorkers = 'auto';
318
+ } else {
319
+ const numValue = parseInt(value, 10);
320
+ if (!isNaN(numValue) && numValue >= 1 && numValue <= 32) {
321
+ config.maxWorkers = numValue;
322
+ console.error(`[Config] Max workers: ${numValue}`);
323
+ } else {
324
+ console.error(`[Config] Invalid SMART_CODING_MAX_WORKERS: ${value}, using default (must be 'auto' or 1-32)`);
325
+ }
326
+ }
327
+ }
328
+
279
329
  return config;
280
330
  }
281
331
 
@@ -0,0 +1,85 @@
1
+ import os from 'os';
2
+
3
+ /**
4
+ * Resource throttling utility to prevent CPU/memory exhaustion during indexing
5
+ * Ensures the MCP server doesn't freeze the user's laptop
6
+ */
7
+ export class ResourceThrottle {
8
+ constructor(config) {
9
+ // Max CPU usage as percentage (default 50%)
10
+ this.maxCpuPercent = config.maxCpuPercent || 50;
11
+
12
+ // Delay between batches in milliseconds
13
+ this.batchDelay = config.batchDelay || 100;
14
+
15
+ // Max worker threads (override auto-detection)
16
+ const cpuCount = os.cpus().length;
17
+ if (config.maxWorkers === 'auto' || config.maxWorkers === undefined) {
18
+ // Use 25% of cores by default for throttling (more conservative)
19
+ this.maxWorkers = Math.max(1, Math.floor(cpuCount * 0.25));
20
+ } else {
21
+ // Validate and parse the value
22
+ const parsed = typeof config.maxWorkers === 'number'
23
+ ? config.maxWorkers
24
+ : parseInt(config.maxWorkers, 10);
25
+
26
+ if (isNaN(parsed) || parsed < 1) {
27
+ console.error(`[Throttle] Invalid maxWorkers: ${config.maxWorkers}, using auto`);
28
+ this.maxWorkers = Math.max(1, Math.floor(cpuCount * 0.5));
29
+ } else {
30
+ this.maxWorkers = Math.max(1, Math.min(parsed, cpuCount));
31
+ }
32
+ }
33
+
34
+ console.error(`[Throttle] CPU limit: ${this.maxCpuPercent}%, Batch delay: ${this.batchDelay}ms, Max workers: ${this.maxWorkers}`);
35
+ }
36
+
37
+ /**
38
+ * Execute work with delay to throttle CPU usage
39
+ */
40
+ async throttledBatch(work, signal = null) {
41
+ // Execute the work
42
+ if (work) {
43
+ await work();
44
+ }
45
+
46
+ // Apply delay if not aborted
47
+ if (!signal?.aborted && this.batchDelay > 0) {
48
+ await this.sleep(this.batchDelay);
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Sleep utility
54
+ */
55
+ sleep(ms) {
56
+ return new Promise(resolve => setTimeout(resolve, ms));
57
+ }
58
+
59
+ /**
60
+ * Calculate optimal worker count based on CPU limit
61
+ */
62
+ getWorkerCount(requestedWorkers) {
63
+ if (requestedWorkers === 'auto') {
64
+ return this.maxWorkers;
65
+ }
66
+ return Math.min(requestedWorkers, this.maxWorkers);
67
+ }
68
+
69
+ /**
70
+ * Check if we should pause due to high CPU usage
71
+ * This is a simple implementation - could be enhanced with actual CPU monitoring
72
+ */
73
+ async checkCpuUsage() {
74
+ // Future enhancement: monitor actual CPU usage and pause if needed
75
+ // For now, we rely on worker limits and batch delays
76
+ return true;
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Sleep utility function
82
+ */
83
+ export function sleep(ms) {
84
+ return new Promise(resolve => setTimeout(resolve, ms));
85
+ }