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.
- package/LICENSE +22 -0
- package/README.md +259 -0
- package/config.json +85 -0
- package/features/check-last-version.js +504 -0
- package/features/clear-cache.js +75 -0
- package/features/get-status.js +210 -0
- package/features/hybrid-search.js +189 -0
- package/features/index-codebase.js +999 -0
- package/features/set-workspace.js +183 -0
- package/index.js +297 -0
- package/lib/ast-chunker.js +273 -0
- package/lib/cache-factory.js +13 -0
- package/lib/cache.js +157 -0
- package/lib/config.js +1296 -0
- package/lib/embedding-worker.js +155 -0
- package/lib/gemini-embedder.js +351 -0
- package/lib/ignore-patterns.js +896 -0
- package/lib/milvus-cache.js +478 -0
- package/lib/mrl-embedder.js +235 -0
- package/lib/project-detector.js +75 -0
- package/lib/resource-throttle.js +85 -0
- package/lib/sqlite-cache.js +468 -0
- package/lib/tokenizer.js +149 -0
- package/lib/utils.js +214 -0
- package/package.json +70 -0
- package/reindex.js +109 -0
|
@@ -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);
|