codesearch-mcp 0.0.13-alpha → 0.0.15-alpha

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/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+ Format based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
5
+
6
+ ## [0.0.15-alpha] - 2026-05-02
7
+
8
+ ### Changed
9
+
10
+ - **MCP server instructions restructured** — `Step 0` (query classification table) moved before the `Tools` list so it arrives before the truncation threshold and is always visible to the model
11
+ - **Lookup workflow fixed** — corrected order to `find_symbol` first (faster, exact match), `search_codebase` as fallback
12
+ - **Path distinction clarified** — `read_file_range` (relative path) and `read_file` (absolute path) now labelled directly in the Tools list, reducing wrong-path errors
13
+ - **Threshold contradiction resolved** — removed conflicting `0.35` example from Lookup/Conceptual workflow; canonical threshold guidance kept only in Relevance scores section
package/README.md CHANGED
@@ -34,7 +34,7 @@ rag-setup
34
34
  Walks you through:
35
35
  - **Local Docker** — copies `docker-compose.yml`, starts PostgreSQL + Ollama, pulls `bge-m3` automatically
36
36
  - **Remote DB** — enter connection details for Supabase, Railway, or self-hosted; connection is verified before saving
37
- - **Logging** — choose log level and file path (defaults: `error` level, `~/.config/codesearch-mcp/rag.log`)
37
+ - **Logging** — choose log level and directory (defaults: `error` level, `~/.config/codesearch-mcp/logs/`)
38
38
 
39
39
  Config is saved to `~/.config/codesearch-mcp/.env`.
40
40
 
@@ -102,18 +102,18 @@ src/generated/
102
102
 
103
103
  ## Logging
104
104
 
105
- Logs are written to `~/.config/codesearch-mcp/rag.log` by default. Only errors are logged unless configured otherwise.
105
+ Logs are written to `~/.config/codesearch-mcp/logs/` by default (separate subdirectories for indexer and MCP). Only errors are logged unless configured otherwise.
106
106
 
107
107
  ```bash
108
108
  # Read logs
109
- tail -f ~/.config/codesearch-mcp/rag.log
110
- npx pino-pretty < ~/.config/codesearch-mcp/rag.log
109
+ tail -f ~/.config/codesearch-mcp/logs/indexer/current.log
110
+ npx pino-pretty < ~/.config/codesearch-mcp/logs/indexer/current.log
111
111
 
112
112
  # Detailed indexing diagnostics (timing per file: deps / embed / db)
113
113
  LOG_LEVEL=debug rag-index /path/to/workspace
114
114
 
115
115
  # Disable file logging
116
- LOG_FILE=false rag-index /path/to/workspace
116
+ LOG_DIR=false rag-index /path/to/workspace
117
117
  ```
118
118
 
119
119
  ## Environment variables
@@ -134,7 +134,7 @@ All set by `rag-setup`. Override any of them in `~/.config/codesearch-mcp/.env`.
134
134
  | `AUTO_TRANSLATE` | `false` | Translate non-English queries to English before embedding |
135
135
  | `TRANSLATE_MODEL` | `llama3.2` | Ollama model for translation (requires `ollama pull llama3.2`) |
136
136
  | `LOG_LEVEL` | `error` | Log level written to file: `error` \| `warn` \| `info` \| `debug` |
137
- | `LOG_FILE` | `~/.config/codesearch-mcp/rag.log` | Log file path, or `false` to disable |
137
+ | `LOG_DIR` | `~/.config/codesearch-mcp/logs` | Base log directory, or `false` to disable |
138
138
 
139
139
  ## License
140
140
 
package/dist/app.js CHANGED
@@ -53,7 +53,8 @@ async function main() {
53
53
  (0, help_1.printHelp)(argv1 !== 'app.js' ? argv1 : mode);
54
54
  process.exit(0);
55
55
  }
56
- const container = (0, container_1.createContainer)();
56
+ const loggerName = mode === 'index' || mode === 'backfill' ? 'indexer' : mode === 'mcp' ? 'mcp' : mode === 'setup' ? 'setup' : undefined;
57
+ const container = (0, container_1.createContainer)(loggerName);
57
58
  if (!container.config.workspaceRoot && mode !== 'setup') {
58
59
  const hint = argv1 === 'rag-mcp'
59
60
  ? 'Set WORKSPACE_ROOT in ~/.config/codesearch-mcp/.env or run `rag-setup`.'
@@ -64,7 +65,7 @@ async function main() {
64
65
  const commands = {
65
66
  index: new IndexCommand_1.IndexCommand(container, argv1),
66
67
  mcp: new McpCommand_1.McpCommand(container),
67
- setup: new SetupCommand_1.SetupCommand(),
68
+ setup: new SetupCommand_1.SetupCommand(container),
68
69
  backfill: new BackfillCommand_1.BackfillCommand(container),
69
70
  };
70
71
  const cmd = commands[mode];
@@ -7,9 +7,22 @@ class McpCommand {
7
7
  this.container = container;
8
8
  }
9
9
  async execute() {
10
- const { db, mcp } = this.container;
11
- await db.initDb();
12
- await mcp.start();
10
+ const { db, mcp, logger } = this.container;
11
+ let startupError;
12
+ try {
13
+ await db.initDb();
14
+ }
15
+ catch (err) {
16
+ startupError = err;
17
+ logger.error('McpCommand: database connection failed', { reason: err.message });
18
+ }
19
+ try {
20
+ await mcp.start(startupError);
21
+ }
22
+ catch (err) {
23
+ logger.error('McpCommand: MCP server failed to start', { reason: err.message });
24
+ return;
25
+ }
13
26
  // server.connect() resolves immediately after setup; keep the process alive
14
27
  // until the MCP client disconnects (stdin closes)
15
28
  await new Promise(resolve => {
@@ -1,4 +1,7 @@
1
+ import type { Container } from '../container';
1
2
  import type { ICommand } from './ICommand';
2
3
  export declare class SetupCommand implements ICommand {
4
+ private container;
5
+ constructor(container: Container);
3
6
  execute(): Promise<void>;
4
7
  }
@@ -3,8 +3,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SetupCommand = void 0;
4
4
  const setup_1 = require("../setup");
5
5
  class SetupCommand {
6
+ container;
7
+ constructor(container) {
8
+ this.container = container;
9
+ }
6
10
  async execute() {
7
- await (0, setup_1.runSetup)();
11
+ await (0, setup_1.runSetup)(this.container.logger);
8
12
  }
9
13
  }
10
14
  exports.SetupCommand = SetupCommand;
@@ -12,4 +12,4 @@ export interface Container {
12
12
  mcp: McpServer;
13
13
  logger: LoggerService;
14
14
  }
15
- export declare function createContainer(): Container;
15
+ export declare function createContainer(loggerName?: string): Container;
@@ -9,8 +9,8 @@ const IndexerService_1 = require("./services/IndexerService");
9
9
  const SearcherService_1 = require("./services/SearcherService");
10
10
  const McpServer_1 = require("./services/McpServer");
11
11
  const LoggerService_1 = require("./services/LoggerService");
12
- function createContainer() {
13
- const logger = LoggerService_1.LoggerService.fromEnv();
12
+ function createContainer(loggerName) {
13
+ const logger = LoggerService_1.LoggerService.fromEnv(loggerName);
14
14
  const db = new DatabaseService_1.DatabaseService(config_1.config);
15
15
  const embedder = new OllamaEmbedder_1.OllamaEmbedder(config_1.config);
16
16
  const ast = new AstParserService_1.AstParserService(logger);
@@ -37,37 +37,49 @@ exports.selectProjects = selectProjects;
37
37
  const fs = __importStar(require("fs"));
38
38
  const path = __importStar(require("path"));
39
39
  const prompts_1 = require("@inquirer/prompts");
40
- function loadLines(filePath) {
41
- if (!fs.existsSync(filePath))
40
+ async function loadLines(filePath) {
41
+ try {
42
+ const content = await fs.promises.readFile(filePath, 'utf-8');
43
+ return new Set(content.split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#')));
44
+ }
45
+ catch (err) {
46
+ if (err.code !== 'ENOENT')
47
+ throw err;
42
48
  return new Set();
43
- return new Set(fs.readFileSync(filePath, 'utf-8')
44
- .split('\n')
45
- .map(l => l.trim())
46
- .filter(l => l && !l.startsWith('#')));
49
+ }
47
50
  }
48
51
  // Write the set if non-empty, otherwise delete the file so state is deterministic.
49
52
  // Previously .ragskip could be left stale (last entry removed but file not rewritten);
50
53
  // treating both files the same way avoids that drift.
51
- function saveLines(filePath, names) {
54
+ async function saveLines(filePath, names) {
52
55
  if (names.size > 0) {
53
- fs.writeFileSync(filePath, [...names].sort().join('\n') + '\n');
56
+ await fs.promises.writeFile(filePath, [...names].sort().join('\n') + '\n');
54
57
  }
55
- else if (fs.existsSync(filePath)) {
56
- fs.unlinkSync(filePath);
58
+ else {
59
+ try {
60
+ await fs.promises.unlink(filePath);
61
+ }
62
+ catch (err) {
63
+ if (err.code !== 'ENOENT')
64
+ throw err;
65
+ }
57
66
  }
58
67
  }
59
- function loadConfig(workspaceDir) {
60
- return {
61
- included: loadLines(path.join(workspaceDir, '.ragprojects')),
62
- skipped: loadLines(path.join(workspaceDir, '.ragskip')),
63
- };
68
+ async function loadConfig(workspaceDir) {
69
+ const [included, skipped] = await Promise.all([
70
+ loadLines(path.join(workspaceDir, '.ragprojects')),
71
+ loadLines(path.join(workspaceDir, '.ragskip')),
72
+ ]);
73
+ return { included, skipped };
64
74
  }
65
- function saveConfig(workspaceDir, config) {
66
- saveLines(path.join(workspaceDir, '.ragprojects'), config.included);
67
- saveLines(path.join(workspaceDir, '.ragskip'), config.skipped);
75
+ async function saveConfig(workspaceDir, config) {
76
+ await Promise.all([
77
+ saveLines(path.join(workspaceDir, '.ragprojects'), config.included),
78
+ saveLines(path.join(workspaceDir, '.ragskip'), config.skipped),
79
+ ]);
68
80
  }
69
81
  async function selectProjects(allProjects, workspaceDir, forceSelect) {
70
- const config = loadConfig(workspaceDir);
82
+ const config = await loadConfig(workspaceDir);
71
83
  const newProjects = allProjects.filter(p => !config.included.has(p.name) && !config.skipped.has(p.name));
72
84
  // Nothing to ask — return previously saved selection silently
73
85
  if (newProjects.length === 0 && !forceSelect) {
@@ -121,7 +133,7 @@ async function selectProjects(allProjects, workspaceDir, forceSelect) {
121
133
  for (const name of toSkip)
122
134
  config.skipped.add(name);
123
135
  }
124
- saveConfig(workspaceDir, config);
136
+ await saveConfig(workspaceDir, config);
125
137
  const selected = allProjects.filter(p => config.included.has(p.name));
126
138
  console.log(`\nSelected: ${selected.map(p => p.name).join(', ') || '(none)'}`);
127
139
  return selected;
@@ -43,6 +43,7 @@ const FALLBACK_OPTS = {
43
43
  skipFileDependencyResolution: false,
44
44
  compilerOptions: { allowJs: true, noEmit: true },
45
45
  };
46
+ const MORPH_CACHE_MAX = 10;
46
47
  class AstParserService {
47
48
  logger;
48
49
  morphProjects = new Map();
@@ -75,38 +76,49 @@ class AstParserService {
75
76
  const { edges } = await this.analyzeFile(filePath, serviceName, projectRoot);
76
77
  return edges;
77
78
  }
78
- // ── private: MorphProject cache ────────────────────────────────────────────
79
+ // ── private: MorphProject cache (LRU, max MORPH_CACHE_MAX entries) ──────────
79
80
  getMorphProject(projectRoot) {
80
- if (!this.morphProjects.has(projectRoot)) {
81
- const tsconfigPath = path.join(projectRoot, 'tsconfig.json');
82
- const hasTsconfig = fs.existsSync(tsconfigPath);
83
- this.logger.debug('AstParser: tsconfig lookup', { projectRoot, found: hasTsconfig });
84
- let project;
85
- try {
86
- const opts = hasTsconfig
87
- ? { tsConfigFilePath: tsconfigPath, skipAddingFilesFromTsConfig: true, skipFileDependencyResolution: false }
88
- : FALLBACK_OPTS;
89
- project = new ts_morph_1.Project(opts);
90
- if (hasTsconfig) {
91
- const co = project.getCompilerOptions();
92
- const pathsMap = co.paths ?? {};
93
- const pathCount = Object.keys(pathsMap).length;
94
- this.logger.debug('AstParser: tsconfig loaded', { pathCount });
95
- if (pathCount > 0) {
96
- this.logger.debug('AstParser: path aliases', { aliases: Object.keys(pathsMap) });
97
- }
81
+ if (this.morphProjects.has(projectRoot)) {
82
+ // Move to end = most recently used
83
+ const cached = this.morphProjects.get(projectRoot);
84
+ this.morphProjects.delete(projectRoot);
85
+ this.morphProjects.set(projectRoot, cached);
86
+ return cached;
87
+ }
88
+ if (this.morphProjects.size >= MORPH_CACHE_MAX) {
89
+ // Evict least recently used (first entry in insertion-order Map)
90
+ const lruKey = this.morphProjects.keys().next().value;
91
+ this.morphProjects.delete(lruKey);
92
+ this.logger.debug('AstParser: evicted LRU project', { lruKey });
93
+ }
94
+ const tsconfigPath = path.join(projectRoot, 'tsconfig.json');
95
+ const hasTsconfig = fs.existsSync(tsconfigPath);
96
+ this.logger.debug('AstParser: tsconfig lookup', { projectRoot, found: hasTsconfig });
97
+ let project;
98
+ try {
99
+ const opts = hasTsconfig
100
+ ? { tsConfigFilePath: tsconfigPath, skipAddingFilesFromTsConfig: true, skipFileDependencyResolution: false }
101
+ : FALLBACK_OPTS;
102
+ project = new ts_morph_1.Project(opts);
103
+ if (hasTsconfig) {
104
+ const co = project.getCompilerOptions();
105
+ const pathsMap = co.paths ?? {};
106
+ const pathCount = Object.keys(pathsMap).length;
107
+ this.logger.debug('AstParser: tsconfig loaded', { pathCount });
108
+ if (pathCount > 0) {
109
+ this.logger.debug('AstParser: path aliases', { aliases: Object.keys(pathsMap) });
98
110
  }
99
111
  }
100
- catch (err) {
101
- this.logger.warn('AstParser: tsconfig load failed, using fallback', {
102
- tsconfigPath,
103
- reason: err.message,
104
- });
105
- project = new ts_morph_1.Project(FALLBACK_OPTS);
106
- }
107
- this.morphProjects.set(projectRoot, project);
108
112
  }
109
- return this.morphProjects.get(projectRoot);
113
+ catch (err) {
114
+ this.logger.warn('AstParser: tsconfig load failed, using fallback', {
115
+ tsconfigPath,
116
+ reason: err.message,
117
+ });
118
+ project = new ts_morph_1.Project(FALLBACK_OPTS);
119
+ }
120
+ this.morphProjects.set(projectRoot, project);
121
+ return project;
110
122
  }
111
123
  // ── private: edge extraction (imports + re-exports) ────────────────────────
112
124
  extractEdges(sourceFile, filePath, serviceName) {
@@ -108,7 +108,8 @@ class DatabaseService {
108
108
  await client.connect();
109
109
  const { rows } = await client.query(`SELECT 1 FROM pg_database WHERE datname = $1`, [database]);
110
110
  if (rows.length === 0) {
111
- await client.query(`CREATE DATABASE "${database}"`);
111
+ const safeName = database.replace(/"/g, '""');
112
+ await client.query(`CREATE DATABASE "${safeName}"`);
112
113
  await client.end();
113
114
  const extClient = new pg_1.Client({ host, port, user: username, password, database });
114
115
  try {
@@ -126,7 +127,8 @@ class DatabaseService {
126
127
  }
127
128
  catch (err) {
128
129
  await client.end().catch(() => { });
129
- throw new Error(`Failed to ensure database "${database}" exists: ${err.message}`);
130
+ const msg = err instanceof Error ? err.message : String(err);
131
+ throw new Error(`Failed to ensure database "${database}" exists: ${msg}`);
130
132
  }
131
133
  }
132
134
  }
@@ -4,7 +4,7 @@ export declare class LoggerService {
4
4
  private readonly logger;
5
5
  constructor(logger: pino.Logger);
6
6
  static silent(): LoggerService;
7
- static fromEnv(): LoggerService;
7
+ static fromEnv(name?: string): LoggerService;
8
8
  debug(msg: string, data?: Record<string, unknown>): void;
9
9
  info(msg: string, data?: Record<string, unknown>): void;
10
10
  warn(msg: string, data?: Record<string, unknown>): void;
@@ -41,7 +41,7 @@ const pino_1 = __importDefault(require("pino"));
41
41
  const path = __importStar(require("path"));
42
42
  const os = __importStar(require("os"));
43
43
  const fs = __importStar(require("fs"));
44
- const DEFAULT_LOG_FILE = path.join(os.homedir(), '.config', 'codesearch-mcp', 'rag.log');
44
+ const DEFAULT_LOG_DIR = path.join(os.homedir(), '.config', 'codesearch-mcp', 'logs');
45
45
  class LoggerService {
46
46
  logger;
47
47
  constructor(logger) {
@@ -50,11 +50,17 @@ class LoggerService {
50
50
  static silent() {
51
51
  return new LoggerService((0, pino_1.default)({ level: 'silent' }));
52
52
  }
53
- static fromEnv() {
53
+ static fromEnv(name) {
54
54
  const level = (process.env.LOG_LEVEL ?? 'error');
55
- const logFileEnv = process.env.LOG_FILE;
56
- const enabled = logFileEnv !== 'false' && logFileEnv !== '0';
57
- const filePath = enabled ? (logFileEnv || DEFAULT_LOG_FILE) : null;
55
+ const logDirEnv = process.env.LOG_DIR;
56
+ const enabled = logDirEnv !== 'false' && logDirEnv !== '0';
57
+ let filePath = null;
58
+ if (enabled) {
59
+ const logDir = logDirEnv || DEFAULT_LOG_DIR;
60
+ filePath = name
61
+ ? path.join(logDir, name, `${name}.log`)
62
+ : path.join(logDir, 'rag.log');
63
+ }
58
64
  if (filePath)
59
65
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
60
66
  const transport = filePath
@@ -6,7 +6,7 @@ export declare class McpServer {
6
6
  private config;
7
7
  private logger;
8
8
  constructor(searcher: SearcherService, config: Config, logger?: LoggerService);
9
- start(): Promise<void>;
9
+ start(startupError?: Error): Promise<void>;
10
10
  private formatChunk;
11
11
  private formatDependencies;
12
12
  private formatDependents;
@@ -48,48 +48,77 @@ const PKG_VERSION = (() => {
48
48
  return '0.0.0';
49
49
  }
50
50
  })();
51
- const SERVER_INSTRUCTIONS = `
52
- You have access to a local semantic code search server. Use these tools to navigate unfamiliar codebases.
51
+ function buildInstructions(t) {
52
+ return `
53
+ You have access to a local code intelligence server. Use these tools to search codebases by meaning, navigate import graphs, look up symbol definitions, and read source files.
54
+
55
+ ## Step 0 — Classify your query before starting
56
+
57
+ Pick one workflow based on what you need:
58
+
59
+ | Type | Signals | Workflow |
60
+ |---|---|---|
61
+ | **Lookup** | "where is X defined", "find Y" | find_symbol (if name known) → search_codebase (if not found) |
62
+ | **Conceptual** | "how does X work", "what is Y" | search_codebase → read_file_range |
63
+ | **Flow audit** | "how does X connect to Y", "audit this flow", "trace the path" | search_codebase → **get_dependencies + get_dependents** → Grep |
64
+ | **Impact** | "what would break if I change X", "what uses Z" | get_dependents → search_codebase for context |
53
65
 
54
66
  ## Tools
55
67
 
56
68
  - **list_projects** — call this first to see what projects are indexed and their exact names.
57
69
  - **search_codebase** — semantic search across indexed source files. Returns file path, line number, and a code chunk.
58
- - **read_file_range** — read a specific line range from any indexed file. Use this to expand context around a search result.
70
+ - **read_file_range** — read a specific line range from any indexed file. Expects a **relative path** as returned by search_codebase.
59
71
  - **get_dependencies** — show what files a given file imports (direct or transitive). Use to understand what a file depends on.
60
72
  - **get_dependents** — show which files import a given file (reverse graph). Use before refactoring to find all callers.
61
73
  - **find_symbol** — find where a symbol (function, class, type, interface, variable, method) is defined by exact name. Faster than semantic search when you know the name.
62
74
  - **list_files** — browse the directory tree of a project path. Use to understand project structure before reading files.
63
- - **read_file** — read a complete file by absolute path with line numbers. Truncates at max_lines with a hint to continue.
75
+ - **read_file** — read a complete file by **absolute path** with line numbers. Truncates at max_lines with a hint to continue.
64
76
 
65
77
  ## Recommended workflow
66
78
 
79
+ ### Lookup / Conceptual
67
80
  1. Use \`list_projects\` to discover available projects before filtering by \`project\`.
68
- 2. Use \`search_codebase\` to find relevant code by meaning.
69
- 3. If a result looks **truncated or incomplete** (e.g. you see only the middle of a function), call \`read_file_range\` with the returned \`file_path\` and \`start_line\` set to the **Line** number shown in the result. Example: if the result shows **Line:** 150, pass \`start_line: 150\`.
70
- 4. For exploratory searches where you expect results to span chunk boundaries, pass \`include_neighbors: true\` to automatically include the chunk before and after each match.
71
- 5. Use \`find_symbol\` when you know the exact name of a function, class, or method and want its definition location immediately — faster than semantic search. Use \`kind: "method"\` to find public class methods like \`canActivate\`, \`execute\`, \`handle\`.
72
- 6. Use \`list_files\` to explore project structure (src/, components/, etc.) before diving into files.
73
- 7. Use \`read_file\` to read a complete file when you have its absolute path from \`search_codebase\` results.
74
- 8. If \`search_codebase\` returns results that seem off-topic, pass a lower \`threshold\` (e.g. \`0.35\`) to filter out weak semantic matches.
81
+ 2. Use \`search_codebase\` to find relevant code by meaning. If your query uses an ambiguous term (e.g. "subscription" could mean pub/sub or billing), pass a lower \`threshold\` (e.g. \`0.40–0.45\`) to filter out weak matches.
82
+ 3. If a result looks **truncated or incomplete**, call \`read_file_range\` with the returned \`file_path\` and \`start_line\`.
83
+ 4. For exploratory searches where results span chunk boundaries, pass \`include_neighbors: true\`.
84
+ 5. Use \`find_symbol\` when you know the exact name — faster than semantic search. Use \`kind: "method"\` for class methods — grep matches lines where \`public\`, \`async\`, \`static\`, or \`override\` appears before the name.
85
+ 6. Use \`list_files\` to explore project structure before diving into files. Use \`read_file\` to read a complete file by absolute path.
75
86
 
76
- ## When to use read_file_range vs include_neighbors
87
+ ### Flow audit (mandatory steps — do not skip 3 and 4)
88
+ 1. Use \`list_projects\` to confirm project names.
89
+ 2. Use \`search_codebase\` to find the **central file** (entry point of the flow).
90
+ 3. **\`get_dependencies(central_file)\`** — what does this file import? Catches configs, loaders, Redis clients, types.
91
+ 4. **\`get_dependents(central_file)\`** — who calls this file? Catches resolvers, routes, socket handlers.
92
+ 5. For each peripheral file found in steps 3–4: use \`read_file_range\` or \`find_symbol\` to inspect it.
93
+ 6. Only after steps 3–4 draw architectural conclusions — \`search_codebase\` alone misses peripheral files because they are semantically thin (configs, types, loaders contain no domain vocabulary).
77
94
 
78
- - **include_neighbors: true** — good when you already know the context will be fragmented (e.g. searching for a class definition that's likely split across chunks).
79
- - **read_file_range** — better when you want to read a precise section, or when a result looks cut off and you want to expand just that one file.
95
+ > **Note:** If \`get_dependencies\` / \`get_dependents\` return empty, the file likely uses path aliases (e.g. \`@core/\`, \`@/\`, \`@db/\`). Fall back to \`Grep\` on the symbol name across the project.
80
96
 
81
- ## Handling Code Structure
82
- - **Class/Function limits**: Chunks often end abruptly. Always use \`read_file_range\` to find the closing brace or the beginning of a method if you suspect you're seeing only a fragment.
83
- - **Cross-file references**: If you see a method call like \`this.validator.validate(data)\`, use \`search_codebase\` to find the definition of that \`validator\` service.
97
+ ### Impact analysis
98
+ 1. \`get_dependents(target_file)\` — direct callers.
99
+ 2. \`get_dependents(target_file, transitive: true)\` — full reverse graph.
100
+ 3. \`search_codebase\` for context on unfamiliar dependents.
84
101
 
85
- ## Relevance scores
102
+ ## When to use read_file_range vs include_neighbors
86
103
 
87
- Lower score = better match (cosine distance). Results above the configured threshold are filtered out automatically. Pass \`threshold\` to \`search_codebase\` to override the default per-query (e.g. \`0.35\` for strict matching, \`0.55\` for broader results).
104
+ - **include_neighbors: true** — good when context is fragmented across chunks (e.g. a class definition split across chunks).
105
+ - **read_file_range** — better for a precise section, or when one result is cut off.
106
+
107
+ ## Handling Code Structure
108
+ - **Class/Function limits**: Chunks often end abruptly. Use \`read_file_range\` to find the closing brace or method start.
109
+ - **Cross-file references**: If you see \`this.validator.validate(data)\`, use \`search_codebase\` to find \`validator\`.
88
110
 
89
111
  ## Dependency graph
90
112
 
91
113
  \`get_dependencies\` and \`get_dependents\` require \`file_path\` (as returned by \`search_codebase\`) and \`project\` (from \`list_projects\`). Pass \`transitive: true\` for the full dependency chain. depType values: \`import\` | \`import-default\` | \`import-namespace\` | \`reexport\` | \`reexport-namespace\`. \`fromSymbol\` — the top-level class or function in the importing file that uses this import (null if ambiguous or not tracked).
114
+
115
+ If \`get_dependents\` returns 0 results, add \`symbol\` (the exported name you care about) — this triggers a grep fallback that finds runtime usages in DI containers, string references, and dynamic lookups that the import graph misses.
116
+
117
+ ## Relevance scores
118
+
119
+ Lower score = better match (cosine distance). Results above the configured threshold are filtered out automatically. Treat scores above 0.50 as weak hints — verify with \`read_file_range\` or \`Grep\`. Pass \`threshold\` to \`search_codebase\` to override per-query: \`0.40–0.45\` for precise technical queries (known term, exact concept); \`${t}\` default (broad but reliable). Avoid \`0.35\` — too strict for DSL files (GraphQL, Prisma) and infrastructure code.
92
120
  `.trim();
121
+ }
93
122
  class McpServer {
94
123
  searcher;
95
124
  config;
@@ -99,8 +128,8 @@ class McpServer {
99
128
  this.config = config;
100
129
  this.logger = logger;
101
130
  }
102
- async start() {
103
- const server = new index_js_1.Server({ name: 'local-rag', version: PKG_VERSION }, { capabilities: { tools: {} }, instructions: SERVER_INSTRUCTIONS });
131
+ async start(startupError) {
132
+ const server = new index_js_1.Server({ name: 'local-rag', version: PKG_VERSION }, { capabilities: { tools: {} }, instructions: buildInstructions(this.config.search.similarityThreshold) });
104
133
  server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
105
134
  tools: [
106
135
  {
@@ -113,14 +142,14 @@ class McpServer {
113
142
  limit: { type: 'number', description: `Max results (default ${this.config.search.defaultLimit}, max ${this.config.search.maxLimit})` },
114
143
  project: { type: 'string', description: 'Filter by project name (optional). Use list_projects to see available names.' },
115
144
  include_neighbors: { type: 'boolean', description: 'Also include the chunk before and after each result from the same file (Small-to-Big retrieval). Useful when results look truncated.' },
116
- threshold: { type: 'number', description: `Max cosine distance (0–1). Lower = stricter. Default: ${this.config.search.similarityThreshold}. Use a lower value (e.g. 0.35) to filter out weak matches.` },
145
+ threshold: { type: 'number', description: `Max cosine distance (0–1). Lower = stricter. Default: ${this.config.search.similarityThreshold}. Use 0.40–0.45 for precise technical queries, 0.55–0.60 for broad exploration.` },
117
146
  },
118
147
  required: ['query'],
119
148
  },
120
149
  },
121
150
  {
122
151
  name: 'list_projects',
123
- description: 'List all indexed projects with their chunk counts',
152
+ description: 'List all indexed projects with their chunk counts and metadata (description, stack, notes, relations from .ragmeta.json).',
124
153
  inputSchema: { type: 'object', properties: {} },
125
154
  },
126
155
  {
@@ -193,11 +222,11 @@ class McpServer {
193
222
  },
194
223
  {
195
224
  name: 'read_file',
196
- description: 'Read a file from the workspace by absolute path (as returned by search_codebase). Returns contents with line numbers. Truncates at max_lines with a hint to continue with read_file_range.',
225
+ description: 'Read a file by absolute path. Returns contents with line numbers. Truncates at max_lines with a hint to continue with read_file_range.',
197
226
  inputSchema: {
198
227
  type: 'object',
199
228
  properties: {
200
- file_path: { type: 'string', description: 'Absolute path to the file (as returned in filePath by search_codebase)' },
229
+ file_path: { type: 'string', description: 'Absolute path to the file. Note: search_codebase returns relative paths — construct the absolute path from your workspace root.' },
201
230
  max_lines: { type: 'number', description: 'Maximum lines to return (default 300, max 1000)' },
202
231
  },
203
232
  required: ['file_path'],
@@ -206,6 +235,14 @@ class McpServer {
206
235
  ],
207
236
  }));
208
237
  server.setRequestHandler(types_js_1.CallToolRequestSchema, async (req) => {
238
+ if (startupError) {
239
+ return {
240
+ content: [{
241
+ type: 'text',
242
+ text: `MCP server is unavailable: ${startupError.message}\n\nCheck the log file and fix the issue, then restart the MCP server.`,
243
+ }],
244
+ };
245
+ }
209
246
  try {
210
247
  if (req.params.name === 'list_projects') {
211
248
  const rows = await this.searcher.listProjects();
@@ -1 +1,2 @@
1
- export declare function runSetup(): Promise<void>;
1
+ import type { LoggerService } from './services/LoggerService';
2
+ export declare function runSetup(logger?: LoggerService): Promise<void>;
package/dist/src/setup.js CHANGED
@@ -41,6 +41,7 @@ const os = __importStar(require("os"));
41
41
  const child_process_1 = require("child_process");
42
42
  const constants_1 = require("./constants");
43
43
  const CONFIG_DIR = path.join(os.homedir(), '.config', 'codesearch-mcp');
44
+ const DEFAULT_LOG_DIR = path.join(CONFIG_DIR, 'logs');
44
45
  const CONFIG_FILE = path.join(CONFIG_DIR, '.env');
45
46
  const DOCKER_COMPOSE_SRC = path.join(__dirname, '..', '..', 'docker', 'docker-compose.yml');
46
47
  const DOCKER_COMPOSE_DST = path.join(CONFIG_DIR, 'docker-compose.yml');
@@ -64,7 +65,7 @@ function writeEnv(vars) {
64
65
  fs.writeFileSync(CONFIG_FILE, content, { mode: 0o600 });
65
66
  console.log(`\nConfig saved to ${CONFIG_FILE}`);
66
67
  }
67
- async function waitForDb(host, port, user, pass, db, maxWaitSec = 60) {
68
+ async function waitForDb(host, port, user, pass, db, maxWaitSec = 60, logger, steps) {
68
69
  const { Client } = await Promise.resolve().then(() => __importStar(require('pg')));
69
70
  const deadline = Date.now() + maxWaitSec * 1000;
70
71
  process.stdout.write('Waiting for PostgreSQL to be ready');
@@ -81,10 +82,12 @@ async function waitForDb(host, port, user, pass, db, maxWaitSec = 60) {
81
82
  await new Promise(r => setTimeout(r, 2000));
82
83
  }
83
84
  }
84
- console.error('\nPostgreSQL did not become ready in time.');
85
+ const reason = 'PostgreSQL did not become ready in time.';
86
+ console.error(`\n${reason}`);
87
+ logger?.error('waitForDb: timeout', { reason, steps });
85
88
  process.exit(1);
86
89
  }
87
- async function waitForOllama(host, maxWaitSec = 60) {
90
+ async function waitForOllama(host, maxWaitSec = 60, logger, steps) {
88
91
  const deadline = Date.now() + maxWaitSec * 1000;
89
92
  process.stdout.write('Waiting for Ollama to be ready');
90
93
  while (Date.now() < deadline) {
@@ -106,10 +109,12 @@ async function waitForOllama(host, maxWaitSec = 60) {
106
109
  await new Promise(r => setTimeout(r, 2000));
107
110
  }
108
111
  }
109
- console.error('\nOllama did not become ready in time.');
112
+ const reason = 'Ollama did not become ready in time.';
113
+ console.error(`\n${reason}`);
114
+ logger?.error('waitForOllama: timeout', { reason, steps });
110
115
  process.exit(1);
111
116
  }
112
- async function checkRemoteDb(host, port, user, pass, db) {
117
+ async function checkRemoteDb(host, port, user, pass, db, logger, steps) {
113
118
  try {
114
119
  const { Client } = await Promise.resolve().then(() => __importStar(require('pg')));
115
120
  const client = new Client({ host, port: parseInt(port), user, password: pass, database: db, connectionTimeoutMillis: 5000 });
@@ -118,50 +123,62 @@ async function checkRemoteDb(host, port, user, pass, db) {
118
123
  return true;
119
124
  }
120
125
  catch (err) {
121
- console.error(` Connection failed: ${err.message}`);
126
+ const msg = err instanceof Error ? err.message : String(err);
127
+ console.error(` Connection failed: ${msg}`);
128
+ logger?.error('checkRemoteDb: connection failed', { reason: msg, steps });
122
129
  return false;
123
130
  }
124
131
  }
125
- async function setupLocalDocker(ollamaHost) {
126
- console.log('\nCopying docker-compose.yml to', DOCKER_COMPOSE_DST);
132
+ async function setupLocalDocker(ollamaHost, dbPort, dbUser, dbPass, logger, steps) {
133
+ console.log('\nWriting docker-compose.yml to', DOCKER_COMPOSE_DST);
134
+ const yamlQuote = (s) => `"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
135
+ let composeContent = fs.readFileSync(DOCKER_COMPOSE_SRC, 'utf-8');
136
+ composeContent = composeContent
137
+ .replace('POSTGRES_USER: dev', `POSTGRES_USER: ${yamlQuote(dbUser)}`)
138
+ .replace('POSTGRES_PASSWORD: dev', `POSTGRES_PASSWORD: ${yamlQuote(dbPass)}`)
139
+ .replace('"5433:5432"', `"${dbPort}:5432"`);
127
140
  fs.mkdirSync(CONFIG_DIR, { recursive: true });
128
- fs.copyFileSync(DOCKER_COMPOSE_SRC, DOCKER_COMPOSE_DST);
141
+ fs.writeFileSync(DOCKER_COMPOSE_DST, composeContent, 'utf-8');
129
142
  console.log('\nStarting Docker containers...');
130
143
  const upResult = (0, child_process_1.spawnSync)('docker', ['compose', '-f', DOCKER_COMPOSE_DST, 'up', '-d'], { stdio: 'inherit' });
131
144
  if (upResult.status !== 0) {
132
- console.error('Failed to start Docker containers. Make sure Docker is running.');
145
+ const reason = 'Failed to start Docker containers. Make sure Docker is running.';
146
+ console.error(reason);
147
+ logger?.error('setupLocalDocker: docker startup failed', { reason, steps });
133
148
  process.exit(1);
134
149
  }
135
- await waitForDb('localhost', 5433, 'dev', 'dev', 'postgres');
136
- await waitForOllama(ollamaHost);
150
+ await waitForDb('localhost', parseInt(dbPort), dbUser, dbPass, 'postgres', 60, logger, steps);
151
+ await waitForOllama(ollamaHost, 60, logger, steps);
137
152
  // Resolve the ollama container ID via compose — compose project name (and thus the
138
153
  // default container name) depends on the parent directory, so hardcoding it breaks when
139
154
  // users install via npm -g or move the compose file around.
140
155
  const ollamaId = (0, child_process_1.spawnSync)('docker', ['compose', '-f', DOCKER_COMPOSE_DST, 'ps', '-q', 'ollama'], { encoding: 'utf-8' })
141
156
  .stdout?.trim();
142
157
  if (!ollamaId) {
143
- console.error('Could not resolve ollama container ID from compose. Pull the model manually:');
158
+ const reason = 'Could not resolve ollama container ID from compose. Pull the model manually:';
159
+ console.error(reason);
144
160
  console.error(` docker exec <ollama-container> ollama pull ${constants_1.DEFAULT_EMBED_MODEL}`);
161
+ logger?.error('setupLocalDocker: ollama container not found', { reason, steps });
145
162
  process.exit(1);
146
163
  }
147
164
  console.log(`Pulling ${constants_1.DEFAULT_EMBED_MODEL} model (this may take a while)...`);
148
165
  (0, child_process_1.spawnSync)('docker', ['exec', ollamaId, 'ollama', 'pull', constants_1.DEFAULT_EMBED_MODEL], { stdio: 'inherit' });
149
- return {
150
- DB_HOST: 'localhost',
151
- DB_PORT: '5433',
152
- DB_USER: 'dev',
153
- DB_PASS: 'dev',
154
- };
166
+ return { DB_HOST: 'localhost', DB_PORT: dbPort, DB_USER: dbUser, DB_PASS: dbPass };
155
167
  }
156
- async function setupRemoteDb(rl) {
168
+ async function setupRemoteDb(rl, logger, steps, logStep) {
169
+ const lp = async (label, def) => {
170
+ const answer = await prompt(rl, label, def);
171
+ logStep?.(label, answer);
172
+ return answer;
173
+ };
157
174
  let vars = {};
158
175
  while (true) {
159
- vars.DB_HOST = await prompt(rl, 'DB host', 'localhost');
160
- vars.DB_PORT = await prompt(rl, 'DB port', '5432');
161
- vars.DB_USER = await prompt(rl, 'DB user', 'postgres');
162
- vars.DB_PASS = await prompt(rl, 'DB password', '');
176
+ vars.DB_HOST = await lp('DB host', 'localhost');
177
+ vars.DB_PORT = await lp('DB port', '5432');
178
+ vars.DB_USER = await lp('DB user', 'postgres');
179
+ vars.DB_PASS = await lp('DB password', '');
163
180
  console.log('\nTesting connection...');
164
- const ok = await checkRemoteDb(vars.DB_HOST, vars.DB_PORT, vars.DB_USER, vars.DB_PASS, 'postgres');
181
+ const ok = await checkRemoteDb(vars.DB_HOST, vars.DB_PORT, vars.DB_USER, vars.DB_PASS, 'postgres', logger, steps);
165
182
  if (ok) {
166
183
  console.log('Connection successful.');
167
184
  break;
@@ -172,18 +189,47 @@ async function setupRemoteDb(rl) {
172
189
  }
173
190
  return vars;
174
191
  }
175
- async function runSetup() {
192
+ async function runSetup(logger) {
176
193
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
177
- console.log('=== codesearch-mcp setup ===\n');
178
- const dbChoice = await ask(rl, 'Infrastructure:\n 1. Local Docker (auto-start PostgreSQL + Ollama)\n 2. Remote (Supabase, Railway, self-hosted DB + local Ollama)\n\nEnter choice (1): ');
194
+ const steps = [];
195
+ function logStep(q, raw) {
196
+ const a = /password/i.test(q) ? '***' : raw.trim();
197
+ logger?.debug('setup: prompt', { q, a });
198
+ steps.push({ q, a });
199
+ }
200
+ const loggedAsk = async (question) => {
201
+ const answer = await ask(rl, question);
202
+ const key = question.split('\n').find(l => l.trim())?.trim() ?? question.trim();
203
+ logStep(key, answer);
204
+ return answer;
205
+ };
206
+ const loggedPrompt = async (label, def) => {
207
+ const answer = await prompt(rl, label, def);
208
+ logStep(label, answer || def);
209
+ return answer;
210
+ };
211
+ console.log('=== codesearch-mcp setup ===');
212
+ console.log('Press Enter to keep the default value shown in parentheses.\n');
213
+ const dbChoice = await loggedAsk('Infrastructure:\n 1. Local Docker (auto-start PostgreSQL + Ollama)\n 2. Remote (Supabase, Railway, self-hosted DB + local Ollama)\n\nEnter choice (1): ');
179
214
  const useDocker = (dbChoice.trim() || '1') === '1';
180
- const defaultWorkspace = path.join(os.homedir(), 'Projects');
181
- const workspaceInput = await ask(rl, `\nWorkspace root — directory that contains all your projects\n Example: ${os.homedir()}/WebstormProjects\n Path (${defaultWorkspace}): `);
182
- const workspaceRoot = expandHome(workspaceInput.trim() || defaultWorkspace);
183
- const ollamaHost = await prompt(rl, 'Ollama API URL', 'http://localhost:11434');
215
+ const workspaceExample = (() => {
216
+ const candidates = ['Projects', 'projects', 'workspace', 'code', 'Code', 'dev', 'Development'];
217
+ const found = candidates.find(d => fs.existsSync(path.join(os.homedir(), d)));
218
+ return path.join(os.homedir(), found ?? 'projects');
219
+ })();
220
+ console.log(`\nWorkspace root — full path to the directory that contains all your projects`);
221
+ console.log(` (e.g. ${workspaceExample})`);
222
+ let workspaceInput;
223
+ do {
224
+ workspaceInput = await loggedAsk('Path: ');
225
+ if (!workspaceInput.trim())
226
+ console.log(' Path is required.');
227
+ } while (!workspaceInput.trim());
228
+ const workspaceRoot = expandHome(workspaceInput.trim());
229
+ const ollamaHost = await loggedPrompt('Ollama API URL', 'http://localhost:11434');
184
230
  const dbVars = useDocker
185
- ? await setupLocalDocker(ollamaHost)
186
- : await setupRemoteDb(rl);
231
+ ? await setupLocalDocker(ollamaHost, await loggedPrompt('DB port', '5433'), await loggedPrompt('DB user', 'dev'), await loggedPrompt('DB password', 'dev'), logger, steps)
232
+ : await setupRemoteDb(rl, logger, steps, logStep);
187
233
  console.log('\n--- Logging ---');
188
234
  console.log(' error — failures only (recommended)');
189
235
  console.log(' warn — + recoverable issues');
@@ -191,20 +237,21 @@ async function runSetup() {
191
237
  const validLevels = ['error', 'warn', 'info', 'debug'];
192
238
  let logLevel;
193
239
  do {
194
- logLevel = await prompt(rl, 'Log level (error/warn/info/debug)', 'error');
240
+ logLevel = await loggedPrompt('Log level (error/warn/info/debug)', 'error');
195
241
  if (!validLevels.includes(logLevel))
196
242
  console.log(` Invalid level. Choose: ${validLevels.join(', ')}`);
197
243
  } while (!validLevels.includes(logLevel));
198
- const defaultLogFile = path.join(os.homedir(), '.config', 'codesearch-mcp', 'rag.log');
199
- const logFileInput = await ask(rl, `Log file (${defaultLogFile}, or "false" to disable): `);
200
- const logFile = logFileInput.trim() || defaultLogFile;
201
- writeEnv({
244
+ const logDirInput = await loggedAsk(`Log dir (leave blank for default: ${DEFAULT_LOG_DIR}, or "false" to disable): `);
245
+ const logDir = logDirInput.trim();
246
+ const envVars = {
202
247
  ...dbVars,
203
248
  WORKSPACE_ROOT: workspaceRoot,
204
249
  OLLAMA_HOST: ollamaHost,
205
250
  LOG_LEVEL: logLevel,
206
- LOG_FILE: logFile,
207
- });
251
+ };
252
+ if (logDir)
253
+ envVars['LOG_DIR'] = logDir;
254
+ writeEnv(envVars);
208
255
  rl.close();
209
256
  console.log('\nInitializing database schema...');
210
257
  const dotenv = await Promise.resolve().then(() => __importStar(require('dotenv')));
@@ -212,8 +259,21 @@ async function runSetup() {
212
259
  const { config } = await Promise.resolve().then(() => __importStar(require('./config')));
213
260
  const { DatabaseService } = await Promise.resolve().then(() => __importStar(require('./services/DatabaseService')));
214
261
  const dbService = new DatabaseService(config);
215
- await dbService.initDbWithSchema();
216
- await dbService.destroy();
262
+ let initError = null;
263
+ try {
264
+ await dbService.initDbWithSchema();
265
+ }
266
+ catch (err) {
267
+ initError = err instanceof Error ? err : new Error(String(err));
268
+ }
269
+ finally {
270
+ await dbService.destroy();
271
+ }
272
+ if (initError) {
273
+ console.error(`Database schema init failed: ${initError.message}`);
274
+ logger?.error('runSetup: database schema init failed', { reason: initError.message, steps });
275
+ process.exit(1);
276
+ }
217
277
  console.log('\nSetup complete!');
218
278
  console.log('\nNext steps:');
219
279
  console.log(` rag-index ${workspaceRoot}`);
@@ -6,7 +6,7 @@ services:
6
6
  environment:
7
7
  POSTGRES_USER: dev
8
8
  POSTGRES_PASSWORD: dev
9
- POSTGRES_DB: rag
9
+ POSTGRES_DB: postgres
10
10
  ports:
11
11
  - "5433:5432"
12
12
  volumes:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codesearch-mcp",
3
- "version": "0.0.13-alpha",
3
+ "version": "0.0.15-alpha",
4
4
  "description": "Local semantic codebase search via MCP — indexes projects into pgvector, exposes search to Claude",
5
5
  "license": "MIT",
6
6
  "main": "dist/app.js",
@@ -12,7 +12,8 @@
12
12
  "files": [
13
13
  "dist/",
14
14
  "docker/",
15
- "README.md"
15
+ "README.md",
16
+ "CHANGELOG.md"
16
17
  ],
17
18
  "scripts": {
18
19
  "build": "tsc",
@@ -29,7 +30,7 @@
29
30
  },
30
31
  "dependencies": {
31
32
  "@inquirer/prompts": "^8.4.1",
32
- "@langchain/textsplitters": "^0.1.0",
33
+ "@langchain/textsplitters": "^1.0.1",
33
34
  "@modelcontextprotocol/sdk": "^1.0.0",
34
35
  "cli-progress": "^3.12.0",
35
36
  "dotenv": "^17.4.2",