@esankhan3/code-search-mcp 1.0.5 → 1.1.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.
Files changed (41) hide show
  1. package/README.md +114 -54
  2. package/dist/__tests__/_harness.js +194 -0
  3. package/dist/__tests__/config-cli.test.js +110 -0
  4. package/dist/__tests__/config-resolver.test.js +1 -1
  5. package/dist/__tests__/daemon-rpc-protocol.test.js +95 -0
  6. package/dist/__tests__/daemon-runtime.test.js +145 -0
  7. package/dist/__tests__/graph-tools-store.test.js +314 -0
  8. package/dist/__tests__/graph-tools.test.js +11 -4
  9. package/dist/__tests__/http-auth.test.js +180 -0
  10. package/dist/__tests__/http-index.test.js +125 -0
  11. package/dist/__tests__/metrics-collect.test.js +48 -0
  12. package/dist/__tests__/observability.test.js +3 -4
  13. package/dist/__tests__/remote-proxy.test.js +105 -0
  14. package/dist/__tests__/server-cli-args.test.js +146 -0
  15. package/dist/__tests__/server-lifecycle.test.js +108 -0
  16. package/dist/__tests__/tool-inputs.test.js +178 -0
  17. package/dist/backends/in-process.js +5 -2
  18. package/dist/backends/index.js +2 -2
  19. package/dist/cli/index.js +143 -76
  20. package/dist/core/config.js +106 -35
  21. package/dist/core/env-config.js +3 -5
  22. package/dist/core/project-name.js +12 -0
  23. package/dist/core/version.js +5 -0
  24. package/dist/daemon/index.js +106 -65
  25. package/dist/daemon/rpc-server.js +38 -18
  26. package/dist/index.js +113 -89
  27. package/dist/middleware/auth.js +34 -66
  28. package/dist/middleware/rate-limit.js +37 -0
  29. package/dist/observability/logger.js +10 -2
  30. package/dist/observability/metrics.js +7 -6
  31. package/dist/projects/registry.js +13 -15
  32. package/dist/resources/resources.js +17 -0
  33. package/dist/server.js +216 -129
  34. package/dist/tools/graph.js +353 -133
  35. package/dist/tools/index-tools.js +3 -2
  36. package/dist/tools/profile.js +14 -8
  37. package/dist/tools/search.js +61 -51
  38. package/dist/tools/validate.js +48 -0
  39. package/dist/transports/http-transport.js +94 -55
  40. package/dist/transports/remote-proxy.js +58 -97
  41. package/package.json +1 -1
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 { getBlobStore, readVectorIndexHealth } from "@esankhan3/anvil-knowledge-core";
12
- import { indexFromPath, invalidateRetriever } from "@esankhan3/anvil-knowledge-core";
13
- import { loadServerConfig } from "./core/env-config.js";
14
- import { toKnowledgeConfig } from "./core/config.js";
15
- import { startHttpTransport } from "./transports/http-transport.js";
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: "1.0.0" },
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
- console.error(`[code-search-mcp] LLM: ${llmInfo}`);
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
- console.error(`[code-search-mcp] Server running for "${projectName}" (stdio)`);
80
- } else {
81
- const workerCount = parseWorkerCount();
82
- if (workerCount > 1 && cluster.isPrimary) {
83
- armScheduledReindex(ctx, () => broadcastInvalidate(ctx));
84
- runClusterPrimary(ctx, workerCount);
85
- return;
86
- }
87
- if (config.authMode === "none" && config.host !== "127.0.0.1" && config.host !== "localhost") {
88
- console.error(`[code-search-mcp] WARNING: Auth is disabled but server binds to ${config.host}. Any machine on the network can access your code search API.`);
89
- console.error(`[code-search-mcp] Set CODE_SEARCH_AUTH_MODE=api-key or CODE_SEARCH_HOST=127.0.0.1 for security.`);
90
- }
91
- const isReadOnlyWorker = cluster.isWorker;
92
- if (isReadOnlyWorker) registerWorkerIpc(ctx);
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
- if (!cluster.isWorker) armScheduledReindex(ctx);
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 armScheduledReindex(ctx, onSuccess) {
139
- const reindexIntervalMs = parseReindexInterval();
140
- if (reindexIntervalMs > 0 && ctx.directoryPath) {
141
- console.error(`[code-search-mcp] Auto-reindex every ${Math.round(reindexIntervalMs / 6e4)}m`);
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
- console.error(`[auto-reindex] Failed:`, err);
181
+ getComponentLogger("auto-reindex").error(`Failed: ${err instanceof Error ? err.message : String(err)}`);
149
182
  }
150
- }, reindexIntervalMs).unref();
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 Error(`Path does not exist: ${dirPath}`);
214
+ throw new HttpError(400, `Path does not exist: ${dirPath}`);
157
215
  }
158
216
  if (ctx.indexing.status === "indexing") {
159
- throw new Error(`Indexing already in progress (phase: ${ctx.indexing.phase}). Wait for it to complete or check GET /status.`);
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: stats.crossRepoEdges,
231
+ crossRepoEdges: rollup.crossRepoEdges,
175
232
  durationMs: stats.indexDurationMs
176
233
  };
177
234
  }
178
- function broadcastInvalidate(ctx) {
179
- const msg = { type: "csm:invalidate", project: ctx.projectName, directoryPath: ctx.directoryPath };
180
- for (const w of Object.values(cluster.workers ?? {})) {
181
- try {
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
- console.error(`[code-search-mcp] Cluster primary ${process.pid}: forking ${workerCount} read-only workers (stateless MCP)`);
250
+ log.info(`Cluster primary ${process.pid}: forking ${workerCount} read-only workers (stateless MCP)`);
193
251
  let shuttingDown = false;
194
- const send = (w, msg) => {
195
- try {
196
- w?.send(msg);
197
- } catch {
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
- send(worker, { type: "csm:index-result", id: msg.id, ok: true, result });
276
+ broadcastInvalidate(ctx.projectName);
277
+ sendToWorker(worker, { type: "csm:index-result", id: msg.id, ok: true, result });
207
278
  } catch (err) {
208
- send(worker, {
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
- console.error(`[code-search-mcp] Worker ${worker.process.pid} exited (${signal ?? code}) \u2014 respawning`);
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 Error(msg.error ?? "Index failed"));
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 = msg.project;
262
- if (msg.directoryPath) ctx.directoryPath = msg.directoryPath;
263
- ctx.indexReady = true;
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: knowledgeConfig,
384
+ config: resolvedKnowledgeConfig(),
302
385
  onProgress: (m) => {
303
386
  ctx.indexing.message = m;
304
- console.error(`[${label}] ${m}`);
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 blobs = getBlobStore(ctx.projectName, toKnowledgeConfig(loadServerConfig().__unified));
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
- console.error(`[code-search-mcp] Index loaded for "${ctx.projectName}"`);
344
- return;
427
+ log.info(`Index loaded for "${ctx.projectName}"`);
428
+ return false;
345
429
  }
346
430
  if (opts?.readOnly) {
347
- console.error(`[code-search-mcp] No index yet \u2014 read-only worker waiting for the writer`);
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
- console.error(`[code-search-mcp] No index found and no directory path \u2014 tools will return empty results`);
352
- return;
435
+ log.warn(`No index found and no directory path \u2014 tools will return empty results`);
436
+ return false;
353
437
  }
354
- console.error(`[code-search-mcp] No index found \u2014 building from ${ctx.directoryPath}...`);
355
- await trackedIndex(ctx, ctx.projectName, ctx.directoryPath, { label: "auto-index" });
356
- console.error(`[code-search-mcp] Index ready.`);
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
- console.error(`[code-search-mcp] Auto-index failed:`, err);
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
  };