@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 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 sessions
249
- Per-session `StreamableHTTPServerTransport`, max 100 concurrent
250
- sessions, 30 min TTL. The remote proxy captures `mcp-session-id`
251
- for continuity. SSE transport available as a fallback.
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 sessions.** HTTP sessions are in-memory by design.
378
- Restart drops them; clients re-init on first request. No persistent
379
- session store to maintain.
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/sdk/server/index.js";
4
- import { Client } from "@modelcontextprotocol/sdk/client/index.js";
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(ListToolsRequestSchema, async () => ({
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: stats.repos.map((r) => ({
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) {
@@ -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([
@@ -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 { existsSync, readFileSync } from "node:fs";
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 openSystemGraphStore(kbPath);
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
- try {
41
- const NODE_LIMIT = 200;
42
- const EDGE_LIMIT = 200;
43
- const { rows: nodes, total: nodeTotal } = store.searchNodes({ minDegree: 0 }, NODE_LIMIT);
44
- const { edges: crossRepoEdges, total: edgeTotal } = store.crossRepoEdges(void 0, EDGE_LIMIT);
45
- const payload = {
46
- nodes,
47
- crossRepoEdges,
48
- totals: { nodes: nodeTotal, crossRepoEdges: edgeTotal },
49
- truncated: nodeTotal > nodes.length || edgeTotal > crossRepoEdges.length,
50
- note: "Bounded overview (top nodes by degree + cross-repo edges). Use the graph tools for full traversal."
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 graphPath = join(kbPath, graphMatch[1], "graph.json");
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: existsSync(graphPath) ? readFileSync(graphPath, "utf-8") : '{"nodes":[],"links":[]}'
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/sdk/server/index.js";
2
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
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 { join, resolve } from "node:path";
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 { getKnowledgeBasePath } from "@esankhan3/anvil-knowledge-core";
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.4.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(ListToolsRequestSchema, async () => ({
27
+ server.setRequestHandler("tools/list", async () => ({
34
28
  tools: allTools
35
29
  }));
36
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
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(ListResourcesRequestSchema, async () => ({
44
+ server.setRequestHandler("resources/list", async () => ({
51
45
  resources: allResources
52
46
  }));
53
- server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
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 kbPath = getKnowledgeBasePath(ctx.projectName);
336
- const hasLanceDB = existsSync(join(kbPath, "lancedb"));
337
- const hasGraph = existsSync(join(kbPath, "system_graph.sqlite")) || existsSync(join(kbPath, "system_graph_v2.json"));
338
- if (hasLanceDB && hasGraph) {
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;
@@ -1,13 +1,14 @@
1
- import { existsSync, readFileSync } from "node:fs";
1
+ import { existsSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import {
4
- getKnowledgeBasePath,
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 kbPath = getKnowledgeBasePath(ctx.projectName);
203
+ const blobs = getBlobStore(ctx.projectName, resolvedKnowledgeConfig());
203
204
  if (name === "get_repo_graph") {
204
205
  const repo = args.repo;
205
- const graphPath = join(kbPath, repo, "graph.json");
206
- if (!existsSync(graphPath)) return text(`No graph found for repo "${repo}"`);
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 openSystemGraphStore(kbPath);
220
+ const store = await getGraphStore(ctx.projectName, resolvedKnowledgeConfig());
221
221
  if (!store) return text(NO_GRAPH);
222
- try {
223
- const repo = args.repo;
224
- const { edges, total } = store.crossRepoEdges(repo, 50);
225
- if (total === 0) return text(repo ? `No cross-repo edges found for "${repo}"` : "No cross-repo edges found");
226
- const body = edges.map((e) => `- ${repoOf(e.source)} \u2192 ${repoOf(e.target)} (${e.type ?? "edge"})`).join("\n");
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 openSystemGraphStore(kbPath);
235
+ const store = await getGraphStore(ctx.projectName, resolvedKnowledgeConfig());
240
236
  if (!store) return text("No system graph found.");
241
- try {
242
- const funcName = args.function;
243
- const repoFilter = args.repo;
244
- const fuzzy = args.fuzzy === true;
245
- const matched = store.resolveNodes(funcName, repoFilter, fuzzy);
246
- if (matched.length === 0) {
247
- return text(`No entity found matching "${funcName}"${repoFilter ? ` in ${repoFilter}` : ""}. Try fuzzy:true for substring matching.`);
248
- }
249
- const keys = matched.map((m) => m.key);
250
- const unique = name === "find_callers" ? store.callers(keys, 30) : store.dependencies(keys, 30);
251
- const direction = name === "find_callers" ? "Callers of" : "Dependencies of";
252
- if (unique.length === 0) {
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
- return text(`# ${direction} "${funcName}"
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 openSystemGraphStore(kbPath);
258
+ const store = await getGraphStore(ctx.projectName, resolvedKnowledgeConfig());
267
259
  if (!store) return text("No system graph found.");
268
- try {
269
- const file = args.file;
270
- const repo = args.repo;
271
- const entity = args.entity;
272
- const fileKeys = store.nodesInFiles(repo, [file], entity);
273
- const { edges: dependents, total, repos } = store.dependents(fileKeys, 30);
274
- const body = [
275
- `# Impact Analysis: ${repo}/${file}${entity ? `::${entity}` : ""}`,
276
- "",
277
- `## Entities in scope: ${fileKeys.length}`,
278
- ...fileKeys.slice(0, 20).map((k) => `- \`${k}\``),
279
- "",
280
- `## Dependents: ${total} edges from ${repos.length} repos`,
281
- ...dependents.map((e) => `- \`${e.source}\` \u2192 \`${e.target}\` (${e.type ?? "edge"})`),
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
- `## Affected repos: ${repos.join(", ") || "none"}`
286
- ].join("\n");
287
- return text(body);
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 openSystemGraphStore(kbPath);
281
+ const store = await getGraphStore(ctx.projectName, resolvedKnowledgeConfig());
294
282
  if (!store) return text("No system graph found.");
295
- try {
296
- const from = args.from;
297
- const to = args.to;
298
- const repo = args.repo;
299
- const fuzzy = args.fuzzy === true;
300
- const direction = args.direction ?? "callees";
301
- const maxDepth = Math.max(1, Math.min(10, args.maxDepth || 4));
302
- const fromKeys = store.resolveNodes(from, repo, fuzzy).map((m) => m.key);
303
- if (fromKeys.length === 0) return text(`No entity found matching "${from}". Try fuzzy:true.`);
304
- const getNeighbors = makeNeighborGetter(store, direction);
305
- if (to) {
306
- const toKeys = new Set(store.resolveNodes(to, repo, fuzzy).map((m) => m.key));
307
- if (toKeys.size === 0) return text(`No entity found matching target "${to}". Try fuzzy:true.`);
308
- const path = shortestPath(getNeighbors, fromKeys, toKeys, maxDepth);
309
- if (!path) return text(`No path from "${from}" to "${to}" within ${maxDepth} hops (direction: ${direction}).`);
310
- const labelOf2 = store.labelsOf(path);
311
- const arrow = direction === "callers" ? " \u2190 " : " \u2192 ";
312
- const rendered = path.map((k) => labelOf2.get(k) ?? k).join(arrow);
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
- const nodes = reachable(getNeighbors, fromKeys, maxDepth);
320
- if (nodes.length === 0) return text(`No ${direction} reachable from "${from}" within ${maxDepth} hops.`);
321
- const labelOf = store.labelsOf(nodes.map((n) => n.key));
322
- const byDepth = /* @__PURE__ */ new Map();
323
- for (const { key, depth } of nodes) {
324
- if (!byDepth.has(depth)) byDepth.set(depth, []);
325
- byDepth.get(depth).push(`\`${labelOf.get(key) ?? key}\` (${key})`);
326
- }
327
- const sections = [...byDepth.entries()].sort((a, b) => a[0] - b[0]).map(([d, ks]) => `## Depth ${d}
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
- return text(`# Reachable from "${from}" (${direction}, \u2264${maxDepth} hops)
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 graphPath = join(kbPath, repo, "graph.json");
342
- if (!existsSync(graphPath)) return text(`No graph found for repo "${repo}"`);
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 pgPath = join(kbPath, "PROJECT_GRAPH.json");
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 summaryPath = join(kbPath, "PROJECT_SUMMARY.md");
411
- if (existsSync(summaryPath)) return text(readFileSync(summaryPath, "utf-8"));
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 openSystemGraphStore(kbPath);
405
+ const store = await getGraphStore(ctx.projectName, resolvedKnowledgeConfig());
424
406
  if (!store) return text(NO_GRAPH);
425
- try {
426
- const { rows, total } = store.searchNodes({
427
- name: args.name,
428
- type: args.type,
429
- file: args.file,
430
- repo: args.repo,
431
- minDegree: args.minDegree ?? 0
432
- }, args.limit || 50);
433
- if (total === 0) return text("No entities match the given filters.");
434
- const body = rows.map((m) => `- \`${m.label}\` (${m.type}, ${m.file}) \u2014 degree ${m.degree} \`${m.key}\``).join("\n");
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
- const metaPath = join(kbPath, repo, "index_meta.json");
456
- if (existsSync(metaPath)) {
457
- try {
458
- baseSha = JSON.parse(readFileSync(metaPath, "utf-8")).lastIndexedSha;
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 openSystemGraphStore(kbPath);
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
- try {
475
- changedKeys = store.nodesInFiles(repo, changedFiles);
476
- const d = store.dependents(changedKeys, limit);
477
- dependents = d.edges;
478
- depTotal = d.total;
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
  "",
@@ -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
  }
@@ -1,6 +1,5 @@
1
- import { existsSync } from "node:fs";
2
- import { join } from "node:path";
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
- let matches = await findChunksInFile(
125
- chunksPath,
126
- (c) => c.repoName === repo && c.filePath === file && (!entity || c.entityName === entity),
127
- 5
128
- );
129
- if (matches.length === 0 && entity) {
130
- matches = await findChunksInFile(chunksPath, (c) => c.repoName === repo && c.entityName === entity, 5);
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 { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
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 sessions = /* @__PURE__ */ new Map();
11
- const ttlMin = parseInt(process.env.CODE_SEARCH_SESSION_TTL_MINUTES ?? "", 10);
12
- const SESSION_TTL_MS = (Number.isFinite(ttlMin) && ttlMin > 0 ? ttlMin : 120) * 60 * 1e3;
13
- const maxEnv = parseInt(process.env.CODE_SEARCH_MAX_SESSIONS ?? "", 10);
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: "0.1.0",
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: "0.1.0",
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
- if (req.method === "POST" && opts.stateless) {
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/sdk/server/index.js";
2
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
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: "0.1.0" }
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: "0.1.0" },
94
+ { name: "code-search-mcp", version: "1.0.0" },
101
95
  { capabilities: { tools: {}, resources: {} } }
102
96
  );
103
- server.setRequestHandler(ListToolsRequestSchema, async () => {
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(CallToolRequestSchema, async (request) => {
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(ListResourcesRequestSchema, async () => {
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(ReadResourceRequestSchema, async (request) => {
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.12",
4
- "description": "MCP server for multi-repo code search — GitHub org or local path, zero config",
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.4.0",
27
- "@esankhan3/anvil-knowledge-core": "^0.3.1",
28
- "@modelcontextprotocol/sdk": "^1.29.0"
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": {