@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/daemon/index.js
CHANGED
|
@@ -1,63 +1,83 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { writeFileSync, existsSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { resolveCodeSearchConfig, parseCliFlags, toKnowledgeConfig } from "../core/config.js";
|
|
2
|
+
import { writeFileSync, existsSync, readFileSync, unlinkSync, mkdirSync } from "node:fs";
|
|
3
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
4
|
+
import { resolveCodeSearchConfig, parseCliFlags, toKnowledgeConfig, exportDataDir } from "../core/config.js";
|
|
5
|
+
import { validateProjectName } from "../core/project-name.js";
|
|
5
6
|
import { InProcessBackend } from "../backends/in-process.js";
|
|
6
|
-
import { daemonSocketPath } from "../backends/index.js";
|
|
7
|
+
import { DaemonBackend, daemonSocketPath } from "../backends/index.js";
|
|
8
|
+
import { configureLogger, getComponentLogger } from "../observability/logger.js";
|
|
7
9
|
import { RpcServer } from "./rpc-server.js";
|
|
8
10
|
import { Watcher } from "./watcher.js";
|
|
11
|
+
const log = getComponentLogger("code-search-daemon");
|
|
12
|
+
function usageError(msg) {
|
|
13
|
+
process.stderr.write(`code-search-daemon: ${msg}
|
|
14
|
+
`);
|
|
15
|
+
process.exit(2);
|
|
16
|
+
}
|
|
9
17
|
async function main() {
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
let
|
|
18
|
+
const { patch, rest, unknown } = parseCliFlags(process.argv.slice(2));
|
|
19
|
+
if (unknown.length) usageError(`unknown flag(s): ${unknown.join(", ")}`);
|
|
20
|
+
let workspaceArg = null;
|
|
13
21
|
for (let i = 0; i < rest.length; i++) {
|
|
14
|
-
if (rest[i] === "--workspace" && rest[i + 1])
|
|
15
|
-
|
|
16
|
-
if (!workspaceDir) {
|
|
17
|
-
process.stderr.write("code-search-daemon: missing --workspace <path>\n");
|
|
18
|
-
process.exit(2);
|
|
19
|
-
}
|
|
20
|
-
if (!existsSync(workspaceDir)) {
|
|
21
|
-
process.stderr.write(`code-search-daemon: workspace does not exist: ${workspaceDir}
|
|
22
|
-
`);
|
|
23
|
-
process.exit(2);
|
|
22
|
+
if (rest[i] === "--workspace" && rest[i + 1]) workspaceArg = rest[++i];
|
|
23
|
+
else usageError(`unexpected argument: ${rest[i]} (usage: code-search-daemon --workspace <path> [--<config.path> <value>])`);
|
|
24
24
|
}
|
|
25
|
+
if (!workspaceArg) usageError("missing --workspace <path>");
|
|
26
|
+
const workspaceDir = resolve(workspaceArg);
|
|
27
|
+
if (!existsSync(workspaceDir)) usageError(`workspace does not exist: ${workspaceDir}`);
|
|
25
28
|
const cfg = resolveCodeSearchConfig({ cli: patch, workspaceDir });
|
|
26
|
-
|
|
29
|
+
configureLogger({ structured: cfg.telemetry.structuredLogs });
|
|
30
|
+
exportDataDir(cfg);
|
|
31
|
+
let project;
|
|
32
|
+
try {
|
|
33
|
+
project = validateProjectName(basename(workspaceDir));
|
|
34
|
+
} catch (err) {
|
|
35
|
+
usageError(err.message);
|
|
36
|
+
}
|
|
27
37
|
const knowledge = toKnowledgeConfig(cfg);
|
|
38
|
+
const socketPath = daemonSocketPath(cfg.storage.dataDir, project);
|
|
39
|
+
const pidFile = join(cfg.storage.dataDir, "daemon", `${project}.pid`);
|
|
40
|
+
if (existsSync(socketPath)) {
|
|
41
|
+
const live = await new DaemonBackend({ project, workspaceDir, knowledge, preferDaemon: true, socketPath }).ping();
|
|
42
|
+
if (live) {
|
|
43
|
+
const pid = existsSync(pidFile) ? readFileSync(pidFile, "utf-8").trim() : "unknown";
|
|
44
|
+
log.error(`a daemon for project "${project}" is already running (pid ${pid}, socket ${socketPath}) \u2014 not starting another`);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
if (process.platform !== "win32") {
|
|
48
|
+
try {
|
|
49
|
+
unlinkSync(socketPath);
|
|
50
|
+
} catch {
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
28
54
|
const backend = new InProcessBackend({
|
|
29
55
|
project,
|
|
30
56
|
workspaceDir,
|
|
31
57
|
knowledge,
|
|
32
58
|
preferDaemon: false
|
|
33
59
|
});
|
|
34
|
-
|
|
35
|
-
`);
|
|
36
|
-
process.stderr.write("[code-search-daemon] building initial index...\n");
|
|
37
|
-
await backend.forceIndex();
|
|
38
|
-
process.stderr.write("[code-search-daemon] initial index ready.\n");
|
|
39
|
-
const socketPath = daemonSocketPath(cfg.storage.dataDir, project);
|
|
40
|
-
const pidFile = join(cfg.storage.dataDir, "daemon", `${project}.pid`);
|
|
60
|
+
let tail = Promise.resolve();
|
|
41
61
|
let queueDepth = 0;
|
|
42
|
-
let
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
62
|
+
let watcherRunPending = false;
|
|
63
|
+
const enqueue = (reason, run) => {
|
|
64
|
+
queueDepth++;
|
|
65
|
+
const job = tail.then(async () => {
|
|
66
|
+
log.info(`reindex started (${reason})`);
|
|
67
|
+
try {
|
|
68
|
+
const out = await run();
|
|
69
|
+
log.info(`reindex finished (${reason})`);
|
|
70
|
+
return out;
|
|
71
|
+
} catch (err) {
|
|
72
|
+
log.error(`reindex failed (${reason}): ${err instanceof Error ? err.message : err}`);
|
|
73
|
+
throw err;
|
|
74
|
+
} finally {
|
|
53
75
|
queueDepth--;
|
|
54
76
|
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
indexing = false;
|
|
60
|
-
}
|
|
77
|
+
});
|
|
78
|
+
tail = job.catch(() => {
|
|
79
|
+
});
|
|
80
|
+
return job;
|
|
61
81
|
};
|
|
62
82
|
const watcher = new Watcher({
|
|
63
83
|
workspaceDir,
|
|
@@ -65,16 +85,18 @@ async function main() {
|
|
|
65
85
|
debounceMs: cfg.indexing.debounceMs
|
|
66
86
|
});
|
|
67
87
|
watcher.on("error", (err) => {
|
|
68
|
-
|
|
69
|
-
`);
|
|
88
|
+
log.error(`watcher error: ${err instanceof Error ? err.message : err}`);
|
|
70
89
|
});
|
|
71
90
|
watcher.onBatch((batch) => {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
91
|
+
log.info(`reindex queued (${batch.changed.size} changed, ${batch.removed.size} removed)`);
|
|
92
|
+
if (watcherRunPending) return;
|
|
93
|
+
watcherRunPending = true;
|
|
94
|
+
enqueue("watcher", () => {
|
|
95
|
+
watcherRunPending = false;
|
|
96
|
+
return backend.forceIndex();
|
|
97
|
+
}).catch(() => {
|
|
98
|
+
});
|
|
76
99
|
});
|
|
77
|
-
watcher.start();
|
|
78
100
|
const startedAt = Date.now();
|
|
79
101
|
const rpc = new RpcServer({
|
|
80
102
|
socketPath,
|
|
@@ -84,36 +106,55 @@ async function main() {
|
|
|
84
106
|
const s = await backend.status();
|
|
85
107
|
return { ...s, watching: true, queueDepth };
|
|
86
108
|
},
|
|
87
|
-
"index.force":
|
|
88
|
-
await backend.forceIndex({ force: p?.force });
|
|
89
|
-
return backend.status();
|
|
90
|
-
},
|
|
109
|
+
"index.force": (p) => enqueue("rpc", () => backend.forceIndex({ force: p?.force })),
|
|
91
110
|
"index.invalidate": async (p) => {
|
|
92
|
-
await backend.invalidate(p
|
|
111
|
+
await backend.invalidate(p?.paths ?? []);
|
|
93
112
|
return { ok: true };
|
|
94
113
|
},
|
|
95
114
|
"health": () => ({ ok: true, uptime: Math.floor((Date.now() - startedAt) / 1e3) })
|
|
96
115
|
}
|
|
97
116
|
});
|
|
98
|
-
await rpc.start();
|
|
99
|
-
process.stderr.write(`[code-search-daemon] RPC listening on ${socketPath}
|
|
100
|
-
`);
|
|
101
117
|
try {
|
|
118
|
+
await rpc.start();
|
|
119
|
+
} catch (err) {
|
|
120
|
+
log.error(`cannot listen on ${socketPath}: ${err instanceof Error ? err.message : err} (another daemon for "${project}" starting?)`);
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
log.info(`RPC listening on ${socketPath}`);
|
|
124
|
+
try {
|
|
125
|
+
mkdirSync(dirname(pidFile), { recursive: true });
|
|
102
126
|
writeFileSync(pidFile, String(process.pid), "utf-8");
|
|
103
127
|
} catch {
|
|
104
128
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
129
|
+
let stopping = false;
|
|
130
|
+
const stop = async (code) => {
|
|
131
|
+
if (stopping) return;
|
|
132
|
+
stopping = true;
|
|
108
133
|
watcher.stop();
|
|
109
134
|
await rpc.stop();
|
|
110
|
-
|
|
135
|
+
try {
|
|
136
|
+
if (readFileSync(pidFile, "utf-8").trim() === String(process.pid)) unlinkSync(pidFile);
|
|
137
|
+
} catch {
|
|
138
|
+
}
|
|
139
|
+
process.exit(code);
|
|
111
140
|
};
|
|
112
|
-
|
|
113
|
-
|
|
141
|
+
for (const sig of ["SIGINT", "SIGTERM"]) {
|
|
142
|
+
process.on(sig, () => {
|
|
143
|
+
log.info(`${sig} \u2014 draining...`);
|
|
144
|
+
void stop(0);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
log.info(`starting for project="${project}", workspace=${workspaceDir}`);
|
|
148
|
+
log.info("building initial index...");
|
|
149
|
+
try {
|
|
150
|
+
await enqueue("initial", () => backend.forceIndex());
|
|
151
|
+
} catch {
|
|
152
|
+
await stop(1);
|
|
153
|
+
}
|
|
154
|
+
log.info("initial index ready.");
|
|
155
|
+
watcher.start();
|
|
114
156
|
}
|
|
115
157
|
main().catch((err) => {
|
|
116
|
-
|
|
117
|
-
`);
|
|
158
|
+
log.error(`fatal: ${err instanceof Error ? err.stack : err}`);
|
|
118
159
|
process.exit(1);
|
|
119
160
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createServer } from "node:net";
|
|
2
|
-
import {
|
|
2
|
+
import { mkdirSync, statSync } from "node:fs";
|
|
3
3
|
import { dirname } from "node:path";
|
|
4
4
|
import { EventEmitter } from "node:events";
|
|
5
5
|
class RpcServer extends EventEmitter {
|
|
@@ -7,18 +7,16 @@ class RpcServer extends EventEmitter {
|
|
|
7
7
|
handlers;
|
|
8
8
|
server = null;
|
|
9
9
|
clients = /* @__PURE__ */ new Set();
|
|
10
|
+
/** Inode of the socket file this server created — stop() unlinks only that. */
|
|
11
|
+
socketIno = null;
|
|
10
12
|
constructor(opts) {
|
|
11
13
|
super();
|
|
12
14
|
this.socketPath = opts.socketPath;
|
|
13
15
|
this.handlers = opts.handlers;
|
|
14
16
|
}
|
|
17
|
+
/** Listen on the socket. An existing socket file is NOT removed — the
|
|
18
|
+
* caller decides whether it is stale; listening then fails EADDRINUSE. */
|
|
15
19
|
async start() {
|
|
16
|
-
if (process.platform !== "win32" && existsSync(this.socketPath)) {
|
|
17
|
-
try {
|
|
18
|
-
unlinkSync(this.socketPath);
|
|
19
|
-
} catch {
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
20
|
mkdirSync(dirname(this.socketPath), { recursive: true });
|
|
23
21
|
await new Promise((resolveP, rejectP) => {
|
|
24
22
|
this.server = createServer((sock) => this.handleClient(sock));
|
|
@@ -27,6 +25,12 @@ class RpcServer extends EventEmitter {
|
|
|
27
25
|
rejectP(err);
|
|
28
26
|
});
|
|
29
27
|
this.server.listen(this.socketPath, () => {
|
|
28
|
+
if (process.platform !== "win32") {
|
|
29
|
+
try {
|
|
30
|
+
this.socketIno = statSync(this.socketPath).ino;
|
|
31
|
+
} catch {
|
|
32
|
+
}
|
|
33
|
+
}
|
|
30
34
|
this.emit("listening", this.socketPath);
|
|
31
35
|
resolveP();
|
|
32
36
|
});
|
|
@@ -41,14 +45,20 @@ class RpcServer extends EventEmitter {
|
|
|
41
45
|
}
|
|
42
46
|
this.clients.clear();
|
|
43
47
|
if (this.server) {
|
|
44
|
-
|
|
48
|
+
if (process.platform === "win32" || this.ownsSocketFile()) {
|
|
49
|
+
await new Promise((r) => this.server.close(() => r()));
|
|
50
|
+
} else {
|
|
51
|
+
this.server.unref();
|
|
52
|
+
}
|
|
45
53
|
this.server = null;
|
|
46
54
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
55
|
+
this.socketIno = null;
|
|
56
|
+
}
|
|
57
|
+
ownsSocketFile() {
|
|
58
|
+
try {
|
|
59
|
+
return this.socketIno !== null && statSync(this.socketPath).ino === this.socketIno;
|
|
60
|
+
} catch {
|
|
61
|
+
return false;
|
|
52
62
|
}
|
|
53
63
|
}
|
|
54
64
|
handleClient(sock) {
|
|
@@ -77,17 +87,27 @@ class RpcServer extends EventEmitter {
|
|
|
77
87
|
sock.write(this.errorEnvelope(null, -32700, err instanceof Error ? err.message : String(err)));
|
|
78
88
|
return;
|
|
79
89
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
90
|
+
if (Array.isArray(req)) {
|
|
91
|
+
sock.write(this.errorEnvelope(null, -32600, "Batch requests are not supported"));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (typeof req !== "object" || req === null || typeof req.method !== "string") {
|
|
95
|
+
sock.write(this.errorEnvelope(null, -32600, "Invalid request"));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const isNotification = !Object.prototype.hasOwnProperty.call(req, "id");
|
|
99
|
+
const id = req.id ?? null;
|
|
100
|
+
const handler = Object.prototype.hasOwnProperty.call(this.handlers, req.method) ? this.handlers[req.method] : void 0;
|
|
101
|
+
if (typeof handler !== "function") {
|
|
102
|
+
if (!isNotification) sock.write(this.errorEnvelope(id, -32601, `Method not found: ${req.method}`));
|
|
83
103
|
return;
|
|
84
104
|
}
|
|
85
105
|
try {
|
|
86
106
|
const result = await handler(req.params);
|
|
87
|
-
sock.write(JSON.stringify({ jsonrpc: "2.0", id
|
|
107
|
+
if (!isNotification) sock.write(JSON.stringify({ jsonrpc: "2.0", id, result }) + "\n");
|
|
88
108
|
} catch (err) {
|
|
89
109
|
const msg = err instanceof Error ? err.message : String(err);
|
|
90
|
-
sock.write(this.errorEnvelope(
|
|
110
|
+
if (!isNotification) sock.write(this.errorEnvelope(id, -32e3, msg));
|
|
91
111
|
}
|
|
92
112
|
}
|
|
93
113
|
errorEnvelope(id, code, message) {
|
package/dist/index.js
CHANGED
|
@@ -1,51 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { resolve } from "node:path";
|
|
2
|
+
import { resolve, basename, join } from "node:path";
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
12
|
-
let mode = "remote";
|
|
13
|
-
let remoteUrl = null;
|
|
14
|
-
let apiKey;
|
|
15
|
-
let source = null;
|
|
16
|
-
let projectName = null;
|
|
17
|
-
let githubToken;
|
|
18
|
-
let force = false;
|
|
19
|
-
let port;
|
|
20
|
-
let transport;
|
|
21
|
-
let authMode;
|
|
22
|
-
for (let i = 0; i < args.length; i++) {
|
|
23
|
-
if (args[i] === "--remote" && args[i + 1]) {
|
|
24
|
-
mode = "remote";
|
|
25
|
-
remoteUrl = args[++i];
|
|
26
|
-
} else if (args[i] === "--local") {
|
|
27
|
-
mode = "local";
|
|
28
|
-
if (args[i + 1] && !args[i + 1].startsWith("--")) {
|
|
29
|
-
source = args[++i];
|
|
30
|
-
}
|
|
31
|
-
} else if (args[i] === "--serve") {
|
|
32
|
-
mode = "serve";
|
|
33
|
-
} else if (args[i] === "--api-key" && args[i + 1]) {
|
|
34
|
-
apiKey = args[++i];
|
|
35
|
-
} else if (args[i] === "--project" && args[i + 1]) {
|
|
36
|
-
projectName = args[++i];
|
|
37
|
-
} else if (args[i] === "--token" && args[i + 1]) {
|
|
38
|
-
githubToken = args[++i];
|
|
39
|
-
} else if (args[i] === "--force") {
|
|
40
|
-
force = true;
|
|
41
|
-
} else if (args[i] === "--port" && args[i + 1]) {
|
|
42
|
-
port = args[++i];
|
|
43
|
-
} else if (args[i] === "--transport" && args[i + 1]) {
|
|
44
|
-
transport = args[++i];
|
|
45
|
-
} else if (args[i] === "--auth" && args[i + 1]) {
|
|
46
|
-
authMode = args[++i];
|
|
47
|
-
} else if (args[i] === "--help" || args[i] === "-h") {
|
|
48
|
-
console.error(`
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { printConfig, resolveCodeSearchConfig, parseCliFlags, exportDataDir } from "./core/config.js";
|
|
6
|
+
import { loadServerConfig } from "./core/env-config.js";
|
|
7
|
+
import { validateProjectName } from "./core/project-name.js";
|
|
8
|
+
import { configureLogger, getComponentLogger } from "./observability/logger.js";
|
|
9
|
+
const log = getComponentLogger("code-search-mcp");
|
|
10
|
+
const HELP = `
|
|
49
11
|
Code Search MCP \u2014 multi-repo code intelligence via MCP
|
|
50
12
|
|
|
51
13
|
REMOTE MODE (default \u2014 connects to a deployed server):
|
|
@@ -70,7 +32,7 @@ REMOTE MODE (default \u2014 connects to a deployed server):
|
|
|
70
32
|
}
|
|
71
33
|
}
|
|
72
34
|
|
|
73
|
-
LOCAL MODE (self-hosted \u2014 indexes repos locally):
|
|
35
|
+
LOCAL MODE (self-hosted \u2014 indexes repos locally, serves over stdio):
|
|
74
36
|
code-search-mcp --local /path/to/repos Index local directory
|
|
75
37
|
code-search-mcp --local github:org-name Clone from GitHub org
|
|
76
38
|
code-search-mcp --local --project my-project Serve existing index
|
|
@@ -78,45 +40,111 @@ LOCAL MODE (self-hosted \u2014 indexes repos locally):
|
|
|
78
40
|
Options:
|
|
79
41
|
--project <name> Project name (default: derived from source)
|
|
80
42
|
--token <token> GitHub token (or GITHUB_TOKEN env)
|
|
81
|
-
--force Force full re-index
|
|
43
|
+
--force Force a full re-index at startup
|
|
82
44
|
|
|
83
|
-
SERVER MODE (deploy as HTTP service):
|
|
84
|
-
code-search-mcp --serve
|
|
85
|
-
code-search-mcp --serve --port 3100 --auth api-key
|
|
45
|
+
SERVER MODE (deploy as HTTP service \u2014 Streamable HTTP on /mcp):
|
|
46
|
+
code-search-mcp --serve [path] Start HTTP server (project "default")
|
|
47
|
+
code-search-mcp --serve /repos --port 3100 --auth api-key
|
|
48
|
+
(--local <path> --serve and --serve --local <path> are equivalent)
|
|
86
49
|
|
|
87
50
|
Options:
|
|
88
|
-
--port <port> HTTP port (default: 3100)
|
|
89
|
-
--
|
|
90
|
-
--
|
|
51
|
+
--port <port> HTTP port (default: 3100) = --server.port
|
|
52
|
+
--host <host> Bind address = --server.host
|
|
53
|
+
--auth <mode> none | api-key | jwt = --auth.mode
|
|
54
|
+
--project <name> Project name (default: "default")
|
|
91
55
|
|
|
92
56
|
Environment:
|
|
93
|
-
CODE_SEARCH_TRANSPORT streamable-http | sse
|
|
94
57
|
CODE_SEARCH_PORT HTTP port (default: 3100)
|
|
95
58
|
CODE_SEARCH_AUTH_MODE none | api-key | jwt
|
|
96
59
|
CODE_SEARCH_AUTH_API_KEYS Comma-separated API keys
|
|
97
60
|
CODE_SEARCH_EMBEDDING_PROVIDER auto | codestral | openai | voyage | ollama | custom
|
|
98
61
|
CODE_SEARCH_EMBEDDING_API_KEY Unified embedding API key
|
|
99
62
|
CODE_SEARCH_DATA_DIR Data directory override
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
63
|
+
|
|
64
|
+
CONFIG (every mode):
|
|
65
|
+
--<dotted.path> <value> Any CodeSearchConfig leaf, e.g. --retrieval.max-chunks 12
|
|
66
|
+
--print-config Print the resolved config (secrets redacted) and exit
|
|
67
|
+
`;
|
|
68
|
+
function usageError(msg) {
|
|
69
|
+
log.error(`${msg} \u2014 see --help`);
|
|
70
|
+
process.exit(2);
|
|
71
|
+
}
|
|
72
|
+
const { patch, rest, unknown } = parseCliFlags(process.argv.slice(2));
|
|
73
|
+
if (unknown.length) usageError(`unknown flag(s): ${unknown.join(", ")}`);
|
|
74
|
+
let printOnly = false;
|
|
75
|
+
let serve = false;
|
|
76
|
+
let local = false;
|
|
77
|
+
let remoteUrl = null;
|
|
78
|
+
let apiKey;
|
|
79
|
+
let source = null;
|
|
80
|
+
let projectName = null;
|
|
81
|
+
let githubToken;
|
|
82
|
+
let force = false;
|
|
83
|
+
for (let i = 0; i < rest.length; i++) {
|
|
84
|
+
const arg = rest[i];
|
|
85
|
+
const value = () => {
|
|
86
|
+
const v = rest[i + 1];
|
|
87
|
+
if (v === void 0 || v.startsWith("--")) usageError(`${arg} needs a value`);
|
|
88
|
+
i++;
|
|
89
|
+
return v;
|
|
90
|
+
};
|
|
91
|
+
const optionalPath = () => rest[i + 1] !== void 0 && !rest[i + 1].startsWith("-") ? rest[++i] : null;
|
|
92
|
+
switch (arg) {
|
|
93
|
+
case "--print-config":
|
|
94
|
+
printOnly = true;
|
|
95
|
+
break;
|
|
96
|
+
case "--remote":
|
|
97
|
+
remoteUrl = value();
|
|
98
|
+
break;
|
|
99
|
+
case "--local":
|
|
100
|
+
local = true;
|
|
101
|
+
source = optionalPath() ?? source;
|
|
102
|
+
break;
|
|
103
|
+
case "--serve":
|
|
104
|
+
serve = true;
|
|
105
|
+
source = optionalPath() ?? source;
|
|
106
|
+
break;
|
|
107
|
+
case "--api-key":
|
|
108
|
+
apiKey = value();
|
|
109
|
+
break;
|
|
110
|
+
case "--project":
|
|
111
|
+
projectName = value();
|
|
112
|
+
break;
|
|
113
|
+
case "--token":
|
|
114
|
+
githubToken = value();
|
|
115
|
+
break;
|
|
116
|
+
case "--force":
|
|
117
|
+
force = true;
|
|
118
|
+
break;
|
|
119
|
+
case "--help":
|
|
120
|
+
case "-h":
|
|
121
|
+
process.stderr.write(HELP);
|
|
122
|
+
process.exit(0);
|
|
123
|
+
default:
|
|
124
|
+
if (arg.startsWith("-")) usageError(`unknown flag: ${arg}`);
|
|
125
|
+
if (/^https?:\/\//.test(arg)) remoteUrl = arg;
|
|
126
|
+
else source = arg;
|
|
111
127
|
}
|
|
112
128
|
}
|
|
129
|
+
const mode = serve ? "serve" : remoteUrl ? "remote" : local || source ? "local" : "remote";
|
|
130
|
+
const workspaceDir = source && !source.startsWith("github:") ? resolve(source) : process.cwd();
|
|
131
|
+
const preview = resolveCodeSearchConfig({ cli: patch, workspaceDir });
|
|
132
|
+
if (preview.server.transport === "sse") {
|
|
133
|
+
usageError("SSE transport is not supported \u2014 the server speaks Streamable HTTP on /mcp (drop --transport / CODE_SEARCH_TRANSPORT)");
|
|
134
|
+
}
|
|
135
|
+
if (mode === "serve") patch.server = { ...patch.server, transport: "streamable-http" };
|
|
136
|
+
if (printOnly) {
|
|
137
|
+
process.stdout.write(printConfig(resolveCodeSearchConfig({ cli: patch, workspaceDir })) + "\n");
|
|
138
|
+
process.exit(0);
|
|
139
|
+
}
|
|
140
|
+
configureLogger({ structured: preview.telemetry.structuredLogs });
|
|
113
141
|
(async () => {
|
|
114
142
|
try {
|
|
115
143
|
if (mode === "remote") {
|
|
116
144
|
const serverUrl = remoteUrl ?? process.env.CODE_SEARCH_SERVER ?? process.env.CODE_SEARCH_REMOTE_URL;
|
|
117
145
|
const key = apiKey ?? process.env.CODE_SEARCH_API_KEY ?? process.env.CODE_SEARCH_AUTH_API_KEY;
|
|
118
146
|
if (!serverUrl) {
|
|
119
|
-
|
|
147
|
+
log.error(`No remote server configured.
|
|
120
148
|
|
|
121
149
|
Set CODE_SEARCH_SERVER environment variable:
|
|
122
150
|
export CODE_SEARCH_SERVER=https://your-server:3100
|
|
@@ -136,14 +164,8 @@ For help:
|
|
|
136
164
|
await startRemoteProxy({ serverUrl, apiKey: key });
|
|
137
165
|
return;
|
|
138
166
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
if (authMode) process.env.CODE_SEARCH_AUTH_MODE = authMode;
|
|
142
|
-
process.env.CODE_SEARCH_TRANSPORT = transport === "sse" ? "sse" : "streamable-http";
|
|
143
|
-
const { startServer: startServer2 } = await import("./server.js");
|
|
144
|
-
await startServer2(projectName ?? "default", source ? resolve(source) : null);
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
167
|
+
const config = loadServerConfig({ cli: patch, workspaceDir });
|
|
168
|
+
exportDataDir(config.__unified);
|
|
147
169
|
let directoryPath = null;
|
|
148
170
|
if (source?.startsWith("github:")) {
|
|
149
171
|
const { cloneOrUpdateOrg } = await import("./sources/github-org.js");
|
|
@@ -151,34 +173,36 @@ For help:
|
|
|
151
173
|
const slashIdx = spec.indexOf("/");
|
|
152
174
|
const org = slashIdx > 0 ? spec.slice(0, slashIdx) : spec;
|
|
153
175
|
const pattern = slashIdx > 0 ? spec.slice(slashIdx + 1) : void 0;
|
|
154
|
-
if (!projectName) projectName = org;
|
|
155
|
-
|
|
176
|
+
if (!projectName && mode === "local") projectName = org;
|
|
177
|
+
log.info(`Cloning repos from github:${org}${pattern ? `/${pattern}` : ""}...`);
|
|
156
178
|
const repos = await cloneOrUpdateOrg(org, {
|
|
157
179
|
pattern,
|
|
158
180
|
token: githubToken,
|
|
159
|
-
onProgress: (m) =>
|
|
181
|
+
onProgress: (m) => log.info(m)
|
|
160
182
|
});
|
|
161
|
-
|
|
162
|
-
const { homedir } = await import("node:os");
|
|
163
|
-
const { join } = await import("node:path");
|
|
183
|
+
log.info(`${repos.length} repos ready`);
|
|
164
184
|
directoryPath = join(homedir(), ".code-search", org);
|
|
165
185
|
} else if (source) {
|
|
166
186
|
directoryPath = resolve(source);
|
|
167
187
|
if (!existsSync(directoryPath)) {
|
|
168
|
-
|
|
188
|
+
log.error(`Directory not found: ${directoryPath}`);
|
|
169
189
|
process.exit(1);
|
|
170
190
|
}
|
|
171
|
-
|
|
172
|
-
projectName = directoryPath.split("/").filter(Boolean).pop() || "project";
|
|
173
|
-
}
|
|
174
|
-
} else if (!projectName) {
|
|
191
|
+
} else if (local && !projectName) {
|
|
175
192
|
directoryPath = process.cwd();
|
|
176
|
-
|
|
193
|
+
}
|
|
194
|
+
if (!projectName) {
|
|
195
|
+
projectName = mode === "serve" ? "default" : directoryPath && basename(directoryPath) || "project";
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
validateProjectName(projectName);
|
|
199
|
+
} catch (err) {
|
|
200
|
+
usageError(err.message);
|
|
177
201
|
}
|
|
178
202
|
const { startServer } = await import("./server.js");
|
|
179
|
-
await startServer(projectName, directoryPath);
|
|
203
|
+
await startServer(projectName, directoryPath, { force });
|
|
180
204
|
} catch (err) {
|
|
181
|
-
|
|
205
|
+
log.error(`Fatal: ${err instanceof Error ? err.stack ?? err.message : String(err)}`);
|
|
182
206
|
process.exit(1);
|
|
183
207
|
}
|
|
184
208
|
})();
|