@esankhan3/code-search-mcp 0.4.12 → 1.0.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 +8 -7
- package/dist/__tests__/http-stateless.test.js +13 -6
- 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 +15 -23
- 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 +9 -111
- 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
|
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
import { describe, it } from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import { Server } from "@modelcontextprotocol/
|
|
4
|
-
import { Client } from "@modelcontextprotocol/
|
|
5
|
-
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
6
|
-
import { ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
3
|
+
import { Server } from "@modelcontextprotocol/server";
|
|
4
|
+
import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
|
|
7
5
|
import { startHttpTransport } from "../transports/http-transport.js";
|
|
8
6
|
function makeMcpServer() {
|
|
9
7
|
const server = new Server(
|
|
10
8
|
{ name: "stateless-test", version: "0.0.0" },
|
|
11
9
|
{ capabilities: { tools: {} } }
|
|
12
10
|
);
|
|
13
|
-
server.setRequestHandler(
|
|
11
|
+
server.setRequestHandler("tools/list", async () => ({
|
|
14
12
|
tools: [{ name: "ping", description: "test tool", inputSchema: { type: "object" } }]
|
|
15
13
|
}));
|
|
16
14
|
return server;
|
|
@@ -27,7 +25,6 @@ describe("http transport \u2014 stateless mode (cluster workers)", () => {
|
|
|
27
25
|
it("serves initialize + tools/list as independent sessionless requests", async () => {
|
|
28
26
|
const httpServer = await startHttpTransport({
|
|
29
27
|
config,
|
|
30
|
-
stateless: true,
|
|
31
28
|
createMcpServer: async () => ({ server: makeMcpServer() })
|
|
32
29
|
});
|
|
33
30
|
const base = `http://127.0.0.1:${httpServer.address().port}`;
|
|
@@ -38,7 +35,17 @@ describe("http transport \u2014 stateless mode (cluster workers)", () => {
|
|
|
38
35
|
const tools = await client.listTools();
|
|
39
36
|
assert.equal(tools.tools[0]?.name, "ping");
|
|
40
37
|
assert.equal(transport.sessionId, void 0, "stateless server must not assign a session");
|
|
38
|
+
assert.equal(client.getProtocolEra(), "legacy");
|
|
41
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();
|
|
42
49
|
const get = await fetch(`${base}/mcp`, { method: "GET" });
|
|
43
50
|
assert.equal(get.status, 405);
|
|
44
51
|
const del = await fetch(`${base}/mcp`, { method: "DELETE" });
|
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,27 +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";
|
|
11
5
|
import cluster from "node:cluster";
|
|
12
6
|
import { registerSearchTools, handleSearchTool } from "./tools/search.js";
|
|
13
7
|
import { registerGraphTools, handleGraphTool } from "./tools/graph.js";
|
|
14
8
|
import { registerProfileTools, handleProfileTool } from "./tools/profile.js";
|
|
15
9
|
import { registerIndexTools, handleIndexTool } from "./tools/index-tools.js";
|
|
16
10
|
import { registerResources, handleResource } from "./resources/resources.js";
|
|
17
|
-
import {
|
|
11
|
+
import { getBlobStore } from "@esankhan3/anvil-knowledge-core";
|
|
18
12
|
import { indexFromPath, invalidateRetriever } from "@esankhan3/anvil-knowledge-core";
|
|
19
13
|
import { loadServerConfig } from "./core/env-config.js";
|
|
20
14
|
import { toKnowledgeConfig } from "./core/config.js";
|
|
21
15
|
import { startHttpTransport } from "./transports/http-transport.js";
|
|
22
16
|
function createMcpServerInstance(ctx) {
|
|
23
17
|
const server = new Server(
|
|
24
|
-
{ name: "code-search-mcp", version: "0.
|
|
18
|
+
{ name: "code-search-mcp", version: "1.0.0" },
|
|
25
19
|
{ capabilities: { tools: {}, resources: {} } }
|
|
26
20
|
);
|
|
27
21
|
const allTools = [
|
|
@@ -30,10 +24,10 @@ function createMcpServerInstance(ctx) {
|
|
|
30
24
|
...registerProfileTools(),
|
|
31
25
|
...registerIndexTools()
|
|
32
26
|
];
|
|
33
|
-
server.setRequestHandler(
|
|
27
|
+
server.setRequestHandler("tools/list", async () => ({
|
|
34
28
|
tools: allTools
|
|
35
29
|
}));
|
|
36
|
-
server.setRequestHandler(
|
|
30
|
+
server.setRequestHandler("tools/call", async (request) => {
|
|
37
31
|
const name = request.params.name;
|
|
38
32
|
const args = request.params.arguments ?? {};
|
|
39
33
|
const searchResult = await handleSearchTool(name, args, ctx);
|
|
@@ -47,10 +41,10 @@ function createMcpServerInstance(ctx) {
|
|
|
47
41
|
return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
|
|
48
42
|
});
|
|
49
43
|
const allResources = registerResources(ctx);
|
|
50
|
-
server.setRequestHandler(
|
|
44
|
+
server.setRequestHandler("resources/list", async () => ({
|
|
51
45
|
resources: allResources
|
|
52
46
|
}));
|
|
53
|
-
server.setRequestHandler(
|
|
47
|
+
server.setRequestHandler("resources/read", async (request) => {
|
|
54
48
|
return handleResource(request.params.uri, ctx);
|
|
55
49
|
});
|
|
56
50
|
return server;
|
|
@@ -97,10 +91,6 @@ async function startServer(projectName, directoryPath) {
|
|
|
97
91
|
if (isReadOnlyWorker) registerWorkerIpc(ctx);
|
|
98
92
|
await startHttpTransport({
|
|
99
93
|
config,
|
|
100
|
-
// Workers must be stateless: the fronting proxy opens a new upstream
|
|
101
|
-
// connection per request, so in-memory MCP sessions cannot stick to one
|
|
102
|
-
// worker. Also opt-in for single-process deploys via env.
|
|
103
|
-
stateless: isReadOnlyWorker || process.env.CODE_SEARCH_STATELESS === "1",
|
|
104
94
|
createMcpServer: async () => ({
|
|
105
95
|
server: createMcpServerInstance(ctx)
|
|
106
96
|
}),
|
|
@@ -332,10 +322,12 @@ async function trackedIndex(ctx, project, dirPath, opts) {
|
|
|
332
322
|
}
|
|
333
323
|
async function autoIndex(ctx, opts) {
|
|
334
324
|
try {
|
|
335
|
-
const
|
|
336
|
-
const
|
|
337
|
-
const
|
|
338
|
-
|
|
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) {
|
|
339
331
|
ctx.indexReady = true;
|
|
340
332
|
console.error(`[code-search-mcp] Index loaded for "${ctx.projectName}"`);
|
|
341
333
|
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,99 +107,7 @@ async function startHttpTransport(opts) {
|
|
|
117
107
|
const identity = authenticate(req, res);
|
|
118
108
|
if (!identity) return;
|
|
119
109
|
}
|
|
120
|
-
|
|
121
|
-
try {
|
|
122
|
-
const transport = new StreamableHTTPServerTransport({
|
|
123
|
-
sessionIdGenerator: void 0,
|
|
124
|
-
enableJsonResponse: true
|
|
125
|
-
});
|
|
126
|
-
const { server: mcpServer } = await createMcpServer();
|
|
127
|
-
res.on("close", () => {
|
|
128
|
-
void transport.close();
|
|
129
|
-
});
|
|
130
|
-
await mcpServer.connect(transport);
|
|
131
|
-
await transport.handleRequest(req, res);
|
|
132
|
-
} catch {
|
|
133
|
-
if (!res.headersSent) {
|
|
134
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
135
|
-
res.end(JSON.stringify({ error: "Request handling failed" }));
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
if ((req.method === "GET" || req.method === "DELETE") && opts.stateless) {
|
|
141
|
-
res.writeHead(405, { "Content-Type": "application/json" });
|
|
142
|
-
res.end(JSON.stringify({ error: "Stateless mode: sessions are not supported" }));
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
if (req.method === "POST") {
|
|
146
|
-
const sessionId = req.headers["mcp-session-id"];
|
|
147
|
-
if (sessionId && sessions.has(sessionId)) {
|
|
148
|
-
sessions.get(sessionId).lastActivity = Date.now();
|
|
149
|
-
await sessions.get(sessionId).transport.handleRequest(req, res);
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
if (sessionId && !sessions.has(sessionId)) {
|
|
153
|
-
res.writeHead(404, { "Content-Type": "application/json" });
|
|
154
|
-
res.end(JSON.stringify({ error: "Session expired or unknown \u2014 reinitialize." }));
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
if (sessions.size >= MAX_SESSIONS) {
|
|
158
|
-
res.writeHead(503, { "Content-Type": "application/json" });
|
|
159
|
-
res.end(JSON.stringify({ error: "Too many active sessions. Try again later." }));
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
try {
|
|
163
|
-
const transport = new StreamableHTTPServerTransport({
|
|
164
|
-
sessionIdGenerator: () => randomUUID()
|
|
165
|
-
});
|
|
166
|
-
const { server: mcpServer } = await createMcpServer();
|
|
167
|
-
await mcpServer.connect(transport);
|
|
168
|
-
transport.onclose = () => {
|
|
169
|
-
for (const [id, s] of sessions) {
|
|
170
|
-
if (s.transport === transport) {
|
|
171
|
-
sessions.delete(id);
|
|
172
|
-
break;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
};
|
|
176
|
-
await transport.handleRequest(req, res);
|
|
177
|
-
const newSessionId = res.getHeader("mcp-session-id");
|
|
178
|
-
if (newSessionId) {
|
|
179
|
-
sessions.set(newSessionId, { transport, lastActivity: Date.now() });
|
|
180
|
-
}
|
|
181
|
-
} catch (err) {
|
|
182
|
-
if (!res.headersSent) {
|
|
183
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
184
|
-
res.end(JSON.stringify({ error: "Failed to create session" }));
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
if (req.method === "GET") {
|
|
190
|
-
const sessionId = req.headers["mcp-session-id"];
|
|
191
|
-
if (sessionId && sessions.has(sessionId)) {
|
|
192
|
-
sessions.get(sessionId).lastActivity = Date.now();
|
|
193
|
-
await sessions.get(sessionId).transport.handleRequest(req, res);
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
196
|
-
res.writeHead(sessionId ? 404 : 400, { "Content-Type": "application/json" });
|
|
197
|
-
res.end(JSON.stringify({ error: sessionId ? "Session expired or unknown \u2014 reinitialize." : "Session ID required for GET requests" }));
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
if (req.method === "DELETE") {
|
|
201
|
-
const sessionId = req.headers["mcp-session-id"];
|
|
202
|
-
if (sessionId && sessions.has(sessionId)) {
|
|
203
|
-
await sessions.get(sessionId).transport.handleRequest(req, res);
|
|
204
|
-
sessions.delete(sessionId);
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
208
|
-
res.end(JSON.stringify({ error: "Invalid session ID" }));
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
res.writeHead(405, { "Content-Type": "application/json" });
|
|
212
|
-
res.end(JSON.stringify({ error: "Method not allowed" }));
|
|
110
|
+
await handleMcp(req, res);
|
|
213
111
|
return;
|
|
214
112
|
}
|
|
215
113
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
@@ -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.1",
|
|
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": {
|