smart-coding-mcp 1.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/ARCHITECTURE.md +287 -0
- package/CONTRIBUTING.md +308 -0
- package/LICENSE +21 -0
- package/README.md +305 -0
- package/config.json +18 -0
- package/features/clear-cache.js +45 -0
- package/features/hybrid-search.js +114 -0
- package/features/index-codebase.js +213 -0
- package/index.js +174 -0
- package/lib/cache.js +114 -0
- package/lib/config.js +146 -0
- package/lib/ignore-patterns.js +314 -0
- package/lib/project-detector.js +75 -0
- package/lib/utils.js +80 -0
- package/package.json +54 -0
- package/scripts/clear-cache.js +31 -0
package/index.js
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
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 { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
import { pipeline } from "@xenova/transformers";
|
|
6
|
+
import fs from "fs/promises";
|
|
7
|
+
|
|
8
|
+
import { loadConfig } from "./lib/config.js";
|
|
9
|
+
import { EmbeddingsCache } from "./lib/cache.js";
|
|
10
|
+
import { CodebaseIndexer } from "./features/index-codebase.js";
|
|
11
|
+
import { HybridSearch } from "./features/hybrid-search.js";
|
|
12
|
+
|
|
13
|
+
import * as IndexCodebaseFeature from "./features/index-codebase.js";
|
|
14
|
+
import * as HybridSearchFeature from "./features/hybrid-search.js";
|
|
15
|
+
import * as ClearCacheFeature from "./features/clear-cache.js";
|
|
16
|
+
|
|
17
|
+
// Parse workspace from command line arguments
|
|
18
|
+
const args = process.argv.slice(2);
|
|
19
|
+
const workspaceIndex = args.findIndex(arg => arg.startsWith('--workspace'));
|
|
20
|
+
let workspaceDir = null;
|
|
21
|
+
|
|
22
|
+
if (workspaceIndex !== -1) {
|
|
23
|
+
const arg = args[workspaceIndex];
|
|
24
|
+
if (arg.includes('=')) {
|
|
25
|
+
workspaceDir = arg.split('=')[1];
|
|
26
|
+
} else if (workspaceIndex + 1 < args.length) {
|
|
27
|
+
workspaceDir = args[workspaceIndex + 1];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (workspaceDir) {
|
|
31
|
+
console.error(`[Server] Workspace mode: ${workspaceDir}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Global state
|
|
36
|
+
let embedder = null;
|
|
37
|
+
let cache = null;
|
|
38
|
+
let indexer = null;
|
|
39
|
+
let hybridSearch = null;
|
|
40
|
+
let config = null;
|
|
41
|
+
|
|
42
|
+
// Feature registry
|
|
43
|
+
const features = [
|
|
44
|
+
{
|
|
45
|
+
module: IndexCodebaseFeature,
|
|
46
|
+
instance: null,
|
|
47
|
+
handler: IndexCodebaseFeature.handleToolCall
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
module: HybridSearchFeature,
|
|
51
|
+
instance: null,
|
|
52
|
+
handler: HybridSearchFeature.handleToolCall
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
module: ClearCacheFeature,
|
|
56
|
+
instance: null,
|
|
57
|
+
handler: ClearCacheFeature.handleToolCall
|
|
58
|
+
}
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
// Initialize application
|
|
62
|
+
async function initialize() {
|
|
63
|
+
// Load configuration with workspace support
|
|
64
|
+
config = await loadConfig(workspaceDir);
|
|
65
|
+
|
|
66
|
+
// Ensure search directory exists
|
|
67
|
+
try {
|
|
68
|
+
await fs.access(config.searchDirectory);
|
|
69
|
+
} catch {
|
|
70
|
+
console.error(`[Server] Error: Search directory "${config.searchDirectory}" does not exist`);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Load AI model
|
|
75
|
+
console.error("[Server] Loading AI embedding model (this may take time on first run)...");
|
|
76
|
+
embedder = await pipeline("feature-extraction", config.embeddingModel);
|
|
77
|
+
|
|
78
|
+
// Initialize cache
|
|
79
|
+
cache = new EmbeddingsCache(config);
|
|
80
|
+
await cache.load();
|
|
81
|
+
|
|
82
|
+
// Initialize features
|
|
83
|
+
indexer = new CodebaseIndexer(embedder, cache, config);
|
|
84
|
+
hybridSearch = new HybridSearch(embedder, cache, config);
|
|
85
|
+
const cacheClearer = new ClearCacheFeature.CacheClearer(embedder, cache, config);
|
|
86
|
+
|
|
87
|
+
// Store feature instances
|
|
88
|
+
features[0].instance = indexer;
|
|
89
|
+
features[1].instance = hybridSearch;
|
|
90
|
+
features[2].instance = cacheClearer;
|
|
91
|
+
|
|
92
|
+
// Index codebase
|
|
93
|
+
await indexer.initialize();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Setup MCP server
|
|
97
|
+
const server = new Server(
|
|
98
|
+
{
|
|
99
|
+
name: "smart-coding-mcp",
|
|
100
|
+
version: "1.0.0"
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
capabilities: {
|
|
104
|
+
tools: {}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
// Register tools from all features
|
|
110
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
111
|
+
const tools = [];
|
|
112
|
+
|
|
113
|
+
for (const feature of features) {
|
|
114
|
+
const toolDef = feature.module.getToolDefinition(config);
|
|
115
|
+
tools.push(toolDef);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return { tools };
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Handle tool calls
|
|
122
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
123
|
+
for (const feature of features) {
|
|
124
|
+
const toolDef = feature.module.getToolDefinition(config);
|
|
125
|
+
|
|
126
|
+
if (request.params.name === toolDef.name) {
|
|
127
|
+
return await feature.handler(request, feature.instance);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
content: [{
|
|
133
|
+
type: "text",
|
|
134
|
+
text: `Unknown tool: ${request.params.name}`
|
|
135
|
+
}]
|
|
136
|
+
};
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Main entry point
|
|
140
|
+
async function main() {
|
|
141
|
+
await initialize();
|
|
142
|
+
|
|
143
|
+
const transport = new StdioServerTransport();
|
|
144
|
+
await server.connect(transport);
|
|
145
|
+
|
|
146
|
+
console.error("[Server] Smart Coding MCP server ready!");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Graceful shutdown
|
|
150
|
+
process.on('SIGINT', async () => {
|
|
151
|
+
console.error("\n[Server] Shutting down gracefully...");
|
|
152
|
+
|
|
153
|
+
// Stop file watcher
|
|
154
|
+
if (indexer && indexer.watcher) {
|
|
155
|
+
await indexer.watcher.close();
|
|
156
|
+
console.error("[Server] File watcher stopped");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Save cache
|
|
160
|
+
if (cache) {
|
|
161
|
+
await cache.save();
|
|
162
|
+
console.error("[Server] Cache saved");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
console.error("[Server] Goodbye!");
|
|
166
|
+
process.exit(0);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
process.on('SIGTERM', async () => {
|
|
170
|
+
console.error("\n[Server] Received SIGTERM, shutting down...");
|
|
171
|
+
process.exit(0);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
main().catch(console.error);
|
package/lib/cache.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
export class EmbeddingsCache {
|
|
5
|
+
constructor(config) {
|
|
6
|
+
this.config = config;
|
|
7
|
+
this.vectorStore = [];
|
|
8
|
+
this.fileHashes = new Map();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async load() {
|
|
12
|
+
if (!this.config.enableCache) return;
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
await fs.mkdir(this.config.cacheDirectory, { recursive: true });
|
|
16
|
+
const cacheFile = path.join(this.config.cacheDirectory, "embeddings.json");
|
|
17
|
+
const hashFile = path.join(this.config.cacheDirectory, "file-hashes.json");
|
|
18
|
+
|
|
19
|
+
const [cacheData, hashData] = await Promise.all([
|
|
20
|
+
fs.readFile(cacheFile, "utf-8").catch(() => null),
|
|
21
|
+
fs.readFile(hashFile, "utf-8").catch(() => null)
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
if (cacheData && hashData) {
|
|
25
|
+
const rawVectorStore = JSON.parse(cacheData);
|
|
26
|
+
const rawHashes = new Map(Object.entries(JSON.parse(hashData)));
|
|
27
|
+
|
|
28
|
+
// Filter cache to only include files matching current extensions
|
|
29
|
+
const allowedExtensions = this.config.fileExtensions.map(ext => `.${ext}`);
|
|
30
|
+
|
|
31
|
+
this.vectorStore = rawVectorStore.filter(chunk => {
|
|
32
|
+
const ext = path.extname(chunk.file);
|
|
33
|
+
return allowedExtensions.includes(ext);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Only keep hashes for files matching current extensions
|
|
37
|
+
for (const [file, hash] of rawHashes) {
|
|
38
|
+
const ext = path.extname(file);
|
|
39
|
+
if (allowedExtensions.includes(ext)) {
|
|
40
|
+
this.fileHashes.set(file, hash);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const filtered = rawVectorStore.length - this.vectorStore.length;
|
|
45
|
+
if (filtered > 0) {
|
|
46
|
+
console.error(`[Cache] Filtered ${filtered} outdated cache entries`);
|
|
47
|
+
}
|
|
48
|
+
console.error(`[Cache] Loaded ${this.vectorStore.length} cached embeddings`);
|
|
49
|
+
}
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error("[Cache] Failed to load cache:", error.message);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async save() {
|
|
56
|
+
if (!this.config.enableCache) return;
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
await fs.mkdir(this.config.cacheDirectory, { recursive: true });
|
|
60
|
+
const cacheFile = path.join(this.config.cacheDirectory, "embeddings.json");
|
|
61
|
+
const hashFile = path.join(this.config.cacheDirectory, "file-hashes.json");
|
|
62
|
+
|
|
63
|
+
await Promise.all([
|
|
64
|
+
fs.writeFile(cacheFile, JSON.stringify(this.vectorStore, null, 2)),
|
|
65
|
+
fs.writeFile(hashFile, JSON.stringify(Object.fromEntries(this.fileHashes), null, 2))
|
|
66
|
+
]);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.error("[Cache] Failed to save cache:", error.message);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
getVectorStore() {
|
|
73
|
+
return this.vectorStore;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
setVectorStore(store) {
|
|
77
|
+
this.vectorStore = store;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
getFileHash(file) {
|
|
81
|
+
return this.fileHashes.get(file);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
setFileHash(file, hash) {
|
|
85
|
+
this.fileHashes.set(file, hash);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
deleteFileHash(file) {
|
|
89
|
+
this.fileHashes.delete(file);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
removeFileFromStore(file) {
|
|
93
|
+
this.vectorStore = this.vectorStore.filter(chunk => chunk.file !== file);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
addToStore(chunk) {
|
|
98
|
+
this.vectorStore.push(chunk);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async clear() {
|
|
102
|
+
if (!this.config.enableCache) return;
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
await fs.rm(this.config.cacheDirectory, { recursive: true, force: true });
|
|
106
|
+
this.vectorStore = [];
|
|
107
|
+
this.fileHashes = new Map();
|
|
108
|
+
console.error(`[Cache] Cache cleared successfully: ${this.config.cacheDirectory}`);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.error("[Cache] Failed to clear cache:", error.message);
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { ProjectDetector } from "./project-detector.js";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_CONFIG = {
|
|
6
|
+
searchDirectory: ".",
|
|
7
|
+
fileExtensions: [
|
|
8
|
+
// JavaScript/TypeScript
|
|
9
|
+
"js", "ts", "jsx", "tsx", "mjs", "cjs",
|
|
10
|
+
// Styles
|
|
11
|
+
"css", "scss", "sass", "less", "styl",
|
|
12
|
+
// Markup
|
|
13
|
+
"html", "htm", "xml", "svg",
|
|
14
|
+
// Python
|
|
15
|
+
"py", "pyw", "pyx",
|
|
16
|
+
// Java/Kotlin/Scala
|
|
17
|
+
"java", "kt", "kts", "scala",
|
|
18
|
+
// C/C++
|
|
19
|
+
"c", "cpp", "cc", "cxx", "h", "hpp", "hxx",
|
|
20
|
+
// C#
|
|
21
|
+
"cs", "csx",
|
|
22
|
+
// Go
|
|
23
|
+
"go",
|
|
24
|
+
// Rust
|
|
25
|
+
"rs",
|
|
26
|
+
// Ruby
|
|
27
|
+
"rb", "rake",
|
|
28
|
+
// PHP
|
|
29
|
+
"php", "phtml",
|
|
30
|
+
// Swift
|
|
31
|
+
"swift",
|
|
32
|
+
// Shell scripts
|
|
33
|
+
"sh", "bash", "zsh", "fish",
|
|
34
|
+
// Config & Data
|
|
35
|
+
"json", "yaml", "yml", "toml", "ini", "env",
|
|
36
|
+
// Documentation
|
|
37
|
+
"md", "mdx", "txt", "rst",
|
|
38
|
+
// Database
|
|
39
|
+
"sql",
|
|
40
|
+
// Other
|
|
41
|
+
"r", "R", "lua", "vim", "pl", "pm"
|
|
42
|
+
],
|
|
43
|
+
excludePatterns: [
|
|
44
|
+
"**/node_modules/**",
|
|
45
|
+
"**/dist/**",
|
|
46
|
+
"**/build/**",
|
|
47
|
+
"**/.git/**",
|
|
48
|
+
"**/coverage/**",
|
|
49
|
+
"**/.next/**",
|
|
50
|
+
"**/target/**",
|
|
51
|
+
"**/vendor/**"
|
|
52
|
+
],
|
|
53
|
+
chunkSize: 15,
|
|
54
|
+
chunkOverlap: 3,
|
|
55
|
+
batchSize: 100,
|
|
56
|
+
maxFileSize: 1048576, // 1MB - skip files larger than this
|
|
57
|
+
maxResults: 5,
|
|
58
|
+
enableCache: true,
|
|
59
|
+
cacheDirectory: "./.smart-coding-cache",
|
|
60
|
+
watchFiles: true,
|
|
61
|
+
verbose: false,
|
|
62
|
+
embeddingModel: "Xenova/all-MiniLM-L6-v2",
|
|
63
|
+
semanticWeight: 0.7,
|
|
64
|
+
exactMatchBoost: 1.5,
|
|
65
|
+
smartIndexing: true
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
let config = { ...DEFAULT_CONFIG };
|
|
69
|
+
|
|
70
|
+
export async function loadConfig(workspaceDir = null) {
|
|
71
|
+
try {
|
|
72
|
+
// Determine the base directory for configuration
|
|
73
|
+
let baseDir;
|
|
74
|
+
let configPath;
|
|
75
|
+
|
|
76
|
+
if (workspaceDir) {
|
|
77
|
+
// Workspace mode: load config from workspace root
|
|
78
|
+
baseDir = path.resolve(workspaceDir);
|
|
79
|
+
configPath = path.join(baseDir, "config.json");
|
|
80
|
+
console.error(`[Config] Workspace mode: ${baseDir}`);
|
|
81
|
+
} else {
|
|
82
|
+
// Server mode: load config from server directory
|
|
83
|
+
const scriptDir = path.dirname(new URL(import.meta.url).pathname);
|
|
84
|
+
baseDir = path.resolve(scriptDir, '..');
|
|
85
|
+
configPath = path.join(baseDir, "config.json");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let userConfig = {};
|
|
89
|
+
try {
|
|
90
|
+
const configData = await fs.readFile(configPath, "utf-8");
|
|
91
|
+
userConfig = JSON.parse(configData);
|
|
92
|
+
} catch (configError) {
|
|
93
|
+
if (workspaceDir) {
|
|
94
|
+
console.error(`[Config] No config.json in workspace, using defaults`);
|
|
95
|
+
} else {
|
|
96
|
+
console.error(`[Config] No config.json found: ${configError.message}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
config = { ...DEFAULT_CONFIG, ...userConfig };
|
|
101
|
+
|
|
102
|
+
// Set workspace-specific directories
|
|
103
|
+
if (workspaceDir) {
|
|
104
|
+
config.searchDirectory = baseDir;
|
|
105
|
+
config.cacheDirectory = path.join(baseDir, ".smart-coding-cache");
|
|
106
|
+
} else {
|
|
107
|
+
config.searchDirectory = path.resolve(baseDir, config.searchDirectory);
|
|
108
|
+
config.cacheDirectory = path.resolve(baseDir, config.cacheDirectory);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Smart project detection
|
|
112
|
+
if (config.smartIndexing !== false) {
|
|
113
|
+
const detector = new ProjectDetector(config.searchDirectory);
|
|
114
|
+
const detectedTypes = await detector.detectProjectTypes();
|
|
115
|
+
|
|
116
|
+
if (detectedTypes.length > 0) {
|
|
117
|
+
const smartPatterns = detector.getSmartIgnorePatterns();
|
|
118
|
+
|
|
119
|
+
// Merge smart patterns with user patterns (user patterns take precedence)
|
|
120
|
+
const userPatterns = userConfig.excludePatterns || [];
|
|
121
|
+
config.excludePatterns = [
|
|
122
|
+
...smartPatterns,
|
|
123
|
+
...userPatterns
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
console.error(`[Config] Smart indexing: ${detectedTypes.join(', ')}`);
|
|
127
|
+
console.error(`[Config] Applied ${smartPatterns.length} smart ignore patterns`);
|
|
128
|
+
} else {
|
|
129
|
+
console.error("[Config] No project markers detected, using default patterns");
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
console.error("[Config] Loaded configuration from config.json");
|
|
134
|
+
} catch (error) {
|
|
135
|
+
console.error("[Config] Using default configuration (config.json not found or invalid)");
|
|
136
|
+
console.error(`[Config] Error: ${error.message}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return config;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function getConfig() {
|
|
143
|
+
return config;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export { DEFAULT_CONFIG };
|