@esankhan3/code-search-mcp 1.0.6 → 1.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/README.md +114 -54
- package/dist/__tests__/_harness.js +194 -0
- package/dist/__tests__/config-cli.test.js +110 -0
- package/dist/__tests__/config-resolver.test.js +1 -1
- package/dist/__tests__/daemon-rpc-protocol.test.js +95 -0
- package/dist/__tests__/daemon-runtime.test.js +145 -0
- package/dist/__tests__/graph-tools-store.test.js +314 -0
- package/dist/__tests__/graph-tools.test.js +11 -4
- package/dist/__tests__/graph-traversal.test.js +262 -0
- package/dist/__tests__/http-auth.test.js +180 -0
- package/dist/__tests__/http-index.test.js +125 -0
- package/dist/__tests__/metrics-collect.test.js +48 -0
- package/dist/__tests__/observability.test.js +3 -4
- package/dist/__tests__/remote-proxy.test.js +105 -0
- package/dist/__tests__/server-cli-args.test.js +146 -0
- package/dist/__tests__/server-lifecycle.test.js +108 -0
- package/dist/__tests__/tool-inputs.test.js +178 -0
- package/dist/backends/in-process.js +5 -2
- package/dist/backends/index.js +2 -2
- package/dist/cli/index.js +143 -76
- package/dist/core/config.js +106 -35
- package/dist/core/env-config.js +3 -5
- package/dist/core/project-name.js +12 -0
- package/dist/core/version.js +5 -0
- package/dist/daemon/index.js +106 -65
- package/dist/daemon/rpc-server.js +38 -18
- package/dist/index.js +113 -89
- package/dist/middleware/auth.js +34 -66
- package/dist/middleware/rate-limit.js +37 -0
- package/dist/observability/logger.js +10 -2
- package/dist/observability/metrics.js +7 -6
- package/dist/projects/registry.js +13 -15
- package/dist/resources/resources.js +17 -0
- package/dist/server.js +216 -129
- package/dist/tools/graph.js +448 -165
- package/dist/tools/index-tools.js +6 -2
- package/dist/tools/profile.js +14 -8
- package/dist/tools/search.js +61 -51
- package/dist/tools/validate.js +48 -0
- package/dist/transports/http-transport.js +94 -55
- package/dist/transports/remote-proxy.js +58 -97
- package/package.json +2 -2
package/dist/server.js
CHANGED
|
@@ -7,16 +7,25 @@ import { registerSearchTools, handleSearchTool } from "./tools/search.js";
|
|
|
7
7
|
import { registerGraphTools, handleGraphTool } from "./tools/graph.js";
|
|
8
8
|
import { registerProfileTools, handleProfileTool } from "./tools/profile.js";
|
|
9
9
|
import { registerIndexTools, handleIndexTool } from "./tools/index-tools.js";
|
|
10
|
-
import { registerResources, handleResource } from "./resources/resources.js";
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
import { registerResources, registerResourceTemplates, handleResource } from "./resources/resources.js";
|
|
11
|
+
import {
|
|
12
|
+
getBlobStore,
|
|
13
|
+
readVectorIndexHealth,
|
|
14
|
+
indexFromPath,
|
|
15
|
+
invalidateRetriever,
|
|
16
|
+
KnowledgeIndexer
|
|
17
|
+
} from "@esankhan3/anvil-knowledge-core";
|
|
18
|
+
import { loadServerConfig, resolvedKnowledgeConfig } from "./core/env-config.js";
|
|
19
|
+
import { PACKAGE_VERSION } from "./core/version.js";
|
|
20
|
+
import { startHttpTransport, HttpError } from "./transports/http-transport.js";
|
|
21
|
+
import { localRateHit, localRateLimiter } from "./middleware/rate-limit.js";
|
|
22
|
+
import { ProjectRegistry, projectAccessAllowed, projectQuotaKey } from "./projects/registry.js";
|
|
16
23
|
import { metrics } from "./observability/metrics.js";
|
|
24
|
+
import { getComponentLogger } from "./observability/logger.js";
|
|
25
|
+
const log = getComponentLogger("code-search-mcp");
|
|
17
26
|
function createMcpServerInstance(ctx) {
|
|
18
27
|
const server = new Server(
|
|
19
|
-
{ name: "code-search-mcp", version:
|
|
28
|
+
{ name: "code-search-mcp", version: PACKAGE_VERSION },
|
|
20
29
|
{ capabilities: { tools: {}, resources: {} } }
|
|
21
30
|
);
|
|
22
31
|
const allTools = [
|
|
@@ -45,13 +54,17 @@ function createMcpServerInstance(ctx) {
|
|
|
45
54
|
server.setRequestHandler("resources/list", async () => ({
|
|
46
55
|
resources: allResources
|
|
47
56
|
}));
|
|
57
|
+
server.setRequestHandler("resources/templates/list", async () => ({
|
|
58
|
+
resourceTemplates: registerResourceTemplates()
|
|
59
|
+
}));
|
|
48
60
|
server.setRequestHandler("resources/read", async (request) => {
|
|
49
61
|
return handleResource(request.params.uri, ctx);
|
|
50
62
|
});
|
|
51
63
|
return server;
|
|
52
64
|
}
|
|
53
|
-
async function startServer(projectName, directoryPath) {
|
|
65
|
+
async function startServer(projectName, directoryPath, opts = {}) {
|
|
54
66
|
const config = loadServerConfig();
|
|
67
|
+
const reindexIntervalMs = config.__unified.indexing.reindexIntervalMs;
|
|
55
68
|
const ctx = {
|
|
56
69
|
projectName,
|
|
57
70
|
directoryPath,
|
|
@@ -70,153 +83,212 @@ async function startServer(projectName, directoryPath) {
|
|
|
70
83
|
}
|
|
71
84
|
};
|
|
72
85
|
const llmInfo = config.llmMode === "none" ? "disabled (profiling + service mesh skipped)" : config.llmMode === "api" ? `api \u2192 ${config.llmProvider}/${config.llmModel}${config.llmApiKey ? "" : " (WARNING: no API key!)"}` : `cli \u2192 ${config.claudeBin}`;
|
|
73
|
-
|
|
74
|
-
await autoIndex(ctx, { readOnly: cluster.isWorker });
|
|
86
|
+
log.info(`LLM: ${llmInfo}`);
|
|
75
87
|
if (config.transport === "stdio") {
|
|
88
|
+
await autoIndex(ctx, { force: opts.force });
|
|
76
89
|
const server = createMcpServerInstance(ctx);
|
|
77
90
|
const transport = new StdioServerTransport();
|
|
78
91
|
await server.connect(transport);
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
if (
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
await startHttpTransport({
|
|
94
|
-
config,
|
|
95
|
-
createMcpServer: async () => ({
|
|
96
|
-
server: createMcpServerInstance(ctx)
|
|
97
|
-
}),
|
|
98
|
-
onReady: (url) => {
|
|
99
|
-
console.error(`[code-search-mcp] Server running for "${projectName}" at ${url}/mcp`);
|
|
100
|
-
console.error(`[code-search-mcp] Health: GET ${url}/health`);
|
|
101
|
-
console.error(`[code-search-mcp] Status: GET ${url}/status`);
|
|
102
|
-
console.error(`[code-search-mcp] Index: POST ${url}/index`);
|
|
103
|
-
console.error(`[code-search-mcp] Auth: ${config.authMode}`);
|
|
104
|
-
},
|
|
105
|
-
getHealth: () => ({
|
|
106
|
-
project: ctx.projectName,
|
|
107
|
-
indexReady: ctx.indexReady,
|
|
108
|
-
indexing: ctx.indexing.status,
|
|
109
|
-
uptime: Math.floor((Date.now() - ctx.startedAt) / 1e3),
|
|
110
|
-
transport: config.transport,
|
|
111
|
-
authMode: config.authMode
|
|
112
|
-
}),
|
|
113
|
-
getStatus: () => ({
|
|
114
|
-
project: ctx.projectName,
|
|
115
|
-
directoryPath: ctx.directoryPath,
|
|
116
|
-
indexReady: ctx.indexReady,
|
|
117
|
-
uptime: Math.floor((Date.now() - ctx.startedAt) / 1e3),
|
|
118
|
-
indexing: {
|
|
119
|
-
...ctx.indexing,
|
|
120
|
-
elapsedMs: ctx.indexing.startedAt ? Date.now() - ctx.indexing.startedAt : null
|
|
121
|
-
}
|
|
122
|
-
}),
|
|
123
|
-
// From the writer's blob record, not process state: in cluster mode the
|
|
124
|
-
// workers serving HTTP are not the writer.
|
|
125
|
-
collectMetrics: async () => {
|
|
126
|
-
const blobs = getBlobStore(ctx.projectName, toKnowledgeConfig(loadServerConfig().__unified));
|
|
127
|
-
const health = await readVectorIndexHealth(blobs);
|
|
128
|
-
if (!health) return;
|
|
129
|
-
const labels = { project: ctx.projectName };
|
|
130
|
-
metrics.vectorIndexUnindexedRows.set(health.numUnindexedRows ?? 0, labels);
|
|
131
|
-
if (health.trainedRows) metrics.vectorIndexGrowthRatio.set(health.rowsAddedSinceTrain / health.trainedRows, labels);
|
|
132
|
-
},
|
|
133
|
-
onIndex: (body) => isReadOnlyWorker ? forwardIndexToPrimary(body) : runAdminIndex(ctx, body)
|
|
134
|
-
});
|
|
92
|
+
log.info(`Server running for "${projectName}" (stdio)`);
|
|
93
|
+
armScheduledReindex(ctx, reindexIntervalMs);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const workerCount = parseWorkerCount();
|
|
97
|
+
if (workerCount > 1 && cluster.isPrimary) {
|
|
98
|
+
runClusterPrimary(ctx, workerCount);
|
|
99
|
+
armScheduledReindex(ctx, reindexIntervalMs, () => broadcastInvalidate(ctx.projectName));
|
|
100
|
+
if (await autoIndex(ctx, { force: opts.force })) broadcastInvalidate(ctx.projectName);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (config.authMode === "none" && config.host !== "127.0.0.1" && config.host !== "localhost") {
|
|
104
|
+
log.warn(`WARNING: Auth is disabled but server binds to ${config.host}. Any machine on the network can access your code search API.`);
|
|
105
|
+
log.warn(`Set CODE_SEARCH_AUTH_MODE=api-key or CODE_SEARCH_HOST=127.0.0.1 for security.`);
|
|
135
106
|
}
|
|
136
|
-
|
|
107
|
+
const isReadOnlyWorker = cluster.isWorker;
|
|
108
|
+
if (isReadOnlyWorker) registerWorkerIpc(ctx);
|
|
109
|
+
const rateLimiter = isReadOnlyWorker ? primaryRateLimiter : localRateLimiter;
|
|
110
|
+
await startHttpTransport({
|
|
111
|
+
config,
|
|
112
|
+
createMcpServer: async () => ({
|
|
113
|
+
server: createMcpServerInstance(ctx)
|
|
114
|
+
}),
|
|
115
|
+
onReady: (url) => {
|
|
116
|
+
log.info(`Server running for "${projectName}" at ${url}/mcp`);
|
|
117
|
+
log.info(`Health: GET ${url}/health`);
|
|
118
|
+
log.info(`Status: GET ${url}/status`);
|
|
119
|
+
log.info(`Index: POST ${url}/index`);
|
|
120
|
+
log.info(`Auth: ${config.authMode}`);
|
|
121
|
+
},
|
|
122
|
+
getHealth: () => ({
|
|
123
|
+
project: ctx.projectName,
|
|
124
|
+
indexReady: ctx.indexReady,
|
|
125
|
+
indexing: ctx.indexing.status,
|
|
126
|
+
uptime: Math.floor((Date.now() - ctx.startedAt) / 1e3),
|
|
127
|
+
transport: config.transport,
|
|
128
|
+
authMode: config.authMode
|
|
129
|
+
}),
|
|
130
|
+
getStatus: () => ({
|
|
131
|
+
project: ctx.projectName,
|
|
132
|
+
directoryPath: ctx.directoryPath,
|
|
133
|
+
indexReady: ctx.indexReady,
|
|
134
|
+
uptime: Math.floor((Date.now() - ctx.startedAt) / 1e3),
|
|
135
|
+
indexing: {
|
|
136
|
+
...ctx.indexing,
|
|
137
|
+
elapsedMs: ctx.indexing.startedAt ? Date.now() - ctx.indexing.startedAt : null
|
|
138
|
+
}
|
|
139
|
+
}),
|
|
140
|
+
collectMetrics: () => collectIndexMetrics(ctx.projectName, resolvedKnowledgeConfig(), ctx.indexing.lastSuccess),
|
|
141
|
+
onIndex: (body) => isReadOnlyWorker ? forwardIndexToPrimary(body) : runAdminIndex(ctx, body),
|
|
142
|
+
authorize: createProjectAuthorizer(ctx, config.dataDir, rateLimiter),
|
|
143
|
+
rateLimiter
|
|
144
|
+
});
|
|
145
|
+
if (!isReadOnlyWorker) armScheduledReindex(ctx, reindexIntervalMs);
|
|
146
|
+
await autoIndex(ctx, { readOnly: isReadOnlyWorker, force: opts.force });
|
|
137
147
|
}
|
|
138
|
-
function
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
148
|
+
async function collectIndexMetrics(project, knowledgeConfig, lastSuccess = null) {
|
|
149
|
+
const owned = [metrics.indexAge, metrics.indexChunks, metrics.vectorIndexUnindexedRows, metrics.vectorIndexGrowthRatio];
|
|
150
|
+
let stats;
|
|
151
|
+
let health;
|
|
152
|
+
try {
|
|
153
|
+
const blobs = getBlobStore(project, knowledgeConfig);
|
|
154
|
+
[stats, health] = await Promise.all([
|
|
155
|
+
blobs.getJson("index_stats.json"),
|
|
156
|
+
readVectorIndexHealth(blobs)
|
|
157
|
+
]);
|
|
158
|
+
} catch (err) {
|
|
159
|
+
for (const g of owned) g.clear();
|
|
160
|
+
throw err;
|
|
161
|
+
}
|
|
162
|
+
for (const g of owned) g.clear();
|
|
163
|
+
const labels = { project };
|
|
164
|
+
const lastMs = [stats?.lastIndexed, lastSuccess].map((t) => t ? Date.parse(t) : NaN).filter((t) => Number.isFinite(t));
|
|
165
|
+
if (lastMs.length) metrics.indexAge.set(Math.max(0, Math.round((Date.now() - Math.max(...lastMs)) / 1e3)), labels);
|
|
166
|
+
for (const r of stats?.repos ?? []) metrics.indexChunks.set(r.chunkCount, { project, repo: r.name });
|
|
167
|
+
if (health) {
|
|
168
|
+
metrics.vectorIndexUnindexedRows.set(health.numUnindexedRows ?? 0, labels);
|
|
169
|
+
if (health.trainedRows) metrics.vectorIndexGrowthRatio.set(health.rowsAddedSinceTrain / health.trainedRows, labels);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function armScheduledReindex(ctx, intervalMs, onSuccess) {
|
|
173
|
+
if (intervalMs > 0 && ctx.directoryPath) {
|
|
174
|
+
log.info(`Auto-reindex every ${Math.round(intervalMs / 6e4)}m`);
|
|
142
175
|
setInterval(async () => {
|
|
143
176
|
if (!ctx.directoryPath || ctx.indexing.status === "indexing") return;
|
|
144
177
|
try {
|
|
145
178
|
await trackedIndex(ctx, ctx.projectName, ctx.directoryPath, { label: "auto-reindex" });
|
|
146
179
|
onSuccess?.();
|
|
147
180
|
} catch (err) {
|
|
148
|
-
|
|
181
|
+
getComponentLogger("auto-reindex").error(`Failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
149
182
|
}
|
|
150
|
-
},
|
|
183
|
+
}, intervalMs).unref();
|
|
151
184
|
}
|
|
152
185
|
}
|
|
186
|
+
function createProjectAuthorizer(ctx, dataDir, rateLimiter) {
|
|
187
|
+
const projects = new ProjectRegistry(dataDir);
|
|
188
|
+
projects.reload();
|
|
189
|
+
return async (identity) => {
|
|
190
|
+
const entry = projects.get(ctx.projectName);
|
|
191
|
+
if (!entry) return { ok: true };
|
|
192
|
+
if (!projectAccessAllowed(entry, identity.scopes)) {
|
|
193
|
+
return { ok: false, status: 403, error: `Forbidden: project "${entry.name}" requires one of the scopes: ${entry.scopes.join(", ")}` };
|
|
194
|
+
}
|
|
195
|
+
const d = await rateLimiter(projectQuotaKey(entry, identity.subject), entry.quotas.maxQueriesPerMinute);
|
|
196
|
+
if (!d.ok) {
|
|
197
|
+
return {
|
|
198
|
+
ok: false,
|
|
199
|
+
status: 429,
|
|
200
|
+
error: `Quota exceeded for project "${entry.name}": ${entry.quotas.maxQueriesPerMinute} requests/min`,
|
|
201
|
+
retryAfterSec: d.retryAfterSec
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
return { ok: true };
|
|
205
|
+
};
|
|
206
|
+
}
|
|
153
207
|
async function runAdminIndex(ctx, body) {
|
|
208
|
+
const project = body.project ?? ctx.projectName;
|
|
209
|
+
if (project !== ctx.projectName) {
|
|
210
|
+
throw new HttpError(400, `This server serves project "${ctx.projectName}"; POST /index only (re)indexes it. Index "${project}" with the code-search CLI or another server.`);
|
|
211
|
+
}
|
|
154
212
|
const dirPath = resolve(body.path);
|
|
155
213
|
if (!existsSync(dirPath)) {
|
|
156
|
-
throw new
|
|
214
|
+
throw new HttpError(400, `Path does not exist: ${dirPath}`);
|
|
157
215
|
}
|
|
158
216
|
if (ctx.indexing.status === "indexing") {
|
|
159
|
-
throw new
|
|
217
|
+
throw new HttpError(409, `Indexing already in progress (phase: ${ctx.indexing.phase}). Wait for it to complete or check GET /status.`);
|
|
160
218
|
}
|
|
161
|
-
const project = body.project || dirPath.split("/").filter(Boolean).pop() || "project";
|
|
162
219
|
const stats = await trackedIndex(ctx, project, dirPath, {
|
|
163
220
|
force: body.force,
|
|
164
221
|
label: "admin-index"
|
|
165
222
|
});
|
|
166
|
-
ctx.projectName = project;
|
|
167
223
|
ctx.directoryPath = dirPath;
|
|
224
|
+
const rollup = await new KnowledgeIndexer().getStats(project, resolvedKnowledgeConfig());
|
|
168
225
|
return {
|
|
169
226
|
status: "ok",
|
|
170
227
|
project,
|
|
171
228
|
path: dirPath,
|
|
172
229
|
chunks: stats.totalChunks,
|
|
173
230
|
repos: stats.repos.length,
|
|
174
|
-
crossRepoEdges:
|
|
231
|
+
crossRepoEdges: rollup.crossRepoEdges,
|
|
175
232
|
durationMs: stats.indexDurationMs
|
|
176
233
|
};
|
|
177
234
|
}
|
|
178
|
-
function
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
w?.send(msg);
|
|
183
|
-
} catch {
|
|
184
|
-
}
|
|
235
|
+
function sendToWorker(w, msg) {
|
|
236
|
+
try {
|
|
237
|
+
w?.send(msg);
|
|
238
|
+
} catch {
|
|
185
239
|
}
|
|
186
240
|
}
|
|
241
|
+
function broadcastInvalidate(project) {
|
|
242
|
+
const msg = { type: "csm:invalidate", project };
|
|
243
|
+
for (const w of Object.values(cluster.workers ?? {})) sendToWorker(w, msg);
|
|
244
|
+
}
|
|
187
245
|
function parseWorkerCount() {
|
|
188
246
|
const n = parseInt(process.env.CODE_SEARCH_WORKERS ?? "", 10);
|
|
189
247
|
return Number.isFinite(n) && n > 1 ? Math.min(n, 16) : 1;
|
|
190
248
|
}
|
|
191
249
|
function runClusterPrimary(ctx, workerCount) {
|
|
192
|
-
|
|
250
|
+
log.info(`Cluster primary ${process.pid}: forking ${workerCount} read-only workers (stateless MCP)`);
|
|
193
251
|
let shuttingDown = false;
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
};
|
|
252
|
+
const stateMsg = () => ({
|
|
253
|
+
type: "csm:state",
|
|
254
|
+
indexReady: ctx.indexReady,
|
|
255
|
+
directoryPath: ctx.directoryPath,
|
|
256
|
+
indexing: ctx.indexing
|
|
257
|
+
});
|
|
258
|
+
let lastState = "";
|
|
259
|
+
setInterval(() => {
|
|
260
|
+
const msg = stateMsg();
|
|
261
|
+
const json = JSON.stringify(msg);
|
|
262
|
+
if (json === lastState) return;
|
|
263
|
+
lastState = json;
|
|
264
|
+
for (const w of Object.values(cluster.workers ?? {})) sendToWorker(w, msg);
|
|
265
|
+
}, 250).unref();
|
|
266
|
+
cluster.on("online", (worker) => sendToWorker(worker, stateMsg()));
|
|
200
267
|
cluster.on("message", (worker, msg) => {
|
|
268
|
+
if (msg?.type === "csm:rate") {
|
|
269
|
+
sendToWorker(worker, { type: "csm:rate-result", id: msg.id, ...localRateHit(msg.key, msg.limit) });
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
201
272
|
if (msg?.type !== "csm:index") return;
|
|
202
273
|
void (async () => {
|
|
203
274
|
try {
|
|
204
275
|
const result = await runAdminIndex(ctx, msg.body);
|
|
205
|
-
broadcastInvalidate(ctx);
|
|
206
|
-
|
|
276
|
+
broadcastInvalidate(ctx.projectName);
|
|
277
|
+
sendToWorker(worker, { type: "csm:index-result", id: msg.id, ok: true, result });
|
|
207
278
|
} catch (err) {
|
|
208
|
-
|
|
279
|
+
sendToWorker(worker, {
|
|
209
280
|
type: "csm:index-result",
|
|
210
281
|
id: msg.id,
|
|
211
282
|
ok: false,
|
|
212
|
-
error: err instanceof Error ? err.message : String(err)
|
|
283
|
+
error: err instanceof Error ? err.message : String(err),
|
|
284
|
+
status: err instanceof HttpError ? err.status : void 0
|
|
213
285
|
});
|
|
214
286
|
}
|
|
215
287
|
})();
|
|
216
288
|
});
|
|
217
289
|
cluster.on("exit", (worker, code, signal) => {
|
|
218
290
|
if (shuttingDown) return;
|
|
219
|
-
|
|
291
|
+
log.warn(`Worker ${worker.process.pid} exited (${signal ?? code}) \u2014 respawning`);
|
|
220
292
|
cluster.fork();
|
|
221
293
|
});
|
|
222
294
|
const shutdown = (sig) => {
|
|
@@ -235,6 +307,7 @@ function runClusterPrimary(ctx, workerCount) {
|
|
|
235
307
|
}
|
|
236
308
|
let ipcSeq = 0;
|
|
237
309
|
const pendingIndexRequests = /* @__PURE__ */ new Map();
|
|
310
|
+
const pendingRateRequests = /* @__PURE__ */ new Map();
|
|
238
311
|
function forwardIndexToPrimary(body) {
|
|
239
312
|
return new Promise((resolvePromise, rejectPromise) => {
|
|
240
313
|
if (!process.send) {
|
|
@@ -247,6 +320,23 @@ function forwardIndexToPrimary(body) {
|
|
|
247
320
|
process.send(msg);
|
|
248
321
|
});
|
|
249
322
|
}
|
|
323
|
+
const primaryRateLimiter = (key, limit) => new Promise((resolveP) => {
|
|
324
|
+
if (!process.send || limit <= 0) {
|
|
325
|
+
resolveP(localRateHit(key, limit));
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
const id = ++ipcSeq;
|
|
329
|
+
const timer = setTimeout(() => {
|
|
330
|
+
pendingRateRequests.delete(id);
|
|
331
|
+
resolveP(localRateHit(key, limit));
|
|
332
|
+
}, 2e3);
|
|
333
|
+
pendingRateRequests.set(id, (d) => {
|
|
334
|
+
clearTimeout(timer);
|
|
335
|
+
resolveP(d);
|
|
336
|
+
});
|
|
337
|
+
const msg = { type: "csm:rate", id, key, limit };
|
|
338
|
+
process.send(msg);
|
|
339
|
+
});
|
|
250
340
|
function registerWorkerIpc(ctx) {
|
|
251
341
|
process.on("message", (msg) => {
|
|
252
342
|
if (msg?.type === "csm:index-result") {
|
|
@@ -254,29 +344,23 @@ function registerWorkerIpc(ctx) {
|
|
|
254
344
|
if (!pending) return;
|
|
255
345
|
pendingIndexRequests.delete(msg.id);
|
|
256
346
|
if (msg.ok) pending.resolve(msg.result ?? {});
|
|
257
|
-
else pending.reject(new
|
|
347
|
+
else pending.reject(new HttpError(msg.status ?? 500, msg.error ?? "Index failed"));
|
|
348
|
+
} else if (msg?.type === "csm:rate-result") {
|
|
349
|
+
const pending = pendingRateRequests.get(msg.id);
|
|
350
|
+
pendingRateRequests.delete(msg.id);
|
|
351
|
+
pending?.({ ok: msg.ok, retryAfterSec: msg.retryAfterSec });
|
|
258
352
|
} else if (msg?.type === "csm:invalidate") {
|
|
259
353
|
void invalidateRetriever(msg.project).catch(() => {
|
|
260
354
|
});
|
|
261
|
-
ctx.projectName =
|
|
262
|
-
|
|
263
|
-
ctx.
|
|
355
|
+
if (msg.project === ctx.projectName) ctx.indexReady = true;
|
|
356
|
+
} else if (msg?.type === "csm:state") {
|
|
357
|
+
ctx.indexing = msg.indexing;
|
|
358
|
+
ctx.directoryPath = msg.directoryPath;
|
|
359
|
+
ctx.indexReady = ctx.indexReady || msg.indexReady;
|
|
264
360
|
}
|
|
265
361
|
});
|
|
266
362
|
process.on("disconnect", () => process.exit(0));
|
|
267
363
|
}
|
|
268
|
-
function parseReindexInterval() {
|
|
269
|
-
const raw = process.env.CODE_SEARCH_REINDEX_INTERVAL?.trim();
|
|
270
|
-
if (!raw || raw === "0" || raw === "none") return 0;
|
|
271
|
-
const match = raw.match(/^(\d+)(m|h)$/);
|
|
272
|
-
if (!match) {
|
|
273
|
-
console.error(`[code-search-mcp] Invalid REINDEX_INTERVAL "${raw}" \u2014 use "30m", "1h", etc. Disabling.`);
|
|
274
|
-
return 0;
|
|
275
|
-
}
|
|
276
|
-
const value = parseInt(match[1], 10);
|
|
277
|
-
const unit = match[2];
|
|
278
|
-
return unit === "h" ? value * 60 * 6e4 : value * 6e4;
|
|
279
|
-
}
|
|
280
364
|
const MAX_HISTORY = 50;
|
|
281
365
|
function pushHistory(ctx, type, message) {
|
|
282
366
|
ctx.indexing.history.push({ timestamp: (/* @__PURE__ */ new Date()).toISOString(), type, message });
|
|
@@ -286,6 +370,7 @@ function pushHistory(ctx, type, message) {
|
|
|
286
370
|
}
|
|
287
371
|
async function trackedIndex(ctx, project, dirPath, opts) {
|
|
288
372
|
const label = opts?.label ?? "index";
|
|
373
|
+
const progressLog = getComponentLogger(label);
|
|
289
374
|
ctx.indexing.status = "indexing";
|
|
290
375
|
ctx.indexing.phase = "starting";
|
|
291
376
|
ctx.indexing.message = `Starting ${label}...`;
|
|
@@ -293,15 +378,13 @@ async function trackedIndex(ctx, project, dirPath, opts) {
|
|
|
293
378
|
ctx.indexing.startedAt = Date.now();
|
|
294
379
|
ctx.indexing.error = null;
|
|
295
380
|
pushHistory(ctx, "start", `${label}: started for "${project}" at ${dirPath}`);
|
|
296
|
-
const unified = loadServerConfig().__unified;
|
|
297
|
-
const knowledgeConfig = toKnowledgeConfig(unified);
|
|
298
381
|
try {
|
|
299
382
|
const stats = await indexFromPath(project, dirPath, {
|
|
300
383
|
force: opts?.force,
|
|
301
|
-
config:
|
|
384
|
+
config: resolvedKnowledgeConfig(),
|
|
302
385
|
onProgress: (m) => {
|
|
303
386
|
ctx.indexing.message = m;
|
|
304
|
-
|
|
387
|
+
progressLog.info(m);
|
|
305
388
|
},
|
|
306
389
|
onDetailedProgress: (p) => {
|
|
307
390
|
ctx.indexing.phase = p.phase;
|
|
@@ -333,31 +416,35 @@ async function trackedIndex(ctx, project, dirPath, opts) {
|
|
|
333
416
|
}
|
|
334
417
|
async function autoIndex(ctx, opts) {
|
|
335
418
|
try {
|
|
336
|
-
const
|
|
419
|
+
const rebuild = !!opts?.force && !opts.readOnly && !!ctx.directoryPath;
|
|
420
|
+
const blobs = getBlobStore(ctx.projectName, resolvedKnowledgeConfig());
|
|
337
421
|
const stats = await blobs.getJson("index_stats.json");
|
|
338
422
|
const statsReady = !!stats && stats.embeddingProvider !== "pending";
|
|
339
423
|
const hasLanceDB = await blobs.exists("lancedb");
|
|
340
424
|
const hasGraph = await blobs.exists("system_graph.sqlite") || await blobs.exists("system_graph_v2.json");
|
|
341
|
-
if (statsReady || hasLanceDB && hasGraph) {
|
|
425
|
+
if ((statsReady || hasLanceDB && hasGraph) && !rebuild) {
|
|
342
426
|
ctx.indexReady = true;
|
|
343
|
-
|
|
344
|
-
return;
|
|
427
|
+
log.info(`Index loaded for "${ctx.projectName}"`);
|
|
428
|
+
return false;
|
|
345
429
|
}
|
|
346
430
|
if (opts?.readOnly) {
|
|
347
|
-
|
|
348
|
-
return;
|
|
431
|
+
log.info(`No index yet \u2014 read-only worker waiting for the writer`);
|
|
432
|
+
return false;
|
|
349
433
|
}
|
|
350
434
|
if (!ctx.directoryPath) {
|
|
351
|
-
|
|
352
|
-
return;
|
|
435
|
+
log.warn(`No index found and no directory path \u2014 tools will return empty results`);
|
|
436
|
+
return false;
|
|
353
437
|
}
|
|
354
|
-
|
|
355
|
-
await trackedIndex(ctx, ctx.projectName, ctx.directoryPath, { label: "auto-index" });
|
|
356
|
-
|
|
438
|
+
log.info(`${rebuild ? "Forced re-index" : "No index found"} \u2014 building from ${ctx.directoryPath}...`);
|
|
439
|
+
await trackedIndex(ctx, ctx.projectName, ctx.directoryPath, { label: "auto-index", force: rebuild });
|
|
440
|
+
log.info(`Index ready.`);
|
|
441
|
+
return true;
|
|
357
442
|
} catch (err) {
|
|
358
|
-
|
|
443
|
+
log.error(`Auto-index failed: ${err instanceof Error ? err.stack ?? err.message : String(err)}`);
|
|
444
|
+
return false;
|
|
359
445
|
}
|
|
360
446
|
}
|
|
361
447
|
export {
|
|
448
|
+
collectIndexMetrics,
|
|
362
449
|
startServer
|
|
363
450
|
};
|