codebasesearch 0.1.23 → 0.1.24
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/mcp.js +96 -139
- package/package.json +1 -1
- package/src/search-worker.js +89 -6
package/mcp.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// MUST patch sharp before any other imports
|
|
4
3
|
import fs from 'fs';
|
|
5
4
|
import path from 'path';
|
|
6
5
|
import { fileURLToPath } from 'url';
|
|
@@ -23,178 +22,136 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
|
23
22
|
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
24
23
|
import { cwd } from 'process';
|
|
25
24
|
import { join } from 'path';
|
|
26
|
-
import { existsSync, readFileSync, appendFileSync, writeFileSync } from 'fs';
|
|
25
|
+
import { existsSync, readFileSync, appendFileSync, writeFileSync, readdirSync } from 'fs';
|
|
26
|
+
import { homedir } from 'os';
|
|
27
27
|
import { supervisor } from './src/supervisor.js';
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
const WORKSPACE_PATH = join(homedir(), 'workspace');
|
|
30
|
+
|
|
31
|
+
function getWorkspaceFolders() {
|
|
32
|
+
try {
|
|
33
|
+
return readdirSync(WORKSPACE_PATH, { withFileTypes: true })
|
|
34
|
+
.filter(e => e.isDirectory() && !e.name.startsWith('.'))
|
|
35
|
+
.map(e => join(WORKSPACE_PATH, e.name));
|
|
36
|
+
} catch { return []; }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function ensureIgnoreEntry(rootPath) {
|
|
30
40
|
const gitignorePath = join(rootPath, '.gitignore');
|
|
31
41
|
const entry = '.code-search/';
|
|
32
|
-
|
|
33
42
|
try {
|
|
34
43
|
if (existsSync(gitignorePath)) {
|
|
35
44
|
const content = readFileSync(gitignorePath, 'utf8');
|
|
36
|
-
if (!content.includes(entry)) {
|
|
37
|
-
appendFileSync(gitignorePath, `\n${entry}`);
|
|
38
|
-
}
|
|
45
|
+
if (!content.includes(entry)) appendFileSync(gitignorePath, `\n${entry}`);
|
|
39
46
|
} else {
|
|
40
47
|
writeFileSync(gitignorePath, `${entry}\n`);
|
|
41
48
|
}
|
|
42
|
-
} catch (e) {
|
|
43
|
-
// Ignore write errors
|
|
44
|
-
}
|
|
49
|
+
} catch (e) {}
|
|
45
50
|
}
|
|
46
51
|
|
|
47
|
-
|
|
48
|
-
{
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
52
|
+
function formatResults(result, query, scope) {
|
|
53
|
+
if (result.resultsCount === 0) return `No results found${scope ? ` across ${scope}` : ''} for: "${query}"`;
|
|
54
|
+
const plural = result.resultsCount !== 1 ? 's' : '';
|
|
55
|
+
const header = `Found ${result.resultsCount} result${plural}${scope ? ` across ${scope}` : ''} for: "${query}"\n\n`;
|
|
56
|
+
const body = result.results.map((r) => {
|
|
57
|
+
const pathPart = r.relativePath || r.absolutePath;
|
|
58
|
+
const lineCount = r.totalLines ? ` [${r.totalLines}L]` : '';
|
|
59
|
+
const ctx = r.enclosingContext ? ` (in: ${r.enclosingContext})` : '';
|
|
60
|
+
const rHeader = `${r.rank}. ${pathPart}${lineCount}:${r.lines}${ctx} (score: ${r.score}%)`;
|
|
61
|
+
const rBody = r.snippet.split('\n').map((line) => ` ${line}`).join('\n');
|
|
62
|
+
return `${rHeader}\n${rBody}`;
|
|
63
|
+
}).join('\n\n');
|
|
64
|
+
return header + body;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function errResponse(msg) {
|
|
68
|
+
return { content: [{ type: 'text', text: msg }], isError: true };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function okResponse(text) {
|
|
72
|
+
return { content: [{ type: 'text', text }] };
|
|
73
|
+
}
|
|
58
74
|
|
|
59
|
-
server
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
description:
|
|
72
|
-
'Absolute or relative path to the repository to search in (defaults to current directory)',
|
|
73
|
-
},
|
|
74
|
-
query: {
|
|
75
|
-
type: 'string',
|
|
76
|
-
description:
|
|
77
|
-
'Natural language search query (e.g., "authentication middleware", "database connection")',
|
|
78
|
-
},
|
|
79
|
-
},
|
|
80
|
-
required: ['query'],
|
|
75
|
+
const server = new Server({ name: 'code-search-mcp', version: '0.1.0' }, { capabilities: { tools: {} } });
|
|
76
|
+
|
|
77
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
78
|
+
tools: [
|
|
79
|
+
{
|
|
80
|
+
name: 'search',
|
|
81
|
+
description: 'Search through a code repository. Automatically indexes before searching.',
|
|
82
|
+
inputSchema: {
|
|
83
|
+
type: 'object',
|
|
84
|
+
properties: {
|
|
85
|
+
repository_path: { type: 'string', description: 'Path to repository (defaults to current directory)' },
|
|
86
|
+
query: { type: 'string', description: 'Natural language search query' },
|
|
81
87
|
},
|
|
88
|
+
required: ['query'],
|
|
82
89
|
},
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: 'search_workspace',
|
|
93
|
+
description: 'Search across ALL repositories in ~/workspace simultaneously. Returns ranked results with repo name prefix.',
|
|
94
|
+
inputSchema: {
|
|
95
|
+
type: 'object',
|
|
96
|
+
properties: {
|
|
97
|
+
query: { type: 'string', description: 'Natural language search query' },
|
|
98
|
+
limit: { type: 'number', description: 'Max results to return (default: 10)' },
|
|
99
|
+
},
|
|
100
|
+
required: ['query'],
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
}));
|
|
86
105
|
|
|
87
106
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
88
107
|
const { name, arguments: args } = request.params;
|
|
89
|
-
|
|
90
|
-
if (name !== 'search') {
|
|
91
|
-
return {
|
|
92
|
-
content: [
|
|
93
|
-
{
|
|
94
|
-
type: 'text',
|
|
95
|
-
text: `Unknown tool: ${name}`,
|
|
96
|
-
},
|
|
97
|
-
],
|
|
98
|
-
isError: true,
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
|
|
102
108
|
const query = args?.query;
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (!query || typeof query !== 'string')
|
|
106
|
-
return {
|
|
107
|
-
content: [
|
|
108
|
-
{
|
|
109
|
-
type: 'text',
|
|
110
|
-
text: 'Error: query is required and must be a string',
|
|
111
|
-
},
|
|
112
|
-
],
|
|
113
|
-
isError: true,
|
|
114
|
-
};
|
|
115
|
-
}
|
|
109
|
+
|
|
110
|
+
if (!['search', 'search_workspace'].includes(name)) return errResponse(`Unknown tool: ${name}`);
|
|
111
|
+
if (!query || typeof query !== 'string') return errResponse('Error: query is required and must be a string');
|
|
116
112
|
|
|
117
113
|
try {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
return
|
|
127
|
-
content: [
|
|
128
|
-
{
|
|
129
|
-
type: 'text',
|
|
130
|
-
text: `Error: ${result.error}`,
|
|
131
|
-
},
|
|
132
|
-
],
|
|
133
|
-
isError: true,
|
|
134
|
-
};
|
|
114
|
+
if (name === 'search_workspace') {
|
|
115
|
+
const result = await supervisor.sendRequest({
|
|
116
|
+
type: 'search-all',
|
|
117
|
+
query,
|
|
118
|
+
workspacePaths: getWorkspaceFolders(),
|
|
119
|
+
limit: args?.limit || 10,
|
|
120
|
+
});
|
|
121
|
+
if (result.error) return errResponse(`Error: ${result.error}`);
|
|
122
|
+
return okResponse(formatResults(result, query, 'workspace'));
|
|
135
123
|
}
|
|
136
124
|
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
.map((r) => {
|
|
143
|
-
const pathPart = r.relativePath || r.absolutePath;
|
|
144
|
-
const lineCount = r.totalLines ? ` [${r.totalLines}L]` : '';
|
|
145
|
-
const ctx = r.enclosingContext ? ` (in: ${r.enclosingContext})` : '';
|
|
146
|
-
const header = `${r.rank}. ${pathPart}${lineCount}:${r.lines}${ctx} (score: ${r.score}%)`;
|
|
147
|
-
const body = r.snippet.split('\n').map((line) => ` ${line}`).join('\n');
|
|
148
|
-
return `${header}\n${body}`;
|
|
149
|
-
})
|
|
150
|
-
.join('\n\n')}`;
|
|
151
|
-
|
|
152
|
-
return {
|
|
153
|
-
content: [
|
|
154
|
-
{
|
|
155
|
-
type: 'text',
|
|
156
|
-
text,
|
|
157
|
-
},
|
|
158
|
-
],
|
|
159
|
-
};
|
|
125
|
+
const repositoryPath = args?.repository_path || cwd();
|
|
126
|
+
ensureIgnoreEntry(repositoryPath);
|
|
127
|
+
const result = await supervisor.sendRequest({ type: 'search', query, repositoryPath });
|
|
128
|
+
if (result.error) return errResponse(`Error: ${result.error}`);
|
|
129
|
+
return okResponse(formatResults(result, query, null));
|
|
160
130
|
} catch (error) {
|
|
161
|
-
return {
|
|
162
|
-
content: [
|
|
163
|
-
{
|
|
164
|
-
type: 'text',
|
|
165
|
-
text: `Error: ${error.message}`,
|
|
166
|
-
},
|
|
167
|
-
],
|
|
168
|
-
isError: true,
|
|
169
|
-
};
|
|
131
|
+
return errResponse(`Error: ${error.message}`);
|
|
170
132
|
}
|
|
171
133
|
});
|
|
172
134
|
|
|
173
135
|
export async function startMcpServer() {
|
|
174
136
|
const transport = new StdioServerTransport();
|
|
175
137
|
await server.connect(transport);
|
|
138
|
+
|
|
139
|
+
const workspacePaths = getWorkspaceFolders();
|
|
140
|
+
if (workspacePaths.length > 0) {
|
|
141
|
+
supervisor.sendRequest({ type: 'index-all', workspacePaths })
|
|
142
|
+
.then(r => console.error(`[MCP] Pre-indexed workspace: ${r.message || JSON.stringify(r)}`))
|
|
143
|
+
.catch(e => console.error(`[MCP] Pre-index warning: ${e.message}`));
|
|
144
|
+
}
|
|
176
145
|
}
|
|
177
146
|
|
|
178
147
|
const isMain = process.argv[1] && (
|
|
179
|
-
process.argv[1] === fileURLToPath(import.meta.url) ||
|
|
180
|
-
process.argv[1].endsWith('mcp.js') ||
|
|
148
|
+
process.argv[1] === fileURLToPath(import.meta.url) ||
|
|
149
|
+
process.argv[1].endsWith('mcp.js') ||
|
|
181
150
|
process.argv[1].endsWith('code-search-mcp')
|
|
182
151
|
);
|
|
183
152
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
console.error('Server error:', error);
|
|
187
|
-
});
|
|
188
|
-
}
|
|
153
|
+
process.on('uncaughtException', (error) => console.error('Uncaught exception:', error));
|
|
154
|
+
process.on('unhandledRejection', (reason) => console.error('Unhandled rejection:', reason));
|
|
189
155
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
process.on('unhandledRejection', (reason) => {
|
|
195
|
-
console.error('Unhandled rejection:', reason);
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
async function main() {
|
|
199
|
-
await startMcpServer();
|
|
200
|
-
}
|
|
156
|
+
async function main() { await startMcpServer(); }
|
|
157
|
+
if (isMain) main().catch((error) => console.error('Server error:', error));
|
package/package.json
CHANGED
package/src/search-worker.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { parentPort } from 'worker_threads';
|
|
2
2
|
import { resolve, relative } from 'path';
|
|
3
|
-
import { existsSync, readFileSync } from 'fs';
|
|
3
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
|
|
4
4
|
import { loadIgnorePatterns } from './ignore-parser.js';
|
|
5
5
|
import { scanRepository } from './scanner.js';
|
|
6
6
|
import { buildTextIndex, searchText } from './text-search.js';
|
|
@@ -31,12 +31,28 @@ function getFileTotalLines(absoluteFilePath) {
|
|
|
31
31
|
|
|
32
32
|
let indexCache = new Map();
|
|
33
33
|
|
|
34
|
+
function getWorkspaceFolders(workspacePath) {
|
|
35
|
+
try {
|
|
36
|
+
const entries = readdirSync(workspacePath, { withFileTypes: true });
|
|
37
|
+
return entries
|
|
38
|
+
.filter(e => e.isDirectory() && !e.name.startsWith('.'))
|
|
39
|
+
.map(e => resolve(workspacePath, e.name));
|
|
40
|
+
} catch {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
34
45
|
async function initializeIndex(repositoryPath) {
|
|
35
46
|
const absolutePath = resolve(repositoryPath);
|
|
36
|
-
const cacheKey = absolutePath;
|
|
37
47
|
|
|
38
|
-
|
|
39
|
-
|
|
48
|
+
const cached = indexCache.get(absolutePath);
|
|
49
|
+
if (cached) {
|
|
50
|
+
try {
|
|
51
|
+
const dirStat = statSync(absolutePath);
|
|
52
|
+
if (dirStat.mtimeMs <= cached.indexedAt) return cached;
|
|
53
|
+
} catch {
|
|
54
|
+
return cached;
|
|
55
|
+
}
|
|
40
56
|
}
|
|
41
57
|
|
|
42
58
|
try {
|
|
@@ -48,8 +64,8 @@ async function initializeIndex(repositoryPath) {
|
|
|
48
64
|
}
|
|
49
65
|
|
|
50
66
|
const indexData = buildTextIndex(chunks);
|
|
51
|
-
const result = { chunks, indexData };
|
|
52
|
-
indexCache.set(
|
|
67
|
+
const result = { chunks, indexData, indexedAt: Date.now() };
|
|
68
|
+
indexCache.set(absolutePath, result);
|
|
53
69
|
|
|
54
70
|
return result;
|
|
55
71
|
} catch (error) {
|
|
@@ -99,6 +115,53 @@ async function performSearch(repositoryPath, query) {
|
|
|
99
115
|
}
|
|
100
116
|
}
|
|
101
117
|
|
|
118
|
+
async function performSearchAll(workspacePaths, query, limit = 10) {
|
|
119
|
+
const allResults = [];
|
|
120
|
+
|
|
121
|
+
for (const repoPath of workspacePaths) {
|
|
122
|
+
const absolutePath = resolve(repoPath);
|
|
123
|
+
if (!existsSync(absolutePath)) continue;
|
|
124
|
+
|
|
125
|
+
const indexData = await initializeIndex(absolutePath);
|
|
126
|
+
if (indexData.error || !indexData.chunks) continue;
|
|
127
|
+
|
|
128
|
+
const results = searchText(query, indexData.chunks, indexData.indexData);
|
|
129
|
+
const repoName = absolutePath.split('/').pop();
|
|
130
|
+
|
|
131
|
+
const seenFiles = new Set();
|
|
132
|
+
for (const r of results) {
|
|
133
|
+
if (!seenFiles.has(r.file_path)) {
|
|
134
|
+
seenFiles.add(r.file_path);
|
|
135
|
+
allResults.push({ ...r, repoName, repoPath: absolutePath });
|
|
136
|
+
}
|
|
137
|
+
if (seenFiles.size >= limit) break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
allResults.sort((a, b) => b.score - a.score);
|
|
142
|
+
const top = allResults.slice(0, limit);
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
query,
|
|
146
|
+
resultsCount: top.length,
|
|
147
|
+
results: top.map((r, idx) => {
|
|
148
|
+
const absoluteFilePath = resolve(r.repoPath, r.file_path);
|
|
149
|
+
const totalLines = getFileTotalLines(absoluteFilePath);
|
|
150
|
+
const enclosingContext = findEnclosingContext(r.content, r.line_start);
|
|
151
|
+
return {
|
|
152
|
+
rank: idx + 1,
|
|
153
|
+
absolutePath: absoluteFilePath,
|
|
154
|
+
relativePath: `${r.repoName}/${r.file_path}`,
|
|
155
|
+
lines: `${r.line_start}-${r.line_end}`,
|
|
156
|
+
totalLines,
|
|
157
|
+
enclosingContext,
|
|
158
|
+
score: (r.score * 100).toFixed(1),
|
|
159
|
+
snippet: r.content.split('\n').slice(0, 30).join('\n'),
|
|
160
|
+
};
|
|
161
|
+
}),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
102
165
|
if (parentPort) {
|
|
103
166
|
parentPort.on('message', async (msg) => {
|
|
104
167
|
try {
|
|
@@ -107,6 +170,26 @@ if (parentPort) {
|
|
|
107
170
|
return;
|
|
108
171
|
}
|
|
109
172
|
|
|
173
|
+
if (msg.type === 'index-all') {
|
|
174
|
+
const folders = msg.workspacePaths || getWorkspaceFolders(msg.workspacePath || '');
|
|
175
|
+
let indexed = 0;
|
|
176
|
+
for (const folder of folders) {
|
|
177
|
+
if (existsSync(folder)) {
|
|
178
|
+
await initializeIndex(folder);
|
|
179
|
+
indexed++;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
parentPort.postMessage({ id: msg.id, result: { indexed, message: `Indexed ${indexed} repositories` } });
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (msg.type === 'search-all') {
|
|
187
|
+
const folders = msg.workspacePaths || getWorkspaceFolders(msg.workspacePath || '');
|
|
188
|
+
const result = await performSearchAll(folders, msg.query, msg.limit || 10);
|
|
189
|
+
parentPort.postMessage({ id: msg.id, result });
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
110
193
|
if (msg.type === 'search') {
|
|
111
194
|
const result = await performSearch(msg.repositoryPath || process.cwd(), msg.query);
|
|
112
195
|
parentPort.postMessage({ id: msg.id, result });
|