@esankhan3/code-search-mcp 0.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.
Files changed (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +333 -0
  3. package/dist/core/ast-graph-builder.js +800 -0
  4. package/dist/core/chunker.js +368 -0
  5. package/dist/core/claude-runner.js +173 -0
  6. package/dist/core/config.js +59 -0
  7. package/dist/core/cross-repo-detector.js +887 -0
  8. package/dist/core/embedder.js +334 -0
  9. package/dist/core/env-config.js +86 -0
  10. package/dist/core/file-walker.js +186 -0
  11. package/dist/core/git-diff.js +160 -0
  12. package/dist/core/graph-metrics.js +233 -0
  13. package/dist/core/indexer.js +552 -0
  14. package/dist/core/project-graph-builder.js +202 -0
  15. package/dist/core/query-classifier.js +144 -0
  16. package/dist/core/query-router.js +210 -0
  17. package/dist/core/rag-evaluator.js +213 -0
  18. package/dist/core/repo-profiler.js +383 -0
  19. package/dist/core/reranker.js +218 -0
  20. package/dist/core/retriever.js +242 -0
  21. package/dist/core/semantic-edge-detector.js +147 -0
  22. package/dist/core/service-mesh-inferrer.js +241 -0
  23. package/dist/core/structural-hasher.js +316 -0
  24. package/dist/core/tree-sitter-parser.js +539 -0
  25. package/dist/core/types.js +0 -0
  26. package/dist/core/vector-store.js +259 -0
  27. package/dist/core/workspace-detector.js +481 -0
  28. package/dist/index.js +177 -0
  29. package/dist/middleware/auth.js +109 -0
  30. package/dist/resources/resources.js +79 -0
  31. package/dist/server.js +242 -0
  32. package/dist/sources/github-org.js +109 -0
  33. package/dist/sources/local-path.js +44 -0
  34. package/dist/tools/graph.js +193 -0
  35. package/dist/tools/index-tools.js +43 -0
  36. package/dist/tools/profile.js +88 -0
  37. package/dist/tools/search.js +84 -0
  38. package/dist/transports/http-transport.js +162 -0
  39. package/dist/transports/remote-proxy.js +152 -0
  40. package/package.json +33 -0
@@ -0,0 +1,193 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { getKnowledgeBasePath } from "../core/config.js";
4
+ function registerGraphTools() {
5
+ return [
6
+ {
7
+ name: "get_repo_graph",
8
+ description: "Get the AST knowledge graph for a repo \u2014 entities (functions, classes, types) and their relationships (calls, imports, inheritance).",
9
+ inputSchema: {
10
+ type: "object",
11
+ properties: {
12
+ repo: { type: "string", description: "Repository name" }
13
+ },
14
+ required: ["repo"]
15
+ }
16
+ },
17
+ {
18
+ name: "get_cross_repo_edges",
19
+ description: "Get connections between repos \u2014 shared deps, Kafka topics, HTTP routes, database tables, gRPC services, etc.",
20
+ inputSchema: {
21
+ type: "object",
22
+ properties: {
23
+ repo: { type: "string", description: "Filter to edges involving this repo (optional)" }
24
+ }
25
+ }
26
+ },
27
+ {
28
+ name: "find_callers",
29
+ description: "Find all functions/methods that call a given function. Uses the AST graph.",
30
+ inputSchema: {
31
+ type: "object",
32
+ properties: {
33
+ function: { type: "string", description: "Function name to find callers of" },
34
+ repo: { type: "string", description: "Limit search to this repo (optional)" }
35
+ },
36
+ required: ["function"]
37
+ }
38
+ },
39
+ {
40
+ name: "find_dependencies",
41
+ description: "Find what a function depends on \u2014 calls, imports, type references.",
42
+ inputSchema: {
43
+ type: "object",
44
+ properties: {
45
+ function: { type: "string", description: "Function name" },
46
+ repo: { type: "string", description: "Limit to this repo (optional)" }
47
+ },
48
+ required: ["function"]
49
+ }
50
+ },
51
+ {
52
+ name: "impact_analysis",
53
+ description: "Analyze what would be affected if a file or entity is changed. Traces callers, dependents, and cross-repo connections.",
54
+ inputSchema: {
55
+ type: "object",
56
+ properties: {
57
+ file: { type: "string", description: "File path (relative to repo root)" },
58
+ entity: { type: "string", description: "Specific entity name (optional \u2014 if omitted, analyzes all entities in the file)" },
59
+ repo: { type: "string", description: "Repository name" }
60
+ },
61
+ required: ["file", "repo"]
62
+ }
63
+ }
64
+ ];
65
+ }
66
+ async function handleGraphTool(name, args, ctx) {
67
+ if (!["get_repo_graph", "get_cross_repo_edges", "find_callers", "find_dependencies", "impact_analysis"].includes(name)) return null;
68
+ try {
69
+ const kbPath = getKnowledgeBasePath(ctx.projectName);
70
+ if (name === "get_repo_graph") {
71
+ const repo = args.repo;
72
+ const graphPath = join(kbPath, repo, "graph.json");
73
+ if (!existsSync(graphPath)) {
74
+ return { content: [{ type: "text", text: `No graph found for repo "${repo}"` }] };
75
+ }
76
+ const graph = JSON.parse(readFileSync(graphPath, "utf-8"));
77
+ const summary = `# ${repo} AST Graph
78
+
79
+ - **Nodes:** ${graph.nodes?.length ?? 0}
80
+ - **Edges:** ${graph.links?.length ?? 0}
81
+
82
+ ## Entities
83
+ ${(graph.nodes ?? []).slice(0, 50).map((n) => `- \`${n.id}\` (${n.type})`).join("\n")}
84
+
85
+ ${graph.nodes?.length > 50 ? `... and ${graph.nodes.length - 50} more` : ""}`;
86
+ return { content: [{ type: "text", text: summary }] };
87
+ }
88
+ if (name === "get_cross_repo_edges") {
89
+ const sysGraphPath = join(kbPath, "system_graph_v2.json");
90
+ if (!existsSync(sysGraphPath)) {
91
+ return { content: [{ type: "text", text: "No system graph found. Build KB first." }] };
92
+ }
93
+ const sysGraph = JSON.parse(readFileSync(sysGraphPath, "utf-8"));
94
+ const edges = sysGraph.edges ?? [];
95
+ const repo = args.repo;
96
+ const relevant = repo ? edges.filter((e) => (e.source ?? "").startsWith(repo + "::") || (e.target ?? "").startsWith(repo + "::")) : edges;
97
+ const crossRepo = relevant.filter((e) => {
98
+ const src = (e.source ?? "").split("::")[0];
99
+ const tgt = (e.target ?? "").split("::")[0];
100
+ return src && tgt && src !== tgt;
101
+ });
102
+ if (crossRepo.length === 0) {
103
+ return { content: [{ type: "text", text: repo ? `No cross-repo edges found for "${repo}"` : "No cross-repo edges found" }] };
104
+ }
105
+ const text = crossRepo.slice(0, 50).map((e) => {
106
+ const attrs = e.attributes ?? {};
107
+ return `- ${(e.source ?? "").split("::")[0]} \u2192 ${(e.target ?? "").split("::")[0]} (${attrs.relation ?? attrs.type ?? "edge"})`;
108
+ }).join("\n");
109
+ return { content: [{ type: "text", text: `# Cross-Repo Edges${repo ? ` for ${repo}` : ""}
110
+
111
+ ${crossRepo.length} edges found:
112
+
113
+ ${text}${crossRepo.length > 50 ? `
114
+
115
+ ... and ${crossRepo.length - 50} more` : ""}` }] };
116
+ }
117
+ if (name === "find_callers" || name === "find_dependencies") {
118
+ const funcName = args.function;
119
+ const repoFilter = args.repo;
120
+ const sysGraphPath = join(kbPath, "system_graph_v2.json");
121
+ if (!existsSync(sysGraphPath)) {
122
+ return { content: [{ type: "text", text: "No system graph found." }] };
123
+ }
124
+ const sysGraph = JSON.parse(readFileSync(sysGraphPath, "utf-8"));
125
+ const edges = sysGraph.edges ?? [];
126
+ const matchingNodes = (sysGraph.nodes ?? []).filter((n) => {
127
+ const label = n.attributes?.label ?? n.key ?? "";
128
+ const matchesName = label.includes(funcName) || (n.key ?? "").includes(funcName);
129
+ const matchesRepo = !repoFilter || (n.key ?? "").startsWith(repoFilter + "::");
130
+ return matchesName && matchesRepo;
131
+ });
132
+ if (matchingNodes.length === 0) {
133
+ return { content: [{ type: "text", text: `No entity found matching "${funcName}"${repoFilter ? ` in ${repoFilter}` : ""}` }] };
134
+ }
135
+ const nodeKeys = new Set(matchingNodes.map((n) => n.key));
136
+ let results;
137
+ if (name === "find_callers") {
138
+ results = edges.filter((e) => nodeKeys.has(e.target)).map((e) => e.source);
139
+ } else {
140
+ results = edges.filter((e) => nodeKeys.has(e.source)).map((e) => e.target);
141
+ }
142
+ const unique = [...new Set(results)].slice(0, 30);
143
+ const direction = name === "find_callers" ? "Callers of" : "Dependencies of";
144
+ return { content: [{ type: "text", text: `# ${direction} "${funcName}"
145
+
146
+ ${unique.length} found:
147
+ ${unique.map((r) => `- \`${r}\``).join("\n")}` }] };
148
+ }
149
+ if (name === "impact_analysis") {
150
+ const file = args.file;
151
+ const repo = args.repo;
152
+ const entity = args.entity;
153
+ const sysGraphPath = join(kbPath, "system_graph_v2.json");
154
+ if (!existsSync(sysGraphPath)) {
155
+ return { content: [{ type: "text", text: "No system graph found." }] };
156
+ }
157
+ const sysGraph = JSON.parse(readFileSync(sysGraphPath, "utf-8"));
158
+ const edges = sysGraph.edges ?? [];
159
+ const nodes = sysGraph.nodes ?? [];
160
+ const fileNodes = nodes.filter((n) => {
161
+ const key = n.key ?? "";
162
+ const matchesFile = key.includes(`${repo}::${file}::`);
163
+ const matchesEntity = !entity || key.includes(entity);
164
+ return matchesFile && matchesEntity;
165
+ });
166
+ const nodeKeys = new Set(fileNodes.map((n) => n.key));
167
+ const dependents = edges.filter((e) => nodeKeys.has(e.target) && !nodeKeys.has(e.source));
168
+ const dependentRepos = new Set(dependents.map((e) => (e.source ?? "").split("::")[0]));
169
+ const text = [
170
+ `# Impact Analysis: ${repo}/${file}${entity ? `::${entity}` : ""}`,
171
+ "",
172
+ `## Entities in scope: ${fileNodes.length}`,
173
+ ...fileNodes.slice(0, 20).map((n) => `- \`${n.key}\``),
174
+ "",
175
+ `## Dependents: ${dependents.length} edges from ${dependentRepos.size} repos`,
176
+ ...dependents.slice(0, 30).map((e) => `- \`${e.source}\` \u2192 \`${e.target}\` (${(e.attributes ?? {}).relation ?? "edge"})`),
177
+ dependents.length > 30 ? `
178
+ ... and ${dependents.length - 30} more` : "",
179
+ "",
180
+ `## Affected repos: ${[...dependentRepos].join(", ") || "none"}`
181
+ ].join("\n");
182
+ return { content: [{ type: "text", text }] };
183
+ }
184
+ return null;
185
+ } catch (err) {
186
+ const msg = err instanceof Error ? err.message : String(err);
187
+ return { content: [{ type: "text", text: `Graph tool error: ${msg}` }] };
188
+ }
189
+ }
190
+ export {
191
+ handleGraphTool,
192
+ registerGraphTools
193
+ };
@@ -0,0 +1,43 @@
1
+ import { KnowledgeIndexer } from "../core/indexer.js";
2
+ const TOOL_NAMES = ["index_status"];
3
+ function registerIndexTools() {
4
+ return [
5
+ {
6
+ name: "index_status",
7
+ description: "Get current index stats \u2014 chunk count, embedding provider, repos indexed, last indexed time.",
8
+ inputSchema: {
9
+ type: "object",
10
+ properties: {}
11
+ }
12
+ }
13
+ ];
14
+ }
15
+ async function handleIndexTool(name, args, ctx) {
16
+ if (!TOOL_NAMES.includes(name)) return null;
17
+ try {
18
+ if (name === "index_status") {
19
+ const indexer = new KnowledgeIndexer();
20
+ const stats = await indexer.getStats(ctx.projectName);
21
+ const lines = [
22
+ `# Index Status: ${ctx.projectName}`,
23
+ "",
24
+ `- **Chunks:** ${stats.totalChunks.toLocaleString()}`,
25
+ `- **Embedding provider:** ${stats.embeddingProvider}`,
26
+ `- **Last indexed:** ${stats.lastIndexed || "never"}`,
27
+ `- **Repos:** ${stats.repos.length}`,
28
+ "",
29
+ "## Repos",
30
+ ...stats.repos.map((r) => `- ${r.name}: ${r.chunkCount} chunks`)
31
+ ];
32
+ return { content: [{ type: "text", text: lines.join("\n") }] };
33
+ }
34
+ return null;
35
+ } catch (err) {
36
+ const msg = err instanceof Error ? err.message : String(err);
37
+ return { content: [{ type: "text", text: `Index tool error: ${msg}` }] };
38
+ }
39
+ }
40
+ export {
41
+ handleIndexTool,
42
+ registerIndexTools
43
+ };
@@ -0,0 +1,88 @@
1
+ import { loadAllProfiles, loadProfile } from "../core/repo-profiler.js";
2
+ import { discoverRepos } from "../core/indexer.js";
3
+ function registerProfileTools() {
4
+ return [
5
+ {
6
+ name: "list_repos",
7
+ description: "List all indexed repos with their role, domain, and description.",
8
+ inputSchema: {
9
+ type: "object",
10
+ properties: {}
11
+ }
12
+ },
13
+ {
14
+ name: "get_repo_profile",
15
+ description: "Get the LLM-generated profile for a repo \u2014 role, domain, tech stack, exposed/consumed endpoints.",
16
+ inputSchema: {
17
+ type: "object",
18
+ properties: {
19
+ repo: { type: "string", description: "Repository name" }
20
+ },
21
+ required: ["repo"]
22
+ }
23
+ }
24
+ ];
25
+ }
26
+ async function handleProfileTool(name, args, ctx) {
27
+ if (!["list_repos", "get_repo_profile"].includes(name)) return null;
28
+ try {
29
+ if (name === "list_repos") {
30
+ const profiles = loadAllProfiles(ctx.projectName);
31
+ if (profiles.length === 0) {
32
+ if (ctx.directoryPath) {
33
+ const repos = discoverRepos(ctx.directoryPath);
34
+ const text2 = repos.map((r) => `- **${r.name}** (${r.language})`).join("\n");
35
+ return { content: [{ type: "text", text: `# Repos (${repos.length}, not yet profiled)
36
+
37
+ ${text2}` }] };
38
+ }
39
+ return { content: [{ type: "text", text: "No repos found. Run reindex first." }] };
40
+ }
41
+ const text = profiles.map(
42
+ (p) => `- **${p.name}** \u2014 ${p.role} | ${p.domain} | ${p.description}`
43
+ ).join("\n");
44
+ return { content: [{ type: "text", text: `# Indexed Repos (${profiles.length})
45
+
46
+ ${text}` }] };
47
+ }
48
+ if (name === "get_repo_profile") {
49
+ const repo = args.repo;
50
+ const profile = loadProfile(ctx.projectName, repo);
51
+ if (!profile) {
52
+ return { content: [{ type: "text", text: `No profile found for "${repo}". Run reindex to generate profiles.` }] };
53
+ }
54
+ const lines = [
55
+ `# ${profile.name}`,
56
+ "",
57
+ `**Role:** ${profile.role}`,
58
+ `**Domain:** ${profile.domain}`,
59
+ `**Description:** ${profile.description}`,
60
+ `**Technologies:** ${profile.technologies.join(", ")}`,
61
+ `**Entry points:** ${profile.entryPoints.join(", ")}`,
62
+ ""
63
+ ];
64
+ if (profile.exposes.length > 0) {
65
+ lines.push("## Exposes");
66
+ for (const e of profile.exposes) {
67
+ lines.push(`- **${e.type}:** \`${e.identifier}\` \u2014 ${e.description}`);
68
+ }
69
+ lines.push("");
70
+ }
71
+ if (profile.consumes.length > 0) {
72
+ lines.push("## Consumes");
73
+ for (const c of profile.consumes) {
74
+ lines.push(`- **${c.type}:** \`${c.identifier}\` \u2014 ${c.description}`);
75
+ }
76
+ }
77
+ return { content: [{ type: "text", text: lines.join("\n") }] };
78
+ }
79
+ return null;
80
+ } catch (err) {
81
+ const msg = err instanceof Error ? err.message : String(err);
82
+ return { content: [{ type: "text", text: `Profile tool error: ${msg}` }] };
83
+ }
84
+ }
85
+ export {
86
+ handleProfileTool,
87
+ registerProfileTools
88
+ };
@@ -0,0 +1,84 @@
1
+ import { getRetriever } from "../core/indexer.js";
2
+ function registerSearchTools() {
3
+ return [
4
+ {
5
+ name: "search_code",
6
+ description: "Hybrid search across all repos using vector + BM25 + graph expansion + reranking. Best for general queries.",
7
+ inputSchema: {
8
+ type: "object",
9
+ properties: {
10
+ query: { type: "string", description: "Search query (natural language or code identifier)" },
11
+ maxResults: { type: "number", description: "Max results to return (default: 10)" },
12
+ repos: { type: "array", items: { type: "string" }, description: "Filter to specific repos (optional)" }
13
+ },
14
+ required: ["query"]
15
+ }
16
+ },
17
+ {
18
+ name: "search_semantic",
19
+ description: 'Vector-only semantic search. Best for conceptual questions like "how does X work?"',
20
+ inputSchema: {
21
+ type: "object",
22
+ properties: {
23
+ query: { type: "string", description: "Search query" },
24
+ maxResults: { type: "number", description: "Max results (default: 10)" }
25
+ },
26
+ required: ["query"]
27
+ }
28
+ },
29
+ {
30
+ name: "search_exact",
31
+ description: "BM25 keyword search. Best for exact function names, error codes, file paths.",
32
+ inputSchema: {
33
+ type: "object",
34
+ properties: {
35
+ query: { type: "string", description: "Exact search query" },
36
+ maxResults: { type: "number", description: "Max results (default: 10)" }
37
+ },
38
+ required: ["query"]
39
+ }
40
+ }
41
+ ];
42
+ }
43
+ async function handleSearchTool(name, args, ctx) {
44
+ if (!["search_code", "search_semantic", "search_exact"].includes(name)) return null;
45
+ if (!ctx.indexReady) {
46
+ return { content: [{ type: "text", text: "Index not ready. Run reindex tool or wait for auto-indexing to complete." }] };
47
+ }
48
+ try {
49
+ const retriever = await getRetriever(ctx.projectName);
50
+ const query = args.query;
51
+ const maxResults = args.maxResults || 10;
52
+ const repos = args.repos;
53
+ const modeMap = {
54
+ search_code: "vector+bm25+graph",
55
+ search_semantic: "vector",
56
+ search_exact: "bm25"
57
+ };
58
+ const result = await retriever.retrieve(query, {
59
+ maxChunks: maxResults,
60
+ repoFilter: repos,
61
+ mode: modeMap[name]
62
+ });
63
+ if (result.chunks.length === 0) {
64
+ return { content: [{ type: "text", text: `No results found for "${query}"` }] };
65
+ }
66
+ const text = result.chunks.map((sc, i) => {
67
+ const c = sc.chunk;
68
+ return `### ${i + 1}. ${c.repoName}/${c.filePath}:${c.startLine} (score: ${sc.score.toFixed(3)}, source: ${sc.source})
69
+ \`\`\`${c.language}
70
+ ${c.content}
71
+ \`\`\``;
72
+ }).join("\n\n");
73
+ return { content: [{ type: "text", text: `Found ${result.chunks.length} results for "${query}" (${result.totalTokens} tokens):
74
+
75
+ ${text}` }] };
76
+ } catch (err) {
77
+ const msg = err instanceof Error ? err.message : String(err);
78
+ return { content: [{ type: "text", text: `Search failed: ${msg}` }] };
79
+ }
80
+ }
81
+ export {
82
+ handleSearchTool,
83
+ registerSearchTools
84
+ };
@@ -0,0 +1,162 @@
1
+ import { createServer } from "node:http";
2
+ import { randomUUID } from "node:crypto";
3
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
4
+ import { createAuthMiddleware } from "../middleware/auth.js";
5
+ async function startHttpTransport(opts) {
6
+ const { config, createMcpServer, onReady, getHealth } = opts;
7
+ const authenticate = createAuthMiddleware(config);
8
+ const sessions = /* @__PURE__ */ new Map();
9
+ const SESSION_TTL_MS = 30 * 60 * 1e3;
10
+ const MAX_SESSIONS = 100;
11
+ setInterval(() => {
12
+ const now = Date.now();
13
+ for (const [id, session] of sessions) {
14
+ if (now - session.lastActivity > SESSION_TTL_MS) {
15
+ sessions.delete(id);
16
+ }
17
+ }
18
+ }, 5 * 60 * 1e3).unref();
19
+ const httpServer = createServer(async (req, res) => {
20
+ const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
21
+ const path = url.pathname;
22
+ const requestId = randomUUID().slice(0, 8);
23
+ res.setHeader("X-Request-ID", requestId);
24
+ if (path === "/health" && req.method === "GET") {
25
+ const health = getHealth?.() ?? {};
26
+ res.writeHead(200, { "Content-Type": "application/json" });
27
+ res.end(JSON.stringify({
28
+ status: "ok",
29
+ activeSessions: sessions.size,
30
+ ...health
31
+ }));
32
+ return;
33
+ }
34
+ if (path === "/status" && req.method === "GET") {
35
+ const status = opts.getStatus?.() ?? {};
36
+ res.writeHead(200, { "Content-Type": "application/json" });
37
+ res.end(JSON.stringify(status, null, 2));
38
+ return;
39
+ }
40
+ if (path === "/index" && req.method === "POST") {
41
+ if (config.authEnabled) {
42
+ const identity = authenticate(req, res);
43
+ if (!identity) return;
44
+ }
45
+ let body = "";
46
+ req.on("data", (chunk) => {
47
+ body += chunk.toString();
48
+ });
49
+ req.on("end", async () => {
50
+ try {
51
+ const parsed = JSON.parse(body);
52
+ if (!parsed.path) {
53
+ res.writeHead(400, { "Content-Type": "application/json" });
54
+ res.end(JSON.stringify({ error: "`path` is required" }));
55
+ return;
56
+ }
57
+ if (!opts.onIndex) {
58
+ res.writeHead(501, { "Content-Type": "application/json" });
59
+ res.end(JSON.stringify({ error: "Indexing handler not configured" }));
60
+ return;
61
+ }
62
+ const result = await opts.onIndex(parsed);
63
+ res.writeHead(200, { "Content-Type": "application/json" });
64
+ res.end(JSON.stringify(result));
65
+ } catch (err) {
66
+ const msg = err instanceof Error ? err.message : String(err);
67
+ if (!res.headersSent) {
68
+ res.writeHead(500, { "Content-Type": "application/json" });
69
+ res.end(JSON.stringify({ error: msg }));
70
+ }
71
+ }
72
+ });
73
+ return;
74
+ }
75
+ if (path === "/mcp") {
76
+ if (config.authEnabled) {
77
+ const identity = authenticate(req, res);
78
+ if (!identity) return;
79
+ }
80
+ if (req.method === "POST") {
81
+ const sessionId = req.headers["mcp-session-id"];
82
+ if (sessionId && sessions.has(sessionId)) {
83
+ sessions.get(sessionId).lastActivity = Date.now();
84
+ await sessions.get(sessionId).transport.handleRequest(req, res);
85
+ return;
86
+ }
87
+ if (sessionId && !sessions.has(sessionId)) {
88
+ res.writeHead(400, { "Content-Type": "application/json" });
89
+ res.end(JSON.stringify({ error: "Invalid session ID. Session may have expired." }));
90
+ return;
91
+ }
92
+ if (sessions.size >= MAX_SESSIONS) {
93
+ res.writeHead(503, { "Content-Type": "application/json" });
94
+ res.end(JSON.stringify({ error: "Too many active sessions. Try again later." }));
95
+ return;
96
+ }
97
+ try {
98
+ const transport = new StreamableHTTPServerTransport({
99
+ sessionIdGenerator: () => randomUUID()
100
+ });
101
+ const { server: mcpServer } = await createMcpServer();
102
+ await mcpServer.connect(transport);
103
+ transport.onclose = () => {
104
+ for (const [id, s] of sessions) {
105
+ if (s.transport === transport) {
106
+ sessions.delete(id);
107
+ break;
108
+ }
109
+ }
110
+ };
111
+ await transport.handleRequest(req, res);
112
+ const newSessionId = res.getHeader("mcp-session-id");
113
+ if (newSessionId) {
114
+ sessions.set(newSessionId, { transport, lastActivity: Date.now() });
115
+ }
116
+ } catch (err) {
117
+ if (!res.headersSent) {
118
+ res.writeHead(500, { "Content-Type": "application/json" });
119
+ res.end(JSON.stringify({ error: "Failed to create session" }));
120
+ }
121
+ }
122
+ return;
123
+ }
124
+ if (req.method === "GET") {
125
+ const sessionId = req.headers["mcp-session-id"];
126
+ if (sessionId && sessions.has(sessionId)) {
127
+ await sessions.get(sessionId).transport.handleRequest(req, res);
128
+ return;
129
+ }
130
+ res.writeHead(400, { "Content-Type": "application/json" });
131
+ res.end(JSON.stringify({ error: "Session ID required for GET requests" }));
132
+ return;
133
+ }
134
+ if (req.method === "DELETE") {
135
+ const sessionId = req.headers["mcp-session-id"];
136
+ if (sessionId && sessions.has(sessionId)) {
137
+ await sessions.get(sessionId).transport.handleRequest(req, res);
138
+ sessions.delete(sessionId);
139
+ return;
140
+ }
141
+ res.writeHead(400, { "Content-Type": "application/json" });
142
+ res.end(JSON.stringify({ error: "Invalid session ID" }));
143
+ return;
144
+ }
145
+ res.writeHead(405, { "Content-Type": "application/json" });
146
+ res.end(JSON.stringify({ error: "Method not allowed" }));
147
+ return;
148
+ }
149
+ res.writeHead(404, { "Content-Type": "application/json" });
150
+ res.end(JSON.stringify({ error: "Not found" }));
151
+ });
152
+ return new Promise((resolve) => {
153
+ httpServer.listen(config.port, config.host, () => {
154
+ const url = `http://${config.host === "0.0.0.0" ? "localhost" : config.host}:${config.port}`;
155
+ onReady?.(url);
156
+ resolve();
157
+ });
158
+ });
159
+ }
160
+ export {
161
+ startHttpTransport
162
+ };