@esankhan3/code-search-mcp 0.1.0 → 0.2.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.
@@ -0,0 +1,99 @@
1
+ import { createServer } from "node:net";
2
+ import { existsSync, unlinkSync, mkdirSync } from "node:fs";
3
+ import { dirname } from "node:path";
4
+ import { EventEmitter } from "node:events";
5
+ class RpcServer extends EventEmitter {
6
+ socketPath;
7
+ handlers;
8
+ server = null;
9
+ clients = /* @__PURE__ */ new Set();
10
+ constructor(opts) {
11
+ super();
12
+ this.socketPath = opts.socketPath;
13
+ this.handlers = opts.handlers;
14
+ }
15
+ async start() {
16
+ if (process.platform !== "win32" && existsSync(this.socketPath)) {
17
+ try {
18
+ unlinkSync(this.socketPath);
19
+ } catch {
20
+ }
21
+ }
22
+ mkdirSync(dirname(this.socketPath), { recursive: true });
23
+ await new Promise((resolveP, rejectP) => {
24
+ this.server = createServer((sock) => this.handleClient(sock));
25
+ this.server.on("error", (err) => {
26
+ this.emit("error", err);
27
+ rejectP(err);
28
+ });
29
+ this.server.listen(this.socketPath, () => {
30
+ this.emit("listening", this.socketPath);
31
+ resolveP();
32
+ });
33
+ });
34
+ }
35
+ async stop() {
36
+ for (const c of this.clients) {
37
+ try {
38
+ c.destroy();
39
+ } catch {
40
+ }
41
+ }
42
+ this.clients.clear();
43
+ if (this.server) {
44
+ await new Promise((r) => this.server.close(() => r()));
45
+ this.server = null;
46
+ }
47
+ if (process.platform !== "win32" && existsSync(this.socketPath)) {
48
+ try {
49
+ unlinkSync(this.socketPath);
50
+ } catch {
51
+ }
52
+ }
53
+ }
54
+ handleClient(sock) {
55
+ this.clients.add(sock);
56
+ let buf = "";
57
+ sock.setNoDelay(true);
58
+ sock.on("end", () => this.clients.delete(sock));
59
+ sock.on("error", () => this.clients.delete(sock));
60
+ sock.on("data", async (chunk) => {
61
+ buf += chunk.toString("utf-8");
62
+ let idx = buf.indexOf("\n");
63
+ while (idx >= 0) {
64
+ const line = buf.slice(0, idx);
65
+ buf = buf.slice(idx + 1);
66
+ await this.handleLine(sock, line);
67
+ idx = buf.indexOf("\n");
68
+ }
69
+ });
70
+ }
71
+ async handleLine(sock, line) {
72
+ if (!line.trim()) return;
73
+ let req;
74
+ try {
75
+ req = JSON.parse(line);
76
+ } catch (err) {
77
+ sock.write(this.errorEnvelope(null, -32700, err instanceof Error ? err.message : String(err)));
78
+ return;
79
+ }
80
+ const handler = this.handlers[req.method];
81
+ if (!handler) {
82
+ sock.write(this.errorEnvelope(req.id ?? null, -32601, `Method not found: ${req.method}`));
83
+ return;
84
+ }
85
+ try {
86
+ const result = await handler(req.params);
87
+ sock.write(JSON.stringify({ jsonrpc: "2.0", id: req.id ?? null, result }) + "\n");
88
+ } catch (err) {
89
+ const msg = err instanceof Error ? err.message : String(err);
90
+ sock.write(this.errorEnvelope(req.id ?? null, -32e3, msg));
91
+ }
92
+ }
93
+ errorEnvelope(id, code, message) {
94
+ return JSON.stringify({ jsonrpc: "2.0", id, error: { code, message } }) + "\n";
95
+ }
96
+ }
97
+ export {
98
+ RpcServer
99
+ };
@@ -0,0 +1,105 @@
1
+ import { watch, statSync, readdirSync } from "node:fs";
2
+ import { join, relative } from "node:path";
3
+ import { EventEmitter } from "node:events";
4
+ const HAS_RECURSIVE = process.platform === "darwin" || process.platform === "win32";
5
+ class Watcher extends EventEmitter {
6
+ workspaceDir;
7
+ ignore;
8
+ debounceMs;
9
+ watchers = [];
10
+ pending = /* @__PURE__ */ new Set();
11
+ timer = null;
12
+ started = false;
13
+ constructor(opts) {
14
+ super();
15
+ this.workspaceDir = opts.workspaceDir;
16
+ this.ignore = opts.ignorePatterns;
17
+ this.debounceMs = Math.max(50, opts.debounceMs);
18
+ }
19
+ start() {
20
+ if (this.started) return;
21
+ this.started = true;
22
+ if (HAS_RECURSIVE) {
23
+ const w = watch(this.workspaceDir, { recursive: true }, (_event, filename) => {
24
+ if (!filename) return;
25
+ this.queue(filename.toString());
26
+ });
27
+ w.on("error", (err) => this.emit("error", err));
28
+ this.watchers.push(w);
29
+ } else {
30
+ this.walkAndWatch(this.workspaceDir);
31
+ }
32
+ }
33
+ stop() {
34
+ if (!this.started) return;
35
+ this.started = false;
36
+ if (this.timer) clearTimeout(this.timer);
37
+ this.timer = null;
38
+ for (const w of this.watchers) {
39
+ try {
40
+ w.close();
41
+ } catch {
42
+ }
43
+ }
44
+ this.watchers = [];
45
+ this.pending.clear();
46
+ }
47
+ onBatch(listener) {
48
+ return this.on("batch", listener);
49
+ }
50
+ walkAndWatch(dir) {
51
+ if (this.shouldIgnore(dir)) return;
52
+ try {
53
+ const w = watch(dir, (_event, filename) => {
54
+ if (!filename) return;
55
+ const full = join(dir, filename.toString());
56
+ this.queue(relative(this.workspaceDir, full));
57
+ try {
58
+ if (statSync(full).isDirectory()) this.walkAndWatch(full);
59
+ } catch {
60
+ }
61
+ });
62
+ w.on("error", (err) => this.emit("error", err));
63
+ this.watchers.push(w);
64
+ for (const entry of readdirSync(dir)) {
65
+ const full = join(dir, entry);
66
+ try {
67
+ if (statSync(full).isDirectory()) this.walkAndWatch(full);
68
+ } catch {
69
+ }
70
+ }
71
+ } catch {
72
+ }
73
+ }
74
+ queue(rel) {
75
+ if (this.shouldIgnore(rel)) return;
76
+ this.pending.add(join(this.workspaceDir, rel));
77
+ if (this.timer) clearTimeout(this.timer);
78
+ this.timer = setTimeout(() => this.flush(), this.debounceMs);
79
+ }
80
+ flush() {
81
+ const batch = { changed: new Set(this.pending), removed: /* @__PURE__ */ new Set() };
82
+ this.pending.clear();
83
+ this.timer = null;
84
+ for (const p of batch.changed) {
85
+ try {
86
+ statSync(p);
87
+ } catch {
88
+ batch.removed.add(p);
89
+ }
90
+ }
91
+ for (const r of batch.removed) batch.changed.delete(r);
92
+ this.emit("batch", batch);
93
+ }
94
+ shouldIgnore(path) {
95
+ for (const pat of this.ignore) {
96
+ if (path.includes(`/${pat}/`) || path.endsWith(`/${pat}`) || path.startsWith(`${pat}/`) || path === pat) {
97
+ return true;
98
+ }
99
+ }
100
+ return false;
101
+ }
102
+ }
103
+ export {
104
+ Watcher
105
+ };
package/dist/index.js CHANGED
@@ -1,7 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import { resolve } from "node:path";
3
3
  import { existsSync } from "node:fs";
4
+ import { printConfig, resolveCodeSearchConfig, parseCliFlags } from "./core/config.js";
4
5
  const args = process.argv.slice(2);
6
+ if (args.includes("--print-config")) {
7
+ const { patch } = parseCliFlags(args.filter((a) => a !== "--print-config"));
8
+ const cfg = resolveCodeSearchConfig({ cli: patch });
9
+ process.stdout.write(printConfig(cfg) + "\n");
10
+ process.exit(0);
11
+ }
5
12
  let mode = "remote";
6
13
  let remoteUrl = null;
7
14
  let apiKey;
@@ -0,0 +1,66 @@
1
+ const LEVEL_RANK = { debug: 10, info: 20, warn: 30, error: 40 };
2
+ class Logger {
3
+ opts;
4
+ constructor(opts) {
5
+ this.opts = {
6
+ structured: opts.structured,
7
+ level: opts.level ?? "info",
8
+ write: opts.write ?? ((line) => process.stderr.write(line + "\n"))
9
+ };
10
+ }
11
+ debug(msg, fields) {
12
+ this.emit("debug", msg, fields);
13
+ }
14
+ info(msg, fields) {
15
+ this.emit("info", msg, fields);
16
+ }
17
+ warn(msg, fields) {
18
+ this.emit("warn", msg, fields);
19
+ }
20
+ error(msg, fields) {
21
+ this.emit("error", msg, fields);
22
+ }
23
+ /** Bind a component name so every line carries it. */
24
+ child(component) {
25
+ return new Logger({
26
+ structured: this.opts.structured,
27
+ level: this.opts.level,
28
+ write: (line) => this.opts.write(line)
29
+ }).withComponent(component);
30
+ }
31
+ /** Internal — return a logger that prefixes the component field. */
32
+ withComponent(component) {
33
+ const self = this;
34
+ return new Proxy(self, {
35
+ get(target, prop) {
36
+ if (prop === "emit") {
37
+ return (level, msg, fields) => self.emit(level, msg, { component, ...fields });
38
+ }
39
+ return target[prop];
40
+ }
41
+ });
42
+ }
43
+ emit(level, msg, fields) {
44
+ if (LEVEL_RANK[level] < LEVEL_RANK[this.opts.level]) return;
45
+ if (this.opts.structured) {
46
+ const entry = { level, msg, ts: (/* @__PURE__ */ new Date()).toISOString(), ...fields };
47
+ this.opts.write(JSON.stringify(entry));
48
+ } else {
49
+ const comp = fields?.component ? `[${fields.component}]` : "";
50
+ this.opts.write(`${level.toUpperCase()} ${comp} ${msg}`);
51
+ }
52
+ }
53
+ }
54
+ let _default = null;
55
+ function getLogger() {
56
+ if (!_default) _default = new Logger({ structured: false });
57
+ return _default;
58
+ }
59
+ function configureLogger(opts) {
60
+ _default = new Logger(opts);
61
+ }
62
+ export {
63
+ Logger,
64
+ configureLogger,
65
+ getLogger
66
+ };
@@ -0,0 +1,138 @@
1
+ function formatLabels(labels) {
2
+ if (!labels) return "";
3
+ const parts = [];
4
+ for (const [k, v] of Object.entries(labels)) {
5
+ parts.push(`${k}="${String(v).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`);
6
+ }
7
+ return parts.length ? `{${parts.join(",")}}` : "";
8
+ }
9
+ function labelKey(labels) {
10
+ if (!labels) return "";
11
+ const keys = Object.keys(labels).sort();
12
+ return keys.map((k) => `${k}=${labels[k]}`).join("|");
13
+ }
14
+ class Counter {
15
+ name;
16
+ help;
17
+ buckets = /* @__PURE__ */ new Map();
18
+ constructor(name, help) {
19
+ this.name = name;
20
+ this.help = help;
21
+ }
22
+ inc(labels, value = 1) {
23
+ const key = labelKey(labels);
24
+ const existing = this.buckets.get(key);
25
+ if (existing) existing.value += value;
26
+ else this.buckets.set(key, { labels, value });
27
+ }
28
+ render() {
29
+ const lines = [];
30
+ lines.push(`# HELP ${this.name} ${this.help}`);
31
+ lines.push(`# TYPE ${this.name} counter`);
32
+ for (const { labels, value } of this.buckets.values()) {
33
+ lines.push(`${this.name}${formatLabels(labels)} ${value}`);
34
+ }
35
+ return lines.join("\n");
36
+ }
37
+ }
38
+ class Gauge {
39
+ name;
40
+ help;
41
+ buckets = /* @__PURE__ */ new Map();
42
+ constructor(name, help) {
43
+ this.name = name;
44
+ this.help = help;
45
+ }
46
+ set(value, labels) {
47
+ this.buckets.set(labelKey(labels), { labels, value });
48
+ }
49
+ render() {
50
+ const lines = [];
51
+ lines.push(`# HELP ${this.name} ${this.help}`);
52
+ lines.push(`# TYPE ${this.name} gauge`);
53
+ for (const { labels, value } of this.buckets.values()) {
54
+ lines.push(`${this.name}${formatLabels(labels)} ${value}`);
55
+ }
56
+ return lines.join("\n");
57
+ }
58
+ }
59
+ const DEFAULT_BUCKETS = [5e-3, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10];
60
+ class Histogram {
61
+ name;
62
+ help;
63
+ bucketBounds;
64
+ buckets = /* @__PURE__ */ new Map();
65
+ constructor(name, help, bucketBounds = DEFAULT_BUCKETS) {
66
+ this.name = name;
67
+ this.help = help;
68
+ this.bucketBounds = bucketBounds;
69
+ }
70
+ observe(value, labels) {
71
+ const key = labelKey(labels);
72
+ let entry = this.buckets.get(key);
73
+ if (!entry) {
74
+ entry = {
75
+ labels,
76
+ counts: new Array(this.bucketBounds.length).fill(0),
77
+ sum: 0,
78
+ total: 0
79
+ };
80
+ this.buckets.set(key, entry);
81
+ }
82
+ for (let i = 0; i < this.bucketBounds.length; i++) {
83
+ if (value <= this.bucketBounds[i]) entry.counts[i]++;
84
+ }
85
+ entry.sum += value;
86
+ entry.total++;
87
+ }
88
+ render() {
89
+ const lines = [];
90
+ lines.push(`# HELP ${this.name} ${this.help}`);
91
+ lines.push(`# TYPE ${this.name} histogram`);
92
+ for (const entry of this.buckets.values()) {
93
+ let cumulative = 0;
94
+ for (let i = 0; i < this.bucketBounds.length; i++) {
95
+ cumulative = entry.counts[i];
96
+ const labels = { ...entry.labels, le: this.bucketBounds[i] };
97
+ lines.push(`${this.name}_bucket${formatLabels(labels)} ${cumulative}`);
98
+ }
99
+ lines.push(`${this.name}_bucket${formatLabels({ ...entry.labels, le: "+Inf" })} ${entry.total}`);
100
+ lines.push(`${this.name}_sum${formatLabels(entry.labels)} ${entry.sum}`);
101
+ lines.push(`${this.name}_count${formatLabels(entry.labels)} ${entry.total}`);
102
+ }
103
+ return lines.join("\n");
104
+ }
105
+ }
106
+ class Registry {
107
+ collectors = /* @__PURE__ */ new Map();
108
+ register(metric) {
109
+ if (!this.collectors.has(metric.name)) this.collectors.set(metric.name, metric);
110
+ return this.collectors.get(metric.name);
111
+ }
112
+ render() {
113
+ const parts = [];
114
+ for (const c of this.collectors.values()) parts.push(c.render());
115
+ return parts.join("\n") + "\n";
116
+ }
117
+ reset() {
118
+ this.collectors.clear();
119
+ }
120
+ }
121
+ const registry = new Registry();
122
+ const metrics = {
123
+ queriesTotal: registry.register(new Counter("code_search_queries_total", "Total search queries by mode + repo.")),
124
+ queryDuration: registry.register(new Histogram("code_search_query_duration_seconds", "Query latency by mode.")),
125
+ indexChunks: registry.register(new Gauge("code_search_index_chunks_total", "Indexed chunks by repo.")),
126
+ indexAge: registry.register(new Gauge("code_search_index_age_seconds", "Seconds since last index by repo.")),
127
+ embeddingCalls: registry.register(new Counter("code_search_embeddings_calls_total", "Embedding API calls by provider.")),
128
+ llmCalls: registry.register(new Counter("code_search_llm_calls_total", "LLM API calls by phase.")),
129
+ errors: registry.register(new Counter("code_search_errors_total", "Errors by kind.")),
130
+ rerankerCacheHits: registry.register(new Counter("code_search_reranker_cache_hits_total", "Rerank cache lookups by outcome."))
131
+ };
132
+ export {
133
+ Counter,
134
+ Gauge,
135
+ Histogram,
136
+ metrics,
137
+ registry
138
+ };
@@ -0,0 +1,146 @@
1
+ import { readdirSync, existsSync, statSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import {
4
+ resolveCodeSearchConfig
5
+ } from "../core/config.js";
6
+ const DEFAULT_QUOTAS = {
7
+ maxQueriesPerMinute: 100,
8
+ maxEmbeddingCostUsd: 0,
9
+ maxLlmCostUsd: 0
10
+ };
11
+ function parseSimpleYaml(src) {
12
+ const obj = {};
13
+ const lines = src.split(/\r?\n/);
14
+ let i = 0;
15
+ while (i < lines.length) {
16
+ const line = lines[i].replace(/#.*$/, "").replace(/\s+$/, "");
17
+ if (!line.trim()) {
18
+ i++;
19
+ continue;
20
+ }
21
+ const top = /^(\w+):\s*(.*)$/.exec(line);
22
+ if (!top) {
23
+ i++;
24
+ continue;
25
+ }
26
+ const key = top[1];
27
+ const valRaw = top[2];
28
+ if (!valRaw) {
29
+ const sub = {};
30
+ i++;
31
+ while (i < lines.length) {
32
+ const sline = lines[i].replace(/#.*$/, "").replace(/\s+$/, "");
33
+ if (!sline) {
34
+ i++;
35
+ continue;
36
+ }
37
+ const indent = sline.length - sline.trimStart().length;
38
+ if (indent < 2) break;
39
+ const m = /^\s*(\w+):\s*(.*)$/.exec(sline);
40
+ if (!m) {
41
+ i++;
42
+ continue;
43
+ }
44
+ sub[m[1]] = coerce(m[2]);
45
+ i++;
46
+ }
47
+ obj[key] = sub;
48
+ } else {
49
+ obj[key] = coerce(valRaw);
50
+ i++;
51
+ }
52
+ }
53
+ return obj;
54
+ }
55
+ function coerce(raw) {
56
+ let v = raw.trim();
57
+ if (v.startsWith('"') && v.endsWith('"') || v.startsWith("'") && v.endsWith("'")) return v.slice(1, -1);
58
+ if (v === "true") return true;
59
+ if (v === "false") return false;
60
+ if (/^-?\d+$/.test(v)) return parseInt(v, 10);
61
+ if (/^-?\d+\.\d+$/.test(v)) return parseFloat(v);
62
+ if (v.startsWith("[") && v.endsWith("]")) return v.slice(1, -1).split(",").map((s) => coerce(s.trim()));
63
+ return v;
64
+ }
65
+ function readProjectYaml(path) {
66
+ if (!existsSync(path)) return {};
67
+ try {
68
+ return parseSimpleYaml(readFileSync(path, "utf-8"));
69
+ } catch {
70
+ return {};
71
+ }
72
+ }
73
+ class ProjectRegistry {
74
+ constructor(dataDir) {
75
+ this.dataDir = dataDir;
76
+ }
77
+ projects = /* @__PURE__ */ new Map();
78
+ /** Re-read the projects/ directory. */
79
+ reload() {
80
+ this.projects.clear();
81
+ const projectsDir = join(this.dataDir, "projects");
82
+ if (!existsSync(projectsDir)) return [];
83
+ for (const name of readdirSync(projectsDir)) {
84
+ const dir = join(projectsDir, name);
85
+ try {
86
+ if (!statSync(dir).isDirectory()) continue;
87
+ } catch {
88
+ continue;
89
+ }
90
+ const yamlPath = join(dir, "project.yaml");
91
+ const meta = readProjectYaml(yamlPath);
92
+ if (!meta.workspace) continue;
93
+ const workspaceDir = String(meta.workspace);
94
+ const quotas = {
95
+ maxQueriesPerMinute: meta.quotas?.max_queries_per_minute ?? meta.quotas?.maxQueriesPerMinute ?? DEFAULT_QUOTAS.maxQueriesPerMinute,
96
+ maxEmbeddingCostUsd: meta.quotas?.max_embedding_cost_usd ?? meta.quotas?.maxEmbeddingCostUsd ?? DEFAULT_QUOTAS.maxEmbeddingCostUsd,
97
+ maxLlmCostUsd: meta.quotas?.max_llm_cost_usd ?? meta.quotas?.maxLlmCostUsd ?? DEFAULT_QUOTAS.maxLlmCostUsd
98
+ };
99
+ const config = resolveCodeSearchConfig({ workspaceDir });
100
+ this.projects.set(name, {
101
+ name,
102
+ workspaceDir,
103
+ repos: Array.isArray(meta.repos) ? meta.repos : [],
104
+ scopes: Array.isArray(meta.scopes) ? meta.scopes : [],
105
+ config,
106
+ quotas
107
+ });
108
+ }
109
+ return [...this.projects.values()];
110
+ }
111
+ list() {
112
+ return [...this.projects.values()];
113
+ }
114
+ get(name) {
115
+ return this.projects.get(name);
116
+ }
117
+ }
118
+ function projectAccessAllowed(project, identityScopes) {
119
+ if (project.scopes.length === 0) return true;
120
+ if (identityScopes.includes("*")) return true;
121
+ for (const s of identityScopes) {
122
+ if (project.scopes.includes(s)) return true;
123
+ }
124
+ return false;
125
+ }
126
+ const projectRateBuckets = /* @__PURE__ */ new Map();
127
+ function checkProjectQuota(project, identity) {
128
+ if (project.quotas.maxQueriesPerMinute <= 0) return { ok: true };
129
+ const key = `${identity}|${project.name}`;
130
+ const now = Date.now();
131
+ const bucket = projectRateBuckets.get(key);
132
+ if (!bucket || now > bucket.resetAt) {
133
+ projectRateBuckets.set(key, { count: 1, resetAt: now + 6e4 });
134
+ return { ok: true };
135
+ }
136
+ if (bucket.count >= project.quotas.maxQueriesPerMinute) {
137
+ return { ok: false, reason: `quota exceeded for project="${project.name}": ${project.quotas.maxQueriesPerMinute}/min` };
138
+ }
139
+ bucket.count++;
140
+ return { ok: true };
141
+ }
142
+ export {
143
+ ProjectRegistry,
144
+ checkProjectQuota,
145
+ projectAccessAllowed
146
+ };
package/dist/server.js CHANGED
@@ -16,6 +16,7 @@ import { registerResources, handleResource } from "./resources/resources.js";
16
16
  import { getKnowledgeBasePath } from "@esankhan3/anvil-knowledge-core";
17
17
  import { indexFromPath } from "@esankhan3/anvil-knowledge-core";
18
18
  import { loadServerConfig } from "./core/env-config.js";
19
+ import { toKnowledgeConfig } from "./core/config.js";
19
20
  import { startHttpTransport } from "./transports/http-transport.js";
20
21
  function createMcpServerInstance(ctx) {
21
22
  const server = new Server(
@@ -185,9 +186,12 @@ async function trackedIndex(ctx, project, dirPath, opts) {
185
186
  ctx.indexing.startedAt = Date.now();
186
187
  ctx.indexing.error = null;
187
188
  pushHistory(ctx, "start", `${label}: started for "${project}" at ${dirPath}`);
189
+ const unified = loadServerConfig().__unified;
190
+ const knowledgeConfig = toKnowledgeConfig(unified);
188
191
  try {
189
192
  const stats = await indexFromPath(project, dirPath, {
190
193
  force: opts?.force,
194
+ config: knowledgeConfig,
191
195
  onProgress: (m) => {
192
196
  ctx.indexing.message = m;
193
197
  console.error(`[${label}] ${m}`);
@@ -2,9 +2,11 @@ import { createServer } from "node:http";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
4
4
  import { createAuthMiddleware } from "../middleware/auth.js";
5
+ import { registry, metrics } from "../observability/metrics.js";
5
6
  async function startHttpTransport(opts) {
6
7
  const { config, createMcpServer, onReady, getHealth } = opts;
7
8
  const authenticate = createAuthMiddleware(config);
9
+ const startedAt = Date.now();
8
10
  const sessions = /* @__PURE__ */ new Map();
9
11
  const SESSION_TTL_MS = 30 * 60 * 1e3;
10
12
  const MAX_SESSIONS = 100;
@@ -37,6 +39,42 @@ async function startHttpTransport(opts) {
37
39
  res.end(JSON.stringify(status, null, 2));
38
40
  return;
39
41
  }
42
+ if (path === "/version" && req.method === "GET") {
43
+ res.writeHead(200, { "Content-Type": "application/json" });
44
+ res.end(JSON.stringify({
45
+ name: "code-search-mcp",
46
+ version: "0.1.0",
47
+ node: process.version,
48
+ platform: process.platform
49
+ }));
50
+ return;
51
+ }
52
+ if (path === "/ready" && req.method === "GET") {
53
+ const health = getHealth?.() ?? {};
54
+ const ready = health.indexReady === true;
55
+ res.writeHead(ready ? 200 : 503, { "Content-Type": "application/json" });
56
+ res.end(JSON.stringify({ ready, ...health }));
57
+ return;
58
+ }
59
+ if (path === "/metrics" && req.method === "GET") {
60
+ const status = opts.getStatus?.() ?? {};
61
+ const indexing = status.indexing;
62
+ metrics.indexAge.set(0, { phase: indexing?.phase ?? "idle" });
63
+ res.writeHead(200, { "Content-Type": "text/plain; version=0.0.4" });
64
+ res.end(registry.render());
65
+ return;
66
+ }
67
+ if (path === "/admin/api/status" && req.method === "GET") {
68
+ const status = opts.getStatus?.() ?? {};
69
+ res.writeHead(200, { "Content-Type": "application/json" });
70
+ res.end(JSON.stringify({
71
+ version: "0.1.0",
72
+ uptime: Math.floor((Date.now() - startedAt) / 1e3),
73
+ sessions: sessions.size,
74
+ ...status
75
+ }, null, 2));
76
+ return;
77
+ }
40
78
  if (path === "/index" && req.method === "POST") {
41
79
  if (config.authEnabled) {
42
80
  const identity = authenticate(req, res);
package/package.json CHANGED
@@ -1,27 +1,39 @@
1
1
  {
2
2
  "name": "@esankhan3/code-search-mcp",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "MCP server for multi-repo code search — GitHub org or local path, zero config",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "bin": {
8
- "code-search-mcp": "./dist/index.js"
8
+ "code-search-mcp": "./dist/index.js",
9
+ "code-search": "./dist/cli/index.js",
10
+ "code-search-daemon": "./dist/daemon/index.js"
9
11
  },
10
12
  "exports": {
11
13
  ".": "./dist/index.js"
12
14
  },
13
15
  "files": [
14
- "dist"
16
+ "dist",
17
+ "scripts/smoke"
15
18
  ],
16
19
  "scripts": {
17
20
  "build": "node build.mjs",
18
- "dev": "tsc -b --watch"
21
+ "dev": "tsc -b --watch",
22
+ "test": "node --test dist/__tests__/*.test.js",
23
+ "smoke": "bash scripts/smoke/run-smoke.sh"
19
24
  },
20
25
  "dependencies": {
21
- "@esankhan3/anvil-agent-core": "^0.1.0",
22
- "@esankhan3/anvil-knowledge-core": "^0.1.0",
26
+ "@esankhan3/anvil-agent-core": "^0.2.0",
27
+ "@esankhan3/anvil-knowledge-core": "^0.2.0",
23
28
  "@modelcontextprotocol/sdk": "^1.29.0"
24
29
  },
30
+ "optionalDependencies": {
31
+ "@lancedb/lancedb-darwin-arm64": "0.27.2",
32
+ "@lancedb/lancedb-darwin-x64": "0.27.2",
33
+ "@lancedb/lancedb-linux-x64-gnu": "0.27.2",
34
+ "@lancedb/lancedb-linux-arm64-gnu": "0.27.2",
35
+ "@lancedb/lancedb-win32-x64-msvc": "0.27.2"
36
+ },
25
37
  "devDependencies": {
26
38
  "@types/node": "^22.15.2"
27
39
  },