@esankhan3/code-search-mcp 0.4.11 → 1.0.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.
- package/README.md +8 -7
- package/dist/__tests__/http-stateless.test.js +57 -0
- package/dist/cli/index.js +4 -4
- package/dist/core/config.js +42 -3
- package/dist/core/env-config.js +7 -2
- package/dist/resources/resources.js +19 -25
- package/dist/server.js +150 -48
- package/dist/tools/graph.js +106 -134
- package/dist/tools/index-tools.js +2 -1
- package/dist/tools/profile.js +3 -2
- package/dist/tools/search.js +26 -15
- package/dist/transports/http-transport.js +10 -87
- package/dist/transports/remote-proxy.js +8 -14
- package/package.json +7 -5
package/README.md
CHANGED
|
@@ -245,10 +245,11 @@ Every index call lands in a 50-entry FIFO history with start
|
|
|
245
245
|
timestamp, success / error, last duration. Available at
|
|
246
246
|
`GET /status`.
|
|
247
247
|
|
|
248
|
-
### Streamable HTTP
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
248
|
+
### Stateless Streamable HTTP
|
|
249
|
+
Every `/mcp` request is served statelessly by a fresh Server
|
|
250
|
+
instance: modern (2026-07-28) clients negotiate via
|
|
251
|
+
`server/discover`; 2025-era clients are answered by the SDK's
|
|
252
|
+
per-request legacy fallback. No session map, no TTL.
|
|
252
253
|
|
|
253
254
|
### Admin index endpoint
|
|
254
255
|
`POST /index` with `{ project, dirPath?, opts? }` triggers a
|
|
@@ -374,9 +375,9 @@ the same router, retries, and cost ledger as the rest of Anvil.
|
|
|
374
375
|
**Three modes, one binary.** Remote proxy for hosted, local for
|
|
375
376
|
solo, serve for teams. The dispatcher is `src/index.ts:argv`.
|
|
376
377
|
|
|
377
|
-
**Stateless
|
|
378
|
-
|
|
379
|
-
|
|
378
|
+
**Stateless serving.** Every `/mcp` request is an independent
|
|
379
|
+
exchange — no session state at all, so restarts and per-request
|
|
380
|
+
proxies (cluster workers) are free.
|
|
380
381
|
|
|
381
382
|
**Security defaults that don't bite.** Default bind is `127.0.0.1`
|
|
382
383
|
when auth is `none`. API keys compared timing-safe. JWT
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { Server } from "@modelcontextprotocol/server";
|
|
4
|
+
import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
|
|
5
|
+
import { startHttpTransport } from "../transports/http-transport.js";
|
|
6
|
+
function makeMcpServer() {
|
|
7
|
+
const server = new Server(
|
|
8
|
+
{ name: "stateless-test", version: "0.0.0" },
|
|
9
|
+
{ capabilities: { tools: {} } }
|
|
10
|
+
);
|
|
11
|
+
server.setRequestHandler("tools/list", async () => ({
|
|
12
|
+
tools: [{ name: "ping", description: "test tool", inputSchema: { type: "object" } }]
|
|
13
|
+
}));
|
|
14
|
+
return server;
|
|
15
|
+
}
|
|
16
|
+
const config = {
|
|
17
|
+
port: 0,
|
|
18
|
+
host: "127.0.0.1",
|
|
19
|
+
transport: "streamable-http",
|
|
20
|
+
authEnabled: false,
|
|
21
|
+
authMode: "none",
|
|
22
|
+
authApiKeys: []
|
|
23
|
+
};
|
|
24
|
+
describe("http transport \u2014 stateless mode (cluster workers)", () => {
|
|
25
|
+
it("serves initialize + tools/list as independent sessionless requests", async () => {
|
|
26
|
+
const httpServer = await startHttpTransport({
|
|
27
|
+
config,
|
|
28
|
+
createMcpServer: async () => ({ server: makeMcpServer() })
|
|
29
|
+
});
|
|
30
|
+
const base = `http://127.0.0.1:${httpServer.address().port}`;
|
|
31
|
+
try {
|
|
32
|
+
const client = new Client({ name: "test-client", version: "1.0.0" });
|
|
33
|
+
const transport = new StreamableHTTPClientTransport(new URL(`${base}/mcp`));
|
|
34
|
+
await client.connect(transport);
|
|
35
|
+
const tools = await client.listTools();
|
|
36
|
+
assert.equal(tools.tools[0]?.name, "ping");
|
|
37
|
+
assert.equal(transport.sessionId, void 0, "stateless server must not assign a session");
|
|
38
|
+
assert.equal(client.getProtocolEra(), "legacy");
|
|
39
|
+
await client.close();
|
|
40
|
+
const modern = new Client(
|
|
41
|
+
{ name: "modern-client", version: "1.0.0" },
|
|
42
|
+
{ versionNegotiation: { mode: "auto" } }
|
|
43
|
+
);
|
|
44
|
+
await modern.connect(new StreamableHTTPClientTransport(new URL(`${base}/mcp`)));
|
|
45
|
+
assert.equal(modern.getProtocolEra(), "modern");
|
|
46
|
+
assert.equal(modern.getNegotiatedProtocolVersion(), "2026-07-28");
|
|
47
|
+
assert.equal((await modern.listTools()).tools[0]?.name, "ping");
|
|
48
|
+
await modern.close();
|
|
49
|
+
const get = await fetch(`${base}/mcp`, { method: "GET" });
|
|
50
|
+
assert.equal(get.status, 405);
|
|
51
|
+
const del = await fetch(`${base}/mcp`, { method: "DELETE" });
|
|
52
|
+
assert.equal(del.status, 405);
|
|
53
|
+
} finally {
|
|
54
|
+
httpServer.close();
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
});
|
package/dist/cli/index.js
CHANGED
|
@@ -180,6 +180,9 @@ async function cmdStatus(rest) {
|
|
|
180
180
|
const indexer = new KnowledgeIndexer();
|
|
181
181
|
const stats = await indexer.getStats(project);
|
|
182
182
|
const socketPath = daemonSocketPath(cfg.storage.dataDir, project);
|
|
183
|
+
const profilesAvailable = await Promise.all(
|
|
184
|
+
stats.repos.map(async (r) => ({ name: r.name, hasProfile: !!await loadProfile(project, r.name) }))
|
|
185
|
+
);
|
|
183
186
|
process.stdout.write(JSON.stringify({
|
|
184
187
|
project,
|
|
185
188
|
dataDir: cfg.storage.dataDir,
|
|
@@ -189,10 +192,7 @@ async function cmdStatus(rest) {
|
|
|
189
192
|
embeddingProvider: stats.embeddingProvider,
|
|
190
193
|
lastIndexedAt: stats.lastIndexed || null,
|
|
191
194
|
repos: stats.repos,
|
|
192
|
-
profilesAvailable
|
|
193
|
-
name: r.name,
|
|
194
|
-
hasProfile: !!loadProfile(project, r.name)
|
|
195
|
-
}))
|
|
195
|
+
profilesAvailable
|
|
196
196
|
}, null, 2) + "\n");
|
|
197
197
|
}
|
|
198
198
|
async function cmdReset(rest) {
|
package/dist/core/config.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { homedir } from "node:os";
|
|
3
|
+
import { homedir, tmpdir } from "node:os";
|
|
4
4
|
function defaultDataDir() {
|
|
5
5
|
return process.env.CODE_SEARCH_DATA_DIR ?? join(homedir(), ".code-search", "index");
|
|
6
6
|
}
|
|
@@ -168,6 +168,43 @@ function parseReindexMs(raw) {
|
|
|
168
168
|
}
|
|
169
169
|
return m[2] === "h" ? parseInt(m[1], 10) * 60 * 6e4 : parseInt(m[1], 10) * 6e4;
|
|
170
170
|
}
|
|
171
|
+
function envStorageBackends(env) {
|
|
172
|
+
const blobBackend = env("STORAGE_BLOB_BACKEND");
|
|
173
|
+
const mongoUri = env("STORAGE_MONGO_URI");
|
|
174
|
+
const mongoDb = env("STORAGE_MONGO_DB");
|
|
175
|
+
const vectorUri = env("STORAGE_VECTOR_URI");
|
|
176
|
+
const s3Endpoint = env("STORAGE_S3_ENDPOINT");
|
|
177
|
+
const s3Region = env("STORAGE_S3_REGION");
|
|
178
|
+
const s3AllowHttp = env("STORAGE_S3_ALLOW_HTTP");
|
|
179
|
+
const cacheMaxBytes = env("STORAGE_CACHE_MAX_BYTES");
|
|
180
|
+
const cacheDir = env("STORAGE_CACHE_DIR");
|
|
181
|
+
if (!blobBackend && !mongoUri && !vectorUri && !cacheMaxBytes && !cacheDir) return void 0;
|
|
182
|
+
const out = {};
|
|
183
|
+
if (blobBackend || mongoUri) {
|
|
184
|
+
out.blob = {
|
|
185
|
+
...blobBackend ? { backend: blobBackend } : {},
|
|
186
|
+
...mongoUri && mongoDb ? { mongo: { uri: mongoUri, db: mongoDb } } : {}
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
if (vectorUri) {
|
|
190
|
+
const s3 = s3Endpoint || s3Region || s3AllowHttp ? {
|
|
191
|
+
...s3Endpoint ? { endpoint: s3Endpoint } : {},
|
|
192
|
+
...s3Region ? { region: s3Region } : {},
|
|
193
|
+
...s3AllowHttp ? { allowHttp: s3AllowHttp === "true" || s3AllowHttp === "1" } : {},
|
|
194
|
+
virtualHostedStyle: false
|
|
195
|
+
// MinIO is path-style
|
|
196
|
+
} : void 0;
|
|
197
|
+
out.vector = { lancedb: { uri: vectorUri, ...s3 ? { s3 } : {} } };
|
|
198
|
+
}
|
|
199
|
+
const max = cacheMaxBytes ? parseInt(cacheMaxBytes, 10) : NaN;
|
|
200
|
+
if (cacheDir || Number.isFinite(max) && max > 0) {
|
|
201
|
+
out.cache = {
|
|
202
|
+
dir: cacheDir ?? join(tmpdir(), "code-search-cache"),
|
|
203
|
+
...Number.isFinite(max) && max > 0 ? { maxBytes: max, mode: "ram" } : {}
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
return out;
|
|
207
|
+
}
|
|
171
208
|
function envLayer() {
|
|
172
209
|
const env = (k) => process.env[`CODE_SEARCH_${k}`];
|
|
173
210
|
const authMode = env("AUTH_MODE");
|
|
@@ -193,7 +230,8 @@ function envLayer() {
|
|
|
193
230
|
rateLimitPerMinute: rateLimit ? parseInt(rateLimit, 10) : void 0
|
|
194
231
|
},
|
|
195
232
|
storage: {
|
|
196
|
-
dataDir: env("DATA_DIR")
|
|
233
|
+
dataDir: env("DATA_DIR"),
|
|
234
|
+
backends: envStorageBackends(env)
|
|
197
235
|
},
|
|
198
236
|
embedding: {
|
|
199
237
|
provider: env("EMBEDDING_PROVIDER"),
|
|
@@ -331,7 +369,8 @@ function toKnowledgeConfig(c) {
|
|
|
331
369
|
hybridWeights: { ...c.retrieval.hybridWeights },
|
|
332
370
|
reranker: { ...c.reranker }
|
|
333
371
|
},
|
|
334
|
-
autoIndex: c.indexing.autoIndex
|
|
372
|
+
autoIndex: c.indexing.autoIndex,
|
|
373
|
+
...c.storage.backends ? { storage: c.storage.backends } : {}
|
|
335
374
|
};
|
|
336
375
|
}
|
|
337
376
|
const SECRET_KEYS = /* @__PURE__ */ new Set([
|
package/dist/core/env-config.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { execSync as _execSync } from "node:child_process";
|
|
2
2
|
import {
|
|
3
3
|
resolveCodeSearchConfig,
|
|
4
|
-
parseCliFlags
|
|
4
|
+
parseCliFlags,
|
|
5
|
+
toKnowledgeConfig
|
|
5
6
|
} from "./config.js";
|
|
6
7
|
import { normalizeRerankerConfig } from "@esankhan3/anvil-knowledge-core";
|
|
7
8
|
let _config = null;
|
|
@@ -26,6 +27,9 @@ function resolveLlmMode(explicit, apiKey, claudeBin) {
|
|
|
26
27
|
);
|
|
27
28
|
return "none";
|
|
28
29
|
}
|
|
30
|
+
function resolvedKnowledgeConfig() {
|
|
31
|
+
return toKnowledgeConfig(loadServerConfig().__unified);
|
|
32
|
+
}
|
|
29
33
|
function loadServerConfig(opts) {
|
|
30
34
|
if (_config) return _config;
|
|
31
35
|
const cli = opts?.argv ? parseCliFlags(opts.argv).patch : void 0;
|
|
@@ -84,5 +88,6 @@ function resetServerConfig() {
|
|
|
84
88
|
}
|
|
85
89
|
export {
|
|
86
90
|
loadServerConfig,
|
|
87
|
-
resetServerConfig
|
|
91
|
+
resetServerConfig,
|
|
92
|
+
resolvedKnowledgeConfig
|
|
88
93
|
};
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { getKnowledgeBasePath, openSystemGraphStore } from "@esankhan3/anvil-knowledge-core";
|
|
1
|
+
import { getBlobStore, getGraphStore } from "@esankhan3/anvil-knowledge-core";
|
|
4
2
|
import { loadAllProfiles } from "@esankhan3/anvil-knowledge-core";
|
|
3
|
+
import { resolvedKnowledgeConfig } from "../core/env-config.js";
|
|
5
4
|
import { loadProfile } from "@esankhan3/anvil-knowledge-core";
|
|
6
5
|
function registerResources(ctx) {
|
|
7
6
|
return [
|
|
@@ -21,9 +20,8 @@ function registerResources(ctx) {
|
|
|
21
20
|
}
|
|
22
21
|
async function handleResource(uri, ctx) {
|
|
23
22
|
try {
|
|
24
|
-
const kbPath = getKnowledgeBasePath(ctx.projectName);
|
|
25
23
|
if (uri === "code-search://repos") {
|
|
26
|
-
const profiles = loadAllProfiles(ctx.projectName);
|
|
24
|
+
const profiles = await loadAllProfiles(ctx.projectName, resolvedKnowledgeConfig());
|
|
27
25
|
return {
|
|
28
26
|
contents: [{
|
|
29
27
|
uri,
|
|
@@ -33,30 +31,26 @@ async function handleResource(uri, ctx) {
|
|
|
33
31
|
};
|
|
34
32
|
}
|
|
35
33
|
if (uri === "code-search://system-graph") {
|
|
36
|
-
const store = await
|
|
34
|
+
const store = await getGraphStore(ctx.projectName, resolvedKnowledgeConfig());
|
|
37
35
|
if (!store) {
|
|
38
36
|
return { contents: [{ uri, mimeType: "application/json", text: '{"nodes":[],"crossRepoEdges":[],"totals":{"nodes":0,"crossRepoEdges":0}}' }] };
|
|
39
37
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return { contents: [{ uri, mimeType: "application/json", text: JSON.stringify(payload, null, 2) }] };
|
|
53
|
-
} finally {
|
|
54
|
-
store.close();
|
|
55
|
-
}
|
|
38
|
+
const NODE_LIMIT = 200;
|
|
39
|
+
const EDGE_LIMIT = 200;
|
|
40
|
+
const { rows: nodes, total: nodeTotal } = store.searchNodes({ minDegree: 0 }, NODE_LIMIT);
|
|
41
|
+
const { edges: crossRepoEdges, total: edgeTotal } = store.crossRepoEdges(void 0, EDGE_LIMIT);
|
|
42
|
+
const payload = {
|
|
43
|
+
nodes,
|
|
44
|
+
crossRepoEdges,
|
|
45
|
+
totals: { nodes: nodeTotal, crossRepoEdges: edgeTotal },
|
|
46
|
+
truncated: nodeTotal > nodes.length || edgeTotal > crossRepoEdges.length,
|
|
47
|
+
note: "Bounded overview (top nodes by degree + cross-repo edges). Use the graph tools for full traversal."
|
|
48
|
+
};
|
|
49
|
+
return { contents: [{ uri, mimeType: "application/json", text: JSON.stringify(payload, null, 2) }] };
|
|
56
50
|
}
|
|
57
51
|
const profileMatch = uri.match(/^code-search:\/\/repo\/([^/]+)\/profile$/);
|
|
58
52
|
if (profileMatch) {
|
|
59
|
-
const profile = loadProfile(ctx.projectName, profileMatch[1]);
|
|
53
|
+
const profile = await loadProfile(ctx.projectName, profileMatch[1], resolvedKnowledgeConfig());
|
|
60
54
|
return {
|
|
61
55
|
contents: [{
|
|
62
56
|
uri,
|
|
@@ -67,12 +61,12 @@ async function handleResource(uri, ctx) {
|
|
|
67
61
|
}
|
|
68
62
|
const graphMatch = uri.match(/^code-search:\/\/repo\/([^/]+)\/graph$/);
|
|
69
63
|
if (graphMatch) {
|
|
70
|
-
const
|
|
64
|
+
const graphJson = await getBlobStore(ctx.projectName, resolvedKnowledgeConfig()).getText(`${graphMatch[1]}/graph.json`);
|
|
71
65
|
return {
|
|
72
66
|
contents: [{
|
|
73
67
|
uri,
|
|
74
68
|
mimeType: "application/json",
|
|
75
|
-
text:
|
|
69
|
+
text: graphJson ?? '{"nodes":[],"links":[]}'
|
|
76
70
|
}]
|
|
77
71
|
};
|
|
78
72
|
}
|
package/dist/server.js
CHANGED
|
@@ -1,26 +1,21 @@
|
|
|
1
|
-
import { Server } from "@modelcontextprotocol/
|
|
2
|
-
import { StdioServerTransport } from "@modelcontextprotocol/
|
|
3
|
-
import {
|
|
4
|
-
CallToolRequestSchema,
|
|
5
|
-
ListToolsRequestSchema,
|
|
6
|
-
ListResourcesRequestSchema,
|
|
7
|
-
ReadResourceRequestSchema
|
|
8
|
-
} from "@modelcontextprotocol/sdk/types.js";
|
|
1
|
+
import { Server } from "@modelcontextprotocol/server";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
|
|
9
3
|
import { existsSync } from "node:fs";
|
|
10
|
-
import {
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import cluster from "node:cluster";
|
|
11
6
|
import { registerSearchTools, handleSearchTool } from "./tools/search.js";
|
|
12
7
|
import { registerGraphTools, handleGraphTool } from "./tools/graph.js";
|
|
13
8
|
import { registerProfileTools, handleProfileTool } from "./tools/profile.js";
|
|
14
9
|
import { registerIndexTools, handleIndexTool } from "./tools/index-tools.js";
|
|
15
10
|
import { registerResources, handleResource } from "./resources/resources.js";
|
|
16
|
-
import {
|
|
11
|
+
import { getBlobStore } from "@esankhan3/anvil-knowledge-core";
|
|
17
12
|
import { indexFromPath, invalidateRetriever } from "@esankhan3/anvil-knowledge-core";
|
|
18
13
|
import { loadServerConfig } from "./core/env-config.js";
|
|
19
14
|
import { toKnowledgeConfig } from "./core/config.js";
|
|
20
15
|
import { startHttpTransport } from "./transports/http-transport.js";
|
|
21
16
|
function createMcpServerInstance(ctx) {
|
|
22
17
|
const server = new Server(
|
|
23
|
-
{ name: "code-search-mcp", version: "0.
|
|
18
|
+
{ name: "code-search-mcp", version: "1.0.0" },
|
|
24
19
|
{ capabilities: { tools: {}, resources: {} } }
|
|
25
20
|
);
|
|
26
21
|
const allTools = [
|
|
@@ -29,10 +24,10 @@ function createMcpServerInstance(ctx) {
|
|
|
29
24
|
...registerProfileTools(),
|
|
30
25
|
...registerIndexTools()
|
|
31
26
|
];
|
|
32
|
-
server.setRequestHandler(
|
|
27
|
+
server.setRequestHandler("tools/list", async () => ({
|
|
33
28
|
tools: allTools
|
|
34
29
|
}));
|
|
35
|
-
server.setRequestHandler(
|
|
30
|
+
server.setRequestHandler("tools/call", async (request) => {
|
|
36
31
|
const name = request.params.name;
|
|
37
32
|
const args = request.params.arguments ?? {};
|
|
38
33
|
const searchResult = await handleSearchTool(name, args, ctx);
|
|
@@ -46,10 +41,10 @@ function createMcpServerInstance(ctx) {
|
|
|
46
41
|
return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
|
|
47
42
|
});
|
|
48
43
|
const allResources = registerResources(ctx);
|
|
49
|
-
server.setRequestHandler(
|
|
44
|
+
server.setRequestHandler("resources/list", async () => ({
|
|
50
45
|
resources: allResources
|
|
51
46
|
}));
|
|
52
|
-
server.setRequestHandler(
|
|
47
|
+
server.setRequestHandler("resources/read", async (request) => {
|
|
53
48
|
return handleResource(request.params.uri, ctx);
|
|
54
49
|
});
|
|
55
50
|
return server;
|
|
@@ -75,17 +70,25 @@ async function startServer(projectName, directoryPath) {
|
|
|
75
70
|
};
|
|
76
71
|
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}`;
|
|
77
72
|
console.error(`[code-search-mcp] LLM: ${llmInfo}`);
|
|
78
|
-
await autoIndex(ctx);
|
|
73
|
+
await autoIndex(ctx, { readOnly: cluster.isWorker });
|
|
79
74
|
if (config.transport === "stdio") {
|
|
80
75
|
const server = createMcpServerInstance(ctx);
|
|
81
76
|
const transport = new StdioServerTransport();
|
|
82
77
|
await server.connect(transport);
|
|
83
78
|
console.error(`[code-search-mcp] Server running for "${projectName}" (stdio)`);
|
|
84
79
|
} else {
|
|
80
|
+
const workerCount = parseWorkerCount();
|
|
81
|
+
if (workerCount > 1 && cluster.isPrimary) {
|
|
82
|
+
armScheduledReindex(ctx, () => broadcastInvalidate(ctx));
|
|
83
|
+
runClusterPrimary(ctx, workerCount);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
85
86
|
if (config.authMode === "none" && config.host !== "127.0.0.1" && config.host !== "localhost") {
|
|
86
87
|
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.`);
|
|
87
88
|
console.error(`[code-search-mcp] Set CODE_SEARCH_AUTH_MODE=api-key or CODE_SEARCH_HOST=127.0.0.1 for security.`);
|
|
88
89
|
}
|
|
90
|
+
const isReadOnlyWorker = cluster.isWorker;
|
|
91
|
+
if (isReadOnlyWorker) registerWorkerIpc(ctx);
|
|
89
92
|
await startHttpTransport({
|
|
90
93
|
config,
|
|
91
94
|
createMcpServer: async () => ({
|
|
@@ -116,35 +119,12 @@ async function startServer(projectName, directoryPath) {
|
|
|
116
119
|
elapsedMs: ctx.indexing.startedAt ? Date.now() - ctx.indexing.startedAt : null
|
|
117
120
|
}
|
|
118
121
|
}),
|
|
119
|
-
onIndex:
|
|
120
|
-
const { resolve } = await import("node:path");
|
|
121
|
-
const { existsSync: existsSync2 } = await import("node:fs");
|
|
122
|
-
const dirPath = resolve(body.path);
|
|
123
|
-
if (!existsSync2(dirPath)) {
|
|
124
|
-
throw new Error(`Path does not exist: ${dirPath}`);
|
|
125
|
-
}
|
|
126
|
-
if (ctx.indexing.status === "indexing") {
|
|
127
|
-
throw new Error(`Indexing already in progress (phase: ${ctx.indexing.phase}). Wait for it to complete or check GET /status.`);
|
|
128
|
-
}
|
|
129
|
-
const project = body.project || dirPath.split("/").filter(Boolean).pop() || "project";
|
|
130
|
-
const stats = await trackedIndex(ctx, project, dirPath, {
|
|
131
|
-
force: body.force,
|
|
132
|
-
label: "admin-index"
|
|
133
|
-
});
|
|
134
|
-
ctx.projectName = project;
|
|
135
|
-
ctx.directoryPath = dirPath;
|
|
136
|
-
return {
|
|
137
|
-
status: "ok",
|
|
138
|
-
project,
|
|
139
|
-
path: dirPath,
|
|
140
|
-
chunks: stats.totalChunks,
|
|
141
|
-
repos: stats.repos.length,
|
|
142
|
-
crossRepoEdges: stats.crossRepoEdges,
|
|
143
|
-
durationMs: stats.indexDurationMs
|
|
144
|
-
};
|
|
145
|
-
}
|
|
122
|
+
onIndex: (body) => isReadOnlyWorker ? forwardIndexToPrimary(body) : runAdminIndex(ctx, body)
|
|
146
123
|
});
|
|
147
124
|
}
|
|
125
|
+
if (!cluster.isWorker) armScheduledReindex(ctx);
|
|
126
|
+
}
|
|
127
|
+
function armScheduledReindex(ctx, onSuccess) {
|
|
148
128
|
const reindexIntervalMs = parseReindexInterval();
|
|
149
129
|
if (reindexIntervalMs > 0 && ctx.directoryPath) {
|
|
150
130
|
console.error(`[code-search-mcp] Auto-reindex every ${Math.round(reindexIntervalMs / 6e4)}m`);
|
|
@@ -152,12 +132,128 @@ async function startServer(projectName, directoryPath) {
|
|
|
152
132
|
if (!ctx.directoryPath || ctx.indexing.status === "indexing") return;
|
|
153
133
|
try {
|
|
154
134
|
await trackedIndex(ctx, ctx.projectName, ctx.directoryPath, { label: "auto-reindex" });
|
|
135
|
+
onSuccess?.();
|
|
155
136
|
} catch (err) {
|
|
156
137
|
console.error(`[auto-reindex] Failed:`, err);
|
|
157
138
|
}
|
|
158
139
|
}, reindexIntervalMs).unref();
|
|
159
140
|
}
|
|
160
141
|
}
|
|
142
|
+
async function runAdminIndex(ctx, body) {
|
|
143
|
+
const dirPath = resolve(body.path);
|
|
144
|
+
if (!existsSync(dirPath)) {
|
|
145
|
+
throw new Error(`Path does not exist: ${dirPath}`);
|
|
146
|
+
}
|
|
147
|
+
if (ctx.indexing.status === "indexing") {
|
|
148
|
+
throw new Error(`Indexing already in progress (phase: ${ctx.indexing.phase}). Wait for it to complete or check GET /status.`);
|
|
149
|
+
}
|
|
150
|
+
const project = body.project || dirPath.split("/").filter(Boolean).pop() || "project";
|
|
151
|
+
const stats = await trackedIndex(ctx, project, dirPath, {
|
|
152
|
+
force: body.force,
|
|
153
|
+
label: "admin-index"
|
|
154
|
+
});
|
|
155
|
+
ctx.projectName = project;
|
|
156
|
+
ctx.directoryPath = dirPath;
|
|
157
|
+
return {
|
|
158
|
+
status: "ok",
|
|
159
|
+
project,
|
|
160
|
+
path: dirPath,
|
|
161
|
+
chunks: stats.totalChunks,
|
|
162
|
+
repos: stats.repos.length,
|
|
163
|
+
crossRepoEdges: stats.crossRepoEdges,
|
|
164
|
+
durationMs: stats.indexDurationMs
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function broadcastInvalidate(ctx) {
|
|
168
|
+
const msg = { type: "csm:invalidate", project: ctx.projectName, directoryPath: ctx.directoryPath };
|
|
169
|
+
for (const w of Object.values(cluster.workers ?? {})) {
|
|
170
|
+
try {
|
|
171
|
+
w?.send(msg);
|
|
172
|
+
} catch {
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function parseWorkerCount() {
|
|
177
|
+
const n = parseInt(process.env.CODE_SEARCH_WORKERS ?? "", 10);
|
|
178
|
+
return Number.isFinite(n) && n > 1 ? Math.min(n, 16) : 1;
|
|
179
|
+
}
|
|
180
|
+
function runClusterPrimary(ctx, workerCount) {
|
|
181
|
+
console.error(`[code-search-mcp] Cluster primary ${process.pid}: forking ${workerCount} read-only workers (stateless MCP)`);
|
|
182
|
+
let shuttingDown = false;
|
|
183
|
+
const send = (w, msg) => {
|
|
184
|
+
try {
|
|
185
|
+
w?.send(msg);
|
|
186
|
+
} catch {
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
cluster.on("message", (worker, msg) => {
|
|
190
|
+
if (msg?.type !== "csm:index") return;
|
|
191
|
+
void (async () => {
|
|
192
|
+
try {
|
|
193
|
+
const result = await runAdminIndex(ctx, msg.body);
|
|
194
|
+
broadcastInvalidate(ctx);
|
|
195
|
+
send(worker, { type: "csm:index-result", id: msg.id, ok: true, result });
|
|
196
|
+
} catch (err) {
|
|
197
|
+
send(worker, {
|
|
198
|
+
type: "csm:index-result",
|
|
199
|
+
id: msg.id,
|
|
200
|
+
ok: false,
|
|
201
|
+
error: err instanceof Error ? err.message : String(err)
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
})();
|
|
205
|
+
});
|
|
206
|
+
cluster.on("exit", (worker, code, signal) => {
|
|
207
|
+
if (shuttingDown) return;
|
|
208
|
+
console.error(`[code-search-mcp] Worker ${worker.process.pid} exited (${signal ?? code}) \u2014 respawning`);
|
|
209
|
+
cluster.fork();
|
|
210
|
+
});
|
|
211
|
+
const shutdown = (sig) => {
|
|
212
|
+
shuttingDown = true;
|
|
213
|
+
for (const w of Object.values(cluster.workers ?? {})) {
|
|
214
|
+
try {
|
|
215
|
+
w?.kill(sig);
|
|
216
|
+
} catch {
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
process.exit(0);
|
|
220
|
+
};
|
|
221
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
222
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
223
|
+
for (let i = 0; i < workerCount; i++) cluster.fork();
|
|
224
|
+
}
|
|
225
|
+
let ipcSeq = 0;
|
|
226
|
+
const pendingIndexRequests = /* @__PURE__ */ new Map();
|
|
227
|
+
function forwardIndexToPrimary(body) {
|
|
228
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
229
|
+
if (!process.send) {
|
|
230
|
+
rejectPromise(new Error("IPC channel unavailable \u2014 not a cluster worker"));
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const id = ++ipcSeq;
|
|
234
|
+
pendingIndexRequests.set(id, { resolve: resolvePromise, reject: rejectPromise });
|
|
235
|
+
const msg = { type: "csm:index", id, body };
|
|
236
|
+
process.send(msg);
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
function registerWorkerIpc(ctx) {
|
|
240
|
+
process.on("message", (msg) => {
|
|
241
|
+
if (msg?.type === "csm:index-result") {
|
|
242
|
+
const pending = pendingIndexRequests.get(msg.id);
|
|
243
|
+
if (!pending) return;
|
|
244
|
+
pendingIndexRequests.delete(msg.id);
|
|
245
|
+
if (msg.ok) pending.resolve(msg.result ?? {});
|
|
246
|
+
else pending.reject(new Error(msg.error ?? "Index failed"));
|
|
247
|
+
} else if (msg?.type === "csm:invalidate") {
|
|
248
|
+
void invalidateRetriever(msg.project).catch(() => {
|
|
249
|
+
});
|
|
250
|
+
ctx.projectName = msg.project;
|
|
251
|
+
if (msg.directoryPath) ctx.directoryPath = msg.directoryPath;
|
|
252
|
+
ctx.indexReady = true;
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
process.on("disconnect", () => process.exit(0));
|
|
256
|
+
}
|
|
161
257
|
function parseReindexInterval() {
|
|
162
258
|
const raw = process.env.CODE_SEARCH_REINDEX_INTERVAL?.trim();
|
|
163
259
|
if (!raw || raw === "0" || raw === "none") return 0;
|
|
@@ -224,16 +320,22 @@ async function trackedIndex(ctx, project, dirPath, opts) {
|
|
|
224
320
|
throw err;
|
|
225
321
|
}
|
|
226
322
|
}
|
|
227
|
-
async function autoIndex(ctx) {
|
|
323
|
+
async function autoIndex(ctx, opts) {
|
|
228
324
|
try {
|
|
229
|
-
const
|
|
230
|
-
const
|
|
231
|
-
const
|
|
232
|
-
|
|
325
|
+
const blobs = getBlobStore(ctx.projectName, toKnowledgeConfig(loadServerConfig().__unified));
|
|
326
|
+
const stats = await blobs.getJson("index_stats.json");
|
|
327
|
+
const statsReady = !!stats && stats.embeddingProvider !== "pending";
|
|
328
|
+
const hasLanceDB = await blobs.exists("lancedb");
|
|
329
|
+
const hasGraph = await blobs.exists("system_graph.sqlite") || await blobs.exists("system_graph_v2.json");
|
|
330
|
+
if (statsReady || hasLanceDB && hasGraph) {
|
|
233
331
|
ctx.indexReady = true;
|
|
234
332
|
console.error(`[code-search-mcp] Index loaded for "${ctx.projectName}"`);
|
|
235
333
|
return;
|
|
236
334
|
}
|
|
335
|
+
if (opts?.readOnly) {
|
|
336
|
+
console.error(`[code-search-mcp] No index yet \u2014 read-only worker waiting for the writer`);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
237
339
|
if (!ctx.directoryPath) {
|
|
238
340
|
console.error(`[code-search-mcp] No index found and no directory path \u2014 tools will return empty results`);
|
|
239
341
|
return;
|
package/dist/tools/graph.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import { existsSync
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import {
|
|
4
|
-
|
|
4
|
+
getBlobStore,
|
|
5
|
+
getGraphStore,
|
|
5
6
|
loadAllProfiles,
|
|
6
7
|
loadProfile,
|
|
7
8
|
getAllChanges,
|
|
8
|
-
getChangedFilesList
|
|
9
|
-
openSystemGraphStore
|
|
9
|
+
getChangedFilesList
|
|
10
10
|
} from "@esankhan3/anvil-knowledge-core";
|
|
11
|
+
import { resolvedKnowledgeConfig } from "../core/env-config.js";
|
|
11
12
|
const GRAPH_TOOL_NAMES = [
|
|
12
13
|
"get_repo_graph",
|
|
13
14
|
"get_cross_repo_edges",
|
|
@@ -199,12 +200,11 @@ async function handleGraphTool(name, args, ctx) {
|
|
|
199
200
|
const text = (t) => ({ content: [{ type: "text", text: t }] });
|
|
200
201
|
const NO_GRAPH = "No system graph found. Build KB first.";
|
|
201
202
|
try {
|
|
202
|
-
const
|
|
203
|
+
const blobs = getBlobStore(ctx.projectName, resolvedKnowledgeConfig());
|
|
203
204
|
if (name === "get_repo_graph") {
|
|
204
205
|
const repo = args.repo;
|
|
205
|
-
const
|
|
206
|
-
if (!
|
|
207
|
-
const graph = JSON.parse(readFileSync(graphPath, "utf-8"));
|
|
206
|
+
const graph = await blobs.getJson(`${repo}/graph.json`);
|
|
207
|
+
if (!graph) return text(`No graph found for repo "${repo}"`);
|
|
208
208
|
const summary = `# ${repo} AST Graph
|
|
209
209
|
|
|
210
210
|
- **Nodes:** ${graph.nodes?.length ?? 0}
|
|
@@ -217,130 +217,113 @@ ${graph.nodes?.length > 50 ? `... and ${graph.nodes.length - 50} more` : ""}`;
|
|
|
217
217
|
return text(summary);
|
|
218
218
|
}
|
|
219
219
|
if (name === "get_cross_repo_edges") {
|
|
220
|
-
const store = await
|
|
220
|
+
const store = await getGraphStore(ctx.projectName, resolvedKnowledgeConfig());
|
|
221
221
|
if (!store) return text(NO_GRAPH);
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
return text(`# Cross-Repo Edges${repo ? ` for ${repo}` : ""}
|
|
222
|
+
const repo = args.repo;
|
|
223
|
+
const { edges, total } = store.crossRepoEdges(repo, 50);
|
|
224
|
+
if (total === 0) return text(repo ? `No cross-repo edges found for "${repo}"` : "No cross-repo edges found");
|
|
225
|
+
const body = edges.map((e) => `- ${repoOf(e.source)} \u2192 ${repoOf(e.target)} (${e.type ?? "edge"})`).join("\n");
|
|
226
|
+
return text(`# Cross-Repo Edges${repo ? ` for ${repo}` : ""}
|
|
228
227
|
|
|
229
228
|
${total} edges found:
|
|
230
229
|
|
|
231
230
|
${body}${total > edges.length ? `
|
|
232
231
|
|
|
233
232
|
... and ${total - edges.length} more` : ""}`);
|
|
234
|
-
} finally {
|
|
235
|
-
store.close();
|
|
236
|
-
}
|
|
237
233
|
}
|
|
238
234
|
if (name === "find_callers" || name === "find_dependencies") {
|
|
239
|
-
const store = await
|
|
235
|
+
const store = await getGraphStore(ctx.projectName, resolvedKnowledgeConfig());
|
|
240
236
|
if (!store) return text("No system graph found.");
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
return text(`# ${direction} "${funcName}"
|
|
237
|
+
const funcName = args.function;
|
|
238
|
+
const repoFilter = args.repo;
|
|
239
|
+
const fuzzy = args.fuzzy === true;
|
|
240
|
+
const matched = store.resolveNodes(funcName, repoFilter, fuzzy);
|
|
241
|
+
if (matched.length === 0) {
|
|
242
|
+
return text(`No entity found matching "${funcName}"${repoFilter ? ` in ${repoFilter}` : ""}. Try fuzzy:true for substring matching.`);
|
|
243
|
+
}
|
|
244
|
+
const keys = matched.map((m) => m.key);
|
|
245
|
+
const unique = name === "find_callers" ? store.callers(keys, 30) : store.dependencies(keys, 30);
|
|
246
|
+
const direction = name === "find_callers" ? "Callers of" : "Dependencies of";
|
|
247
|
+
if (unique.length === 0) {
|
|
248
|
+
return text(`# ${direction} "${funcName}"
|
|
254
249
|
|
|
255
250
|
Matched ${matched.length} entit${matched.length === 1 ? "y" : "ies"}, but no ${name === "find_callers" ? "callers" : "dependencies"} found.`);
|
|
256
|
-
|
|
257
|
-
|
|
251
|
+
}
|
|
252
|
+
return text(`# ${direction} "${funcName}"
|
|
258
253
|
|
|
259
254
|
${unique.length} found:
|
|
260
255
|
${unique.map((r) => `- \`${r}\``).join("\n")}`);
|
|
261
|
-
} finally {
|
|
262
|
-
store.close();
|
|
263
|
-
}
|
|
264
256
|
}
|
|
265
257
|
if (name === "impact_analysis") {
|
|
266
|
-
const store = await
|
|
258
|
+
const store = await getGraphStore(ctx.projectName, resolvedKnowledgeConfig());
|
|
267
259
|
if (!store) return text("No system graph found.");
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
total > dependents.length ? `
|
|
260
|
+
const file = args.file;
|
|
261
|
+
const repo = args.repo;
|
|
262
|
+
const entity = args.entity;
|
|
263
|
+
const fileKeys = store.nodesInFiles(repo, [file], entity);
|
|
264
|
+
const { edges: dependents, total, repos } = store.dependents(fileKeys, 30);
|
|
265
|
+
const body = [
|
|
266
|
+
`# Impact Analysis: ${repo}/${file}${entity ? `::${entity}` : ""}`,
|
|
267
|
+
"",
|
|
268
|
+
`## Entities in scope: ${fileKeys.length}`,
|
|
269
|
+
...fileKeys.slice(0, 20).map((k) => `- \`${k}\``),
|
|
270
|
+
"",
|
|
271
|
+
`## Dependents: ${total} edges from ${repos.length} repos`,
|
|
272
|
+
...dependents.map((e) => `- \`${e.source}\` \u2192 \`${e.target}\` (${e.type ?? "edge"})`),
|
|
273
|
+
total > dependents.length ? `
|
|
283
274
|
... and ${total - dependents.length} more` : "",
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
} finally {
|
|
289
|
-
store.close();
|
|
290
|
-
}
|
|
275
|
+
"",
|
|
276
|
+
`## Affected repos: ${repos.join(", ") || "none"}`
|
|
277
|
+
].join("\n");
|
|
278
|
+
return text(body);
|
|
291
279
|
}
|
|
292
280
|
if (name === "trace_path") {
|
|
293
|
-
const store = await
|
|
281
|
+
const store = await getGraphStore(ctx.projectName, resolvedKnowledgeConfig());
|
|
294
282
|
if (!store) return text("No system graph found.");
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
return text(`# Path (${path.length - 1} hops)
|
|
283
|
+
const from = args.from;
|
|
284
|
+
const to = args.to;
|
|
285
|
+
const repo = args.repo;
|
|
286
|
+
const fuzzy = args.fuzzy === true;
|
|
287
|
+
const direction = args.direction ?? "callees";
|
|
288
|
+
const maxDepth = Math.max(1, Math.min(10, args.maxDepth || 4));
|
|
289
|
+
const fromKeys = store.resolveNodes(from, repo, fuzzy).map((m) => m.key);
|
|
290
|
+
if (fromKeys.length === 0) return text(`No entity found matching "${from}". Try fuzzy:true.`);
|
|
291
|
+
const getNeighbors = makeNeighborGetter(store, direction);
|
|
292
|
+
if (to) {
|
|
293
|
+
const toKeys = new Set(store.resolveNodes(to, repo, fuzzy).map((m) => m.key));
|
|
294
|
+
if (toKeys.size === 0) return text(`No entity found matching target "${to}". Try fuzzy:true.`);
|
|
295
|
+
const path = shortestPath(getNeighbors, fromKeys, toKeys, maxDepth);
|
|
296
|
+
if (!path) return text(`No path from "${from}" to "${to}" within ${maxDepth} hops (direction: ${direction}).`);
|
|
297
|
+
const labelOf2 = store.labelsOf(path);
|
|
298
|
+
const arrow = direction === "callers" ? " \u2190 " : " \u2192 ";
|
|
299
|
+
const rendered = path.map((k) => labelOf2.get(k) ?? k).join(arrow);
|
|
300
|
+
return text(`# Path (${path.length - 1} hops)
|
|
314
301
|
|
|
315
302
|
${rendered}
|
|
316
303
|
|
|
317
304
|
${path.map((k) => `- \`${k}\``).join("\n")}`);
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
305
|
+
}
|
|
306
|
+
const nodes = reachable(getNeighbors, fromKeys, maxDepth);
|
|
307
|
+
if (nodes.length === 0) return text(`No ${direction} reachable from "${from}" within ${maxDepth} hops.`);
|
|
308
|
+
const labelOf = store.labelsOf(nodes.map((n) => n.key));
|
|
309
|
+
const byDepth = /* @__PURE__ */ new Map();
|
|
310
|
+
for (const { key, depth } of nodes) {
|
|
311
|
+
if (!byDepth.has(depth)) byDepth.set(depth, []);
|
|
312
|
+
byDepth.get(depth).push(`\`${labelOf.get(key) ?? key}\` (${key})`);
|
|
313
|
+
}
|
|
314
|
+
const sections = [...byDepth.entries()].sort((a, b) => a[0] - b[0]).map(([d, ks]) => `## Depth ${d}
|
|
328
315
|
${ks.map((k) => `- ${k}`).join("\n")}`).join("\n\n");
|
|
329
|
-
|
|
316
|
+
return text(`# Reachable from "${from}" (${direction}, \u2264${maxDepth} hops)
|
|
330
317
|
|
|
331
318
|
${nodes.length} nodes:
|
|
332
319
|
|
|
333
320
|
${sections}`);
|
|
334
|
-
} finally {
|
|
335
|
-
store.close();
|
|
336
|
-
}
|
|
337
321
|
}
|
|
338
322
|
if (name === "find_dead_code") {
|
|
339
323
|
const repo = args.repo;
|
|
340
324
|
const limit = args.limit || 50;
|
|
341
|
-
const
|
|
342
|
-
if (!
|
|
343
|
-
const graph = JSON.parse(readFileSync(graphPath, "utf-8"));
|
|
325
|
+
const graph = await blobs.getJson(`${repo}/graph.json`);
|
|
326
|
+
if (!graph) return text(`No graph found for repo "${repo}"`);
|
|
344
327
|
const ENTITY_TYPES = /* @__PURE__ */ new Set(["function", "method", "class", "struct", "enum", "trait"]);
|
|
345
328
|
const inDegree = /* @__PURE__ */ new Map();
|
|
346
329
|
for (const l of graph.links ?? []) {
|
|
@@ -365,7 +348,7 @@ _Note: exported APIs, reflection, and dynamic dispatch can cause false positives
|
|
|
365
348
|
if (name === "get_architecture") {
|
|
366
349
|
const repo = args.repo;
|
|
367
350
|
if (repo) {
|
|
368
|
-
const profile = loadProfile(ctx.projectName, repo);
|
|
351
|
+
const profile = await loadProfile(ctx.projectName, repo, resolvedKnowledgeConfig());
|
|
369
352
|
if (!profile) return text(`No profile for "${repo}". Run profiling (requires LLM) to generate one.`);
|
|
370
353
|
const ep = (xs) => xs?.length ? xs.map((e) => ` - ${e.type}: ${e.identifier} \u2014 ${e.description}`).join("\n") : " - (none)";
|
|
371
354
|
const body = [
|
|
@@ -384,8 +367,7 @@ _Note: exported APIs, reflection, and dynamic dispatch can cause false positives
|
|
|
384
367
|
].join("\n");
|
|
385
368
|
return text(body);
|
|
386
369
|
}
|
|
387
|
-
const
|
|
388
|
-
const pg = existsSync(pgPath) ? JSON.parse(readFileSync(pgPath, "utf-8")) : null;
|
|
370
|
+
const pg = await blobs.getJson("PROJECT_GRAPH.json");
|
|
389
371
|
if (pg) {
|
|
390
372
|
const lines = ["# Project Architecture", "", pg.architectureSummary ?? ""];
|
|
391
373
|
if (pg.repoRoles && Object.keys(pg.repoRoles).length) {
|
|
@@ -407,9 +389,9 @@ _Note: exported APIs, reflection, and dynamic dispatch can cause false positives
|
|
|
407
389
|
}
|
|
408
390
|
return text(lines.join("\n"));
|
|
409
391
|
}
|
|
410
|
-
const
|
|
411
|
-
if (
|
|
412
|
-
const profiles = loadAllProfiles(ctx.projectName);
|
|
392
|
+
const summaryText = await blobs.getText("PROJECT_SUMMARY.md");
|
|
393
|
+
if (summaryText !== null) return text(summaryText);
|
|
394
|
+
const profiles = await loadAllProfiles(ctx.projectName, resolvedKnowledgeConfig());
|
|
413
395
|
if (profiles.length) {
|
|
414
396
|
return text(`# Project Repos
|
|
415
397
|
|
|
@@ -420,26 +402,22 @@ _Run project-graph generation (requires LLM) for a full architecture view._`);
|
|
|
420
402
|
return text("No architecture data yet. Index the project, then run profiling / project-graph generation (requires an LLM) for an architecture overview.");
|
|
421
403
|
}
|
|
422
404
|
if (name === "search_graph") {
|
|
423
|
-
const store = await
|
|
405
|
+
const store = await getGraphStore(ctx.projectName, resolvedKnowledgeConfig());
|
|
424
406
|
if (!store) return text(NO_GRAPH);
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
return text(`# search_graph \u2014 ${total} match${total === 1 ? "" : "es"}
|
|
407
|
+
const { rows, total } = store.searchNodes({
|
|
408
|
+
name: args.name,
|
|
409
|
+
type: args.type,
|
|
410
|
+
file: args.file,
|
|
411
|
+
repo: args.repo,
|
|
412
|
+
minDegree: args.minDegree ?? 0
|
|
413
|
+
}, args.limit || 50);
|
|
414
|
+
if (total === 0) return text("No entities match the given filters.");
|
|
415
|
+
const body = rows.map((m) => `- \`${m.label}\` (${m.type}, ${m.file}) \u2014 degree ${m.degree} \`${m.key}\``).join("\n");
|
|
416
|
+
return text(`# search_graph \u2014 ${total} match${total === 1 ? "" : "es"}
|
|
436
417
|
|
|
437
418
|
${body}${total > rows.length ? `
|
|
438
419
|
|
|
439
420
|
... and ${total - rows.length} more` : ""}`);
|
|
440
|
-
} finally {
|
|
441
|
-
store.close();
|
|
442
|
-
}
|
|
443
421
|
}
|
|
444
422
|
if (name === "detect_changes") {
|
|
445
423
|
const repo = args.repo;
|
|
@@ -452,12 +430,10 @@ ${body}${total > rows.length ? `
|
|
|
452
430
|
if (!existsSync(repoPath)) return text(`Repo path not found: ${repoPath}`);
|
|
453
431
|
let baseSha = args.baseSha;
|
|
454
432
|
if (!baseSha) {
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
} catch {
|
|
460
|
-
}
|
|
433
|
+
try {
|
|
434
|
+
const meta = await blobs.getJson(`${repo}/index_meta.json`);
|
|
435
|
+
baseSha = meta?.lastIndexedSha;
|
|
436
|
+
} catch {
|
|
461
437
|
}
|
|
462
438
|
}
|
|
463
439
|
if (!baseSha) return text("No base commit available. Pass baseSha, or index the repo first so a last-indexed SHA exists.");
|
|
@@ -465,21 +441,17 @@ ${body}${total > rows.length ? `
|
|
|
465
441
|
if (diff.fallbackToFull) return text(`git diff against ${baseSha.slice(0, 7)} failed or is too large to map incrementally. Check the base SHA.`);
|
|
466
442
|
const changedFiles = getChangedFilesList(diff);
|
|
467
443
|
if (changedFiles.length === 0 && diff.deleted.length === 0) return text(`No source changes since ${baseSha.slice(0, 7)}.`);
|
|
468
|
-
const store = await
|
|
444
|
+
const store = await getGraphStore(ctx.projectName, resolvedKnowledgeConfig());
|
|
469
445
|
let changedKeys = [];
|
|
470
446
|
let dependents = [];
|
|
471
447
|
let depTotal = 0;
|
|
472
448
|
let depRepos = [];
|
|
473
449
|
if (store) {
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
depRepos = d.repos;
|
|
480
|
-
} finally {
|
|
481
|
-
store.close();
|
|
482
|
-
}
|
|
450
|
+
changedKeys = store.nodesInFiles(repo, changedFiles);
|
|
451
|
+
const d = store.dependents(changedKeys, limit);
|
|
452
|
+
dependents = d.edges;
|
|
453
|
+
depTotal = d.total;
|
|
454
|
+
depRepos = d.repos;
|
|
483
455
|
}
|
|
484
456
|
const lines = [
|
|
485
457
|
`# Changes since ${baseSha.slice(0, 7)} \u2014 ${repo}`,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolvedKnowledgeConfig } from "../core/env-config.js";
|
|
1
2
|
import { KnowledgeIndexer } from "@esankhan3/anvil-knowledge-core";
|
|
2
3
|
const TOOL_NAMES = ["index_status"];
|
|
3
4
|
function registerIndexTools() {
|
|
@@ -17,7 +18,7 @@ async function handleIndexTool(name, args, ctx) {
|
|
|
17
18
|
try {
|
|
18
19
|
if (name === "index_status") {
|
|
19
20
|
const indexer = new KnowledgeIndexer();
|
|
20
|
-
const stats = await indexer.getStats(ctx.projectName);
|
|
21
|
+
const stats = await indexer.getStats(ctx.projectName, resolvedKnowledgeConfig());
|
|
21
22
|
const lines = [
|
|
22
23
|
`# Index Status: ${ctx.projectName}`,
|
|
23
24
|
"",
|
package/dist/tools/profile.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { loadAllProfiles, loadProfile } from "@esankhan3/anvil-knowledge-core";
|
|
2
|
+
import { resolvedKnowledgeConfig } from "../core/env-config.js";
|
|
2
3
|
import { discoverRepos } from "@esankhan3/anvil-knowledge-core";
|
|
3
4
|
function registerProfileTools() {
|
|
4
5
|
return [
|
|
@@ -27,7 +28,7 @@ async function handleProfileTool(name, args, ctx) {
|
|
|
27
28
|
if (!["list_repos", "get_repo_profile"].includes(name)) return null;
|
|
28
29
|
try {
|
|
29
30
|
if (name === "list_repos") {
|
|
30
|
-
const profiles = loadAllProfiles(ctx.projectName);
|
|
31
|
+
const profiles = await loadAllProfiles(ctx.projectName, resolvedKnowledgeConfig());
|
|
31
32
|
if (profiles.length === 0) {
|
|
32
33
|
if (ctx.directoryPath) {
|
|
33
34
|
const repos = discoverRepos(ctx.directoryPath);
|
|
@@ -47,7 +48,7 @@ ${text}` }] };
|
|
|
47
48
|
}
|
|
48
49
|
if (name === "get_repo_profile") {
|
|
49
50
|
const repo = args.repo;
|
|
50
|
-
const profile = loadProfile(ctx.projectName, repo);
|
|
51
|
+
const profile = await loadProfile(ctx.projectName, repo, resolvedKnowledgeConfig());
|
|
51
52
|
if (!profile) {
|
|
52
53
|
return { content: [{ type: "text", text: `No profile found for "${repo}". Run reindex to generate profiles.` }] };
|
|
53
54
|
}
|
package/dist/tools/search.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { getRetriever, getKnowledgeBasePath, findChunksInFile } from "@esankhan3/anvil-knowledge-core";
|
|
1
|
+
import { getRetriever, getBlobStore, findChunksInProject } from "@esankhan3/anvil-knowledge-core";
|
|
2
|
+
import { resolvedKnowledgeConfig } from "../core/env-config.js";
|
|
4
3
|
function registerSearchTools() {
|
|
5
4
|
return [
|
|
6
5
|
{
|
|
@@ -62,7 +61,7 @@ async function handleSearchTool(name, args, ctx) {
|
|
|
62
61
|
return { content: [{ type: "text", text: "Index not ready. Run reindex tool or wait for auto-indexing to complete." }] };
|
|
63
62
|
}
|
|
64
63
|
try {
|
|
65
|
-
const retriever = await getRetriever(ctx.projectName);
|
|
64
|
+
const retriever = await getRetriever(ctx.projectName, resolvedKnowledgeConfig());
|
|
66
65
|
const query = args.query;
|
|
67
66
|
const maxResults = args.maxResults || 10;
|
|
68
67
|
const repos = args.repos;
|
|
@@ -116,18 +115,30 @@ async function handleGetCodeSnippet(args, ctx) {
|
|
|
116
115
|
if (!repo || !file) {
|
|
117
116
|
return { content: [{ type: "text", text: 'Provide id="repo::file::entity" or repo + file (entity optional).' }] };
|
|
118
117
|
}
|
|
119
|
-
const chunksPath = join(getKnowledgeBasePath(ctx.projectName), "chunks.json");
|
|
120
|
-
if (!existsSync(chunksPath)) {
|
|
121
|
-
return { content: [{ type: "text", text: "No index found \u2014 chunks.json missing. Index the project first." }] };
|
|
122
|
-
}
|
|
123
118
|
try {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
119
|
+
const esc = (v) => v.replace(/'/g, "''");
|
|
120
|
+
let matches = [];
|
|
121
|
+
try {
|
|
122
|
+
const store = (await getRetriever(ctx.projectName, resolvedKnowledgeConfig())).vectorStore;
|
|
123
|
+
let scored = await store.getChunksByEntity([
|
|
124
|
+
{ repoName: repo, filePath: file, ...entity ? { entityName: entity } : {} }
|
|
125
|
+
]);
|
|
126
|
+
if (scored.length === 0 && entity) {
|
|
127
|
+
scored = await store.searchByEntityName([entity], 5, `repoName = '${esc(repo)}'`);
|
|
128
|
+
}
|
|
129
|
+
matches = scored.slice(0, 5).map((sc) => sc.chunk);
|
|
130
|
+
} catch {
|
|
131
|
+
}
|
|
132
|
+
if (matches.length === 0 && await getBlobStore(ctx.projectName, resolvedKnowledgeConfig()).exists("chunks.json")) {
|
|
133
|
+
matches = await findChunksInProject(
|
|
134
|
+
ctx.projectName,
|
|
135
|
+
(c) => c.repoName === repo && c.filePath === file && (!entity || c.entityName === entity),
|
|
136
|
+
5,
|
|
137
|
+
resolvedKnowledgeConfig()
|
|
138
|
+
);
|
|
139
|
+
if (matches.length === 0 && entity) {
|
|
140
|
+
matches = await findChunksInProject(ctx.projectName, (c) => c.repoName === repo && c.entityName === entity, 5, resolvedKnowledgeConfig());
|
|
141
|
+
}
|
|
131
142
|
}
|
|
132
143
|
if (matches.length === 0) {
|
|
133
144
|
return { content: [{ type: "text", text: `No snippet found for ${repo}::${file}${entity ? `::${entity}` : ""}` }] };
|
|
@@ -1,25 +1,17 @@
|
|
|
1
1
|
import { createServer } from "node:http";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
-
import {
|
|
3
|
+
import { createMcpHandler } from "@modelcontextprotocol/server";
|
|
4
|
+
import { toNodeHandler } from "@modelcontextprotocol/node";
|
|
4
5
|
import { createAuthMiddleware } from "../middleware/auth.js";
|
|
5
6
|
import { registry, metrics } from "../observability/metrics.js";
|
|
6
7
|
async function startHttpTransport(opts) {
|
|
7
8
|
const { config, createMcpServer, onReady, getHealth } = opts;
|
|
8
9
|
const authenticate = createAuthMiddleware(config);
|
|
9
10
|
const startedAt = Date.now();
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
const MAX_SESSIONS = Number.isFinite(maxEnv) && maxEnv > 0 ? maxEnv : 100;
|
|
15
|
-
setInterval(() => {
|
|
16
|
-
const now = Date.now();
|
|
17
|
-
for (const [id, session] of sessions) {
|
|
18
|
-
if (now - session.lastActivity > SESSION_TTL_MS) {
|
|
19
|
-
sessions.delete(id);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
}, 5 * 60 * 1e3).unref();
|
|
11
|
+
const mcpHandler = createMcpHandler(async () => (await createMcpServer()).server, {
|
|
12
|
+
onerror: (err) => console.error(`[code-search-mcp] MCP handler error: ${err.message}`)
|
|
13
|
+
});
|
|
14
|
+
const handleMcp = toNodeHandler(mcpHandler);
|
|
23
15
|
const httpServer = createServer(async (req, res) => {
|
|
24
16
|
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
25
17
|
const path = url.pathname;
|
|
@@ -30,7 +22,6 @@ async function startHttpTransport(opts) {
|
|
|
30
22
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
31
23
|
res.end(JSON.stringify({
|
|
32
24
|
status: "ok",
|
|
33
|
-
activeSessions: sessions.size,
|
|
34
25
|
...health
|
|
35
26
|
}));
|
|
36
27
|
return;
|
|
@@ -45,7 +36,7 @@ async function startHttpTransport(opts) {
|
|
|
45
36
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
46
37
|
res.end(JSON.stringify({
|
|
47
38
|
name: "code-search-mcp",
|
|
48
|
-
version: "
|
|
39
|
+
version: "1.0.0",
|
|
49
40
|
node: process.version,
|
|
50
41
|
platform: process.platform
|
|
51
42
|
}));
|
|
@@ -70,9 +61,8 @@ async function startHttpTransport(opts) {
|
|
|
70
61
|
const status = opts.getStatus?.() ?? {};
|
|
71
62
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
72
63
|
res.end(JSON.stringify({
|
|
73
|
-
version: "
|
|
64
|
+
version: "1.0.0",
|
|
74
65
|
uptime: Math.floor((Date.now() - startedAt) / 1e3),
|
|
75
|
-
sessions: sessions.size,
|
|
76
66
|
...status
|
|
77
67
|
}, null, 2));
|
|
78
68
|
return;
|
|
@@ -117,74 +107,7 @@ async function startHttpTransport(opts) {
|
|
|
117
107
|
const identity = authenticate(req, res);
|
|
118
108
|
if (!identity) return;
|
|
119
109
|
}
|
|
120
|
-
|
|
121
|
-
const sessionId = req.headers["mcp-session-id"];
|
|
122
|
-
if (sessionId && sessions.has(sessionId)) {
|
|
123
|
-
sessions.get(sessionId).lastActivity = Date.now();
|
|
124
|
-
await sessions.get(sessionId).transport.handleRequest(req, res);
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
if (sessionId && !sessions.has(sessionId)) {
|
|
128
|
-
res.writeHead(404, { "Content-Type": "application/json" });
|
|
129
|
-
res.end(JSON.stringify({ error: "Session expired or unknown \u2014 reinitialize." }));
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
if (sessions.size >= MAX_SESSIONS) {
|
|
133
|
-
res.writeHead(503, { "Content-Type": "application/json" });
|
|
134
|
-
res.end(JSON.stringify({ error: "Too many active sessions. Try again later." }));
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
try {
|
|
138
|
-
const transport = new StreamableHTTPServerTransport({
|
|
139
|
-
sessionIdGenerator: () => randomUUID()
|
|
140
|
-
});
|
|
141
|
-
const { server: mcpServer } = await createMcpServer();
|
|
142
|
-
await mcpServer.connect(transport);
|
|
143
|
-
transport.onclose = () => {
|
|
144
|
-
for (const [id, s] of sessions) {
|
|
145
|
-
if (s.transport === transport) {
|
|
146
|
-
sessions.delete(id);
|
|
147
|
-
break;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
};
|
|
151
|
-
await transport.handleRequest(req, res);
|
|
152
|
-
const newSessionId = res.getHeader("mcp-session-id");
|
|
153
|
-
if (newSessionId) {
|
|
154
|
-
sessions.set(newSessionId, { transport, lastActivity: Date.now() });
|
|
155
|
-
}
|
|
156
|
-
} catch (err) {
|
|
157
|
-
if (!res.headersSent) {
|
|
158
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
159
|
-
res.end(JSON.stringify({ error: "Failed to create session" }));
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
if (req.method === "GET") {
|
|
165
|
-
const sessionId = req.headers["mcp-session-id"];
|
|
166
|
-
if (sessionId && sessions.has(sessionId)) {
|
|
167
|
-
sessions.get(sessionId).lastActivity = Date.now();
|
|
168
|
-
await sessions.get(sessionId).transport.handleRequest(req, res);
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
res.writeHead(sessionId ? 404 : 400, { "Content-Type": "application/json" });
|
|
172
|
-
res.end(JSON.stringify({ error: sessionId ? "Session expired or unknown \u2014 reinitialize." : "Session ID required for GET requests" }));
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
if (req.method === "DELETE") {
|
|
176
|
-
const sessionId = req.headers["mcp-session-id"];
|
|
177
|
-
if (sessionId && sessions.has(sessionId)) {
|
|
178
|
-
await sessions.get(sessionId).transport.handleRequest(req, res);
|
|
179
|
-
sessions.delete(sessionId);
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
183
|
-
res.end(JSON.stringify({ error: "Invalid session ID" }));
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
res.writeHead(405, { "Content-Type": "application/json" });
|
|
187
|
-
res.end(JSON.stringify({ error: "Method not allowed" }));
|
|
110
|
+
await handleMcp(req, res);
|
|
188
111
|
return;
|
|
189
112
|
}
|
|
190
113
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
@@ -194,7 +117,7 @@ async function startHttpTransport(opts) {
|
|
|
194
117
|
httpServer.listen(config.port, config.host, () => {
|
|
195
118
|
const url = `http://${config.host === "0.0.0.0" ? "localhost" : config.host}:${config.port}`;
|
|
196
119
|
onReady?.(url);
|
|
197
|
-
resolve();
|
|
120
|
+
resolve(httpServer);
|
|
198
121
|
});
|
|
199
122
|
});
|
|
200
123
|
}
|
|
@@ -1,11 +1,5 @@
|
|
|
1
|
-
import { Server } from "@modelcontextprotocol/
|
|
2
|
-
import { StdioServerTransport } from "@modelcontextprotocol/
|
|
3
|
-
import {
|
|
4
|
-
CallToolRequestSchema,
|
|
5
|
-
ListToolsRequestSchema,
|
|
6
|
-
ListResourcesRequestSchema,
|
|
7
|
-
ReadResourceRequestSchema
|
|
8
|
-
} from "@modelcontextprotocol/sdk/types.js";
|
|
1
|
+
import { Server } from "@modelcontextprotocol/server";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
|
|
9
3
|
class RemoteConnection {
|
|
10
4
|
serverUrl;
|
|
11
5
|
apiKey;
|
|
@@ -70,7 +64,7 @@ class RemoteConnection {
|
|
|
70
64
|
return this.request("initialize", {
|
|
71
65
|
protocolVersion: "2024-11-05",
|
|
72
66
|
capabilities: {},
|
|
73
|
-
clientInfo: { name: "code-search-mcp-proxy", version: "
|
|
67
|
+
clientInfo: { name: "code-search-mcp-proxy", version: "1.0.0" }
|
|
74
68
|
});
|
|
75
69
|
}
|
|
76
70
|
/** Check remote server health */
|
|
@@ -97,10 +91,10 @@ async function startRemoteProxy(config) {
|
|
|
97
91
|
console.error(`[code-search-mcp] WARNING: Session init failed: ${err.message}. Will retry on first request.`);
|
|
98
92
|
}
|
|
99
93
|
const server = new Server(
|
|
100
|
-
{ name: "code-search-mcp", version: "
|
|
94
|
+
{ name: "code-search-mcp", version: "1.0.0" },
|
|
101
95
|
{ capabilities: { tools: {}, resources: {} } }
|
|
102
96
|
);
|
|
103
|
-
server.setRequestHandler(
|
|
97
|
+
server.setRequestHandler("tools/list", async () => {
|
|
104
98
|
try {
|
|
105
99
|
const response = await remote.request("tools/list");
|
|
106
100
|
return response.result ?? { tools: [] };
|
|
@@ -109,7 +103,7 @@ async function startRemoteProxy(config) {
|
|
|
109
103
|
return { tools: [] };
|
|
110
104
|
}
|
|
111
105
|
});
|
|
112
|
-
server.setRequestHandler(
|
|
106
|
+
server.setRequestHandler("tools/call", async (request) => {
|
|
113
107
|
try {
|
|
114
108
|
const response = await remote.request("tools/call", {
|
|
115
109
|
name: request.params.name,
|
|
@@ -126,7 +120,7 @@ async function startRemoteProxy(config) {
|
|
|
126
120
|
};
|
|
127
121
|
}
|
|
128
122
|
});
|
|
129
|
-
server.setRequestHandler(
|
|
123
|
+
server.setRequestHandler("resources/list", async () => {
|
|
130
124
|
try {
|
|
131
125
|
const response = await remote.request("resources/list");
|
|
132
126
|
return response.result ?? { resources: [] };
|
|
@@ -134,7 +128,7 @@ async function startRemoteProxy(config) {
|
|
|
134
128
|
return { resources: [] };
|
|
135
129
|
}
|
|
136
130
|
});
|
|
137
|
-
server.setRequestHandler(
|
|
131
|
+
server.setRequestHandler("resources/read", async (request) => {
|
|
138
132
|
try {
|
|
139
133
|
const response = await remote.request("resources/read", {
|
|
140
134
|
uri: request.params.uri
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@esankhan3/code-search-mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "MCP server for multi-repo code search
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for multi-repo code search \u2014 GitHub org or local path, zero config",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"bin": {
|
|
@@ -23,9 +23,10 @@
|
|
|
23
23
|
"smoke": "bash scripts/smoke/run-smoke.sh"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@esankhan3/anvil-agent-core": "^0.
|
|
27
|
-
"@esankhan3/anvil-knowledge-core": "^0.
|
|
28
|
-
"@modelcontextprotocol/
|
|
26
|
+
"@esankhan3/anvil-agent-core": "^0.5.0",
|
|
27
|
+
"@esankhan3/anvil-knowledge-core": "^0.4.0",
|
|
28
|
+
"@modelcontextprotocol/node": "^2.0.0",
|
|
29
|
+
"@modelcontextprotocol/server": "^2.0.0"
|
|
29
30
|
},
|
|
30
31
|
"optionalDependencies": {
|
|
31
32
|
"@lancedb/lancedb-darwin-arm64": "0.27.2",
|
|
@@ -35,6 +36,7 @@
|
|
|
35
36
|
"@lancedb/lancedb-win32-x64-msvc": "0.27.2"
|
|
36
37
|
},
|
|
37
38
|
"devDependencies": {
|
|
39
|
+
"@modelcontextprotocol/client": "^2.0.0",
|
|
38
40
|
"@types/node": "^22.15.2"
|
|
39
41
|
},
|
|
40
42
|
"repository": {
|