semantic-code-mcp 2.0.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,183 @@
1
+ /**
2
+ * Set Workspace Feature
3
+ *
4
+ * MCP tool to change the project workspace path at runtime.
5
+ * Useful when agent detects it's in a different directory.
6
+ */
7
+
8
+ import fs from 'fs/promises';
9
+ import path from 'path';
10
+ import { loadConfig } from '../lib/config.js';
11
+
12
+ /**
13
+ * Get tool definition for MCP registration
14
+ */
15
+ export function getToolDefinition(config) {
16
+ return {
17
+ name: "e_set_workspace",
18
+ description: "Change the project workspace path at runtime. Use this when you detect the current workspace is incorrect or you need to switch to a different project directory. Creates cache folder automatically and optionally re-indexes the new workspace.",
19
+ inputSchema: {
20
+ type: "object",
21
+ properties: {
22
+ path: {
23
+ type: "string",
24
+ description: "Absolute path to the new workspace directory"
25
+ },
26
+ clearCache: {
27
+ type: "boolean",
28
+ description: "Whether to clear existing cache before switching (default: false)"
29
+ },
30
+ reindex: {
31
+ type: "boolean",
32
+ description: "Whether to trigger re-indexing after switching (default: true)"
33
+ }
34
+ },
35
+ required: ["path"]
36
+ }
37
+ };
38
+ }
39
+
40
+ /**
41
+ * Workspace Manager class
42
+ */
43
+ export class WorkspaceManager {
44
+ constructor(config, cache, indexer) {
45
+ this.config = config;
46
+ this.cache = cache;
47
+ this.indexer = indexer;
48
+ }
49
+
50
+ /**
51
+ * Set new workspace path
52
+ */
53
+ async setWorkspace(newPath, options = {}) {
54
+ const { clearCache = false, reindex = true } = options;
55
+
56
+ // Validate path
57
+ try {
58
+ const stats = await fs.stat(newPath);
59
+ if (!stats.isDirectory()) {
60
+ return {
61
+ success: false,
62
+ error: `Path is not a directory: ${newPath}`
63
+ };
64
+ }
65
+ } catch (err) {
66
+ return {
67
+ success: false,
68
+ error: `Path does not exist: ${newPath}`
69
+ };
70
+ }
71
+
72
+ const oldPath = this.config.searchDirectory;
73
+
74
+ // Reload config for new workspace (re-detects project types & rebuilds excludePatterns)
75
+ const newConfig = await loadConfig(newPath);
76
+
77
+ // Preserve runtime overrides (env vars, embedding settings, etc.)
78
+ const preserveKeys = [
79
+ 'embeddingProvider', 'embeddingModel', 'embeddingDimension',
80
+ 'geminiApiKey', 'geminiModel', 'geminiBaseURL', 'geminiDimensions',
81
+ 'geminiBatchSize', 'geminiBatchFlushMs', 'geminiMaxRetries',
82
+ 'embeddingApiKey', 'embeddingBaseURL',
83
+ 'vertexProject', 'vertexLocation',
84
+ 'vectorStoreProvider', 'milvusAddress', 'milvusToken',
85
+ 'milvusDatabase', 'milvusCollection',
86
+ 'workerThreads', 'maxCpuPercent', 'batchDelay', 'maxWorkers',
87
+ 'verbose'
88
+ ];
89
+ const runtimeOverrides = {};
90
+ for (const key of preserveKeys) {
91
+ if (this.config[key] !== undefined) {
92
+ runtimeOverrides[key] = this.config[key];
93
+ }
94
+ }
95
+
96
+ // Apply new config with runtime overrides
97
+ Object.assign(this.config, newConfig, runtimeOverrides);
98
+ this.config.searchDirectory = newPath;
99
+
100
+ // Update cache directory
101
+ const newCacheDir = path.join(newPath, '.smart-coding-cache');
102
+ this.config.cacheDirectory = newCacheDir;
103
+
104
+ // Ensure cache directory exists
105
+ try {
106
+ await fs.mkdir(newCacheDir, { recursive: true });
107
+ } catch (err) {
108
+ // Ignore if already exists
109
+ }
110
+
111
+ // Clear cache if requested
112
+ if (clearCache && this.cache) {
113
+ if (typeof this.cache.resetForFullReindex === "function") {
114
+ await this.cache.resetForFullReindex();
115
+ } else {
116
+ this.cache.setVectorStore([]);
117
+ this.cache.clearAllFileHashes();
118
+ }
119
+ console.error(`[Workspace] Cache cleared for new workspace`);
120
+ }
121
+
122
+ // Update cache path and reload
123
+ if (this.cache) {
124
+ this.cache.config = this.config;
125
+ await this.cache.load();
126
+ }
127
+
128
+ // Update indexer config
129
+ if (this.indexer) {
130
+ this.indexer.config = this.config;
131
+ }
132
+
133
+ console.error(`[Workspace] Changed from ${oldPath} to ${newPath}`);
134
+
135
+ // Trigger re-indexing if requested
136
+ let indexResult = null;
137
+ if (reindex && this.indexer) {
138
+ console.error(`[Workspace] Starting re-indexing...`);
139
+ try {
140
+ indexResult = await this.indexer.indexAll(clearCache);
141
+ } catch (err) {
142
+ console.error(`[Workspace] Re-indexing error: ${err.message}`);
143
+ }
144
+ }
145
+
146
+ return {
147
+ success: true,
148
+ oldPath,
149
+ newPath,
150
+ cacheDirectory: newCacheDir,
151
+ reindexed: reindex,
152
+ indexResult
153
+ };
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Handle MCP tool call
159
+ */
160
+ export async function handleToolCall(request, instance) {
161
+ const { path: newPath, clearCache, reindex } = request.params.arguments || {};
162
+
163
+ if (!newPath) {
164
+ return {
165
+ content: [{
166
+ type: "text",
167
+ text: JSON.stringify({
168
+ success: false,
169
+ error: "Missing required parameter: path"
170
+ }, null, 2)
171
+ }]
172
+ };
173
+ }
174
+
175
+ const result = await instance.setWorkspace(newPath, { clearCache, reindex });
176
+
177
+ return {
178
+ content: [{
179
+ type: "text",
180
+ text: JSON.stringify(result, null, 2)
181
+ }]
182
+ };
183
+ }
package/index.js ADDED
@@ -0,0 +1,297 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import {
5
+ CallToolRequestSchema,
6
+ ListToolsRequestSchema,
7
+ } from "@modelcontextprotocol/sdk/types.js";
8
+ import fs from "fs/promises";
9
+ import { createRequire } from "module";
10
+
11
+ // Import package.json for version
12
+ const require = createRequire(import.meta.url);
13
+ const packageJson = require("./package.json");
14
+
15
+ import { loadConfig } from "./lib/config.js";
16
+ import { createCache } from "./lib/cache-factory.js";
17
+ import { createEmbedder } from "./lib/mrl-embedder.js";
18
+ import { CodebaseIndexer } from "./features/index-codebase.js";
19
+ import { HybridSearch } from "./features/hybrid-search.js";
20
+
21
+ import * as IndexCodebaseFeature from "./features/index-codebase.js";
22
+ import * as HybridSearchFeature from "./features/hybrid-search.js";
23
+ import * as ClearCacheFeature from "./features/clear-cache.js";
24
+ import * as CheckLastVersionFeature from "./features/check-last-version.js";
25
+ import * as SetWorkspaceFeature from "./features/set-workspace.js";
26
+ import * as GetStatusFeature from "./features/get-status.js";
27
+
28
+ // Parse workspace from command line arguments
29
+ const args = process.argv.slice(2);
30
+ const workspaceIndex = args.findIndex((arg) => arg.startsWith("--workspace"));
31
+ let workspaceDir = process.cwd(); // Default to current directory
32
+
33
+ if (workspaceIndex !== -1) {
34
+ const arg = args[workspaceIndex];
35
+ let rawWorkspace = null;
36
+
37
+ if (arg.includes("=")) {
38
+ rawWorkspace = arg.split("=")[1];
39
+ } else if (workspaceIndex + 1 < args.length) {
40
+ rawWorkspace = args[workspaceIndex + 1];
41
+ }
42
+
43
+ // Check if IDE variable wasn't expanded (contains ${})
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
+ );
54
+ process.exit(1);
55
+ } else if (rawWorkspace) {
56
+ workspaceDir = rawWorkspace;
57
+ }
58
+
59
+ if (workspaceDir) {
60
+ console.error(`[Server] Workspace mode: ${workspaceDir}`);
61
+ }
62
+ }
63
+
64
+ // Global state
65
+ let embedder = null;
66
+ let cache = null;
67
+ let indexer = null;
68
+ let hybridSearch = null;
69
+ let config = null;
70
+ let isInitialized = false;
71
+ let initializationPromise = null;
72
+
73
+ // Feature registry - ordered by priority (semantic_search first as primary tool)
74
+ const features = [
75
+ {
76
+ module: HybridSearchFeature,
77
+ instance: null,
78
+ handler: HybridSearchFeature.handleToolCall,
79
+ },
80
+ {
81
+ module: IndexCodebaseFeature,
82
+ instance: null,
83
+ handler: IndexCodebaseFeature.handleToolCall,
84
+ },
85
+ {
86
+ module: ClearCacheFeature,
87
+ instance: null,
88
+ handler: ClearCacheFeature.handleToolCall,
89
+ },
90
+ {
91
+ module: CheckLastVersionFeature,
92
+ instance: null,
93
+ handler: CheckLastVersionFeature.handleToolCall,
94
+ },
95
+ {
96
+ module: SetWorkspaceFeature,
97
+ instance: null,
98
+ handler: SetWorkspaceFeature.handleToolCall,
99
+ },
100
+ {
101
+ module: GetStatusFeature,
102
+ instance: null,
103
+ handler: GetStatusFeature.handleToolCall,
104
+ },
105
+ ];
106
+
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 (sqlite or milvus)
133
+ cache = createCache(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)
176
+ async function initialize() {
177
+ // Load configuration with workspace support
178
+ config = await loadConfig(workspaceDir);
179
+
180
+ // Ensure search directory exists
181
+ try {
182
+ await fs.access(config.searchDirectory);
183
+ } catch {
184
+ console.error(
185
+ `[Server] Error: Search directory "${config.searchDirectory}" does not exist`
186
+ );
187
+ process.exit(1);
188
+ }
189
+
190
+ console.error(
191
+ "[Server] Configuration loaded. Model will load on first use (lazy initialization)."
192
+ );
193
+
194
+ // Progressive background indexing: starts after short delay, doesn't block
195
+ // Search works right away with partial results while indexing continues
196
+ if (config.autoIndexDelay !== false && config.autoIndexDelay > 0) {
197
+ console.error(
198
+ `[Server] Progressive indexing will start in ${config.autoIndexDelay}ms (search available immediately)...`
199
+ );
200
+ setTimeout(async () => {
201
+ try {
202
+ await ensureInitialized();
203
+ // Use background indexing - non-blocking!
204
+ // Search can return partial results while indexing continues
205
+ indexer.startBackgroundIndexing();
206
+ if (config.watchFiles) {
207
+ indexer.setupFileWatcher();
208
+ }
209
+ } catch (err) {
210
+ console.error("[Server] Background indexing error:", err.message);
211
+ }
212
+ }, config.autoIndexDelay);
213
+ }
214
+ }
215
+
216
+ // Setup MCP server
217
+ const server = new Server(
218
+ {
219
+ name: "smart-coding-mcp",
220
+ version: packageJson.version,
221
+ },
222
+ {
223
+ capabilities: {
224
+ tools: {},
225
+ },
226
+ }
227
+ );
228
+
229
+ // Register tools from all features
230
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
231
+ const tools = [];
232
+
233
+ for (const feature of features) {
234
+ const toolDef = feature.module.getToolDefinition(config);
235
+ tools.push(toolDef);
236
+ }
237
+
238
+ return { tools };
239
+ });
240
+
241
+ // Handle tool calls
242
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
243
+ // Ensure model and cache are loaded before handling any tool
244
+ await ensureInitialized();
245
+
246
+ for (const feature of features) {
247
+ const toolDef = feature.module.getToolDefinition(config);
248
+
249
+ if (request.params.name === toolDef.name) {
250
+ return await feature.handler(request, feature.instance);
251
+ }
252
+ }
253
+
254
+ return {
255
+ content: [{
256
+ type: "text",
257
+ text: `Unknown tool: ${request.params.name}`
258
+ }]
259
+ };
260
+ });
261
+
262
+ // Main entry point
263
+ async function main() {
264
+ await initialize();
265
+
266
+ const transport = new StdioServerTransport();
267
+ await server.connect(transport);
268
+
269
+ console.error("[Server] Smart Coding MCP server ready!");
270
+ }
271
+
272
+ // Graceful shutdown
273
+ process.on("SIGINT", async () => {
274
+ console.error("\n[Server] Shutting down gracefully...");
275
+
276
+ // Stop file watcher
277
+ if (indexer && indexer.watcher) {
278
+ await indexer.watcher.close();
279
+ console.error("[Server] File watcher stopped");
280
+ }
281
+
282
+ // Save cache
283
+ if (cache) {
284
+ await cache.save();
285
+ console.error("[Server] Cache saved");
286
+ }
287
+
288
+ console.error("[Server] Goodbye!");
289
+ process.exit(0);
290
+ });
291
+
292
+ process.on("SIGTERM", async () => {
293
+ console.error("\n[Server] Received SIGTERM, shutting down...");
294
+ process.exit(0);
295
+ });
296
+
297
+ main().catch(console.error);