codesearch-mcp 0.0.12-alpha → 0.0.14-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/README.md +6 -6
- package/dist/app.js +3 -2
- package/dist/src/commands/McpCommand.js +16 -3
- package/dist/src/commands/SetupCommand.d.ts +3 -0
- package/dist/src/commands/SetupCommand.js +5 -1
- package/dist/src/container.d.ts +1 -1
- package/dist/src/container.js +2 -2
- package/dist/src/interfaces/IAstParser.d.ts +1 -1
- package/dist/src/project-selector.js +32 -20
- package/dist/src/services/AstParserService.js +42 -29
- package/dist/src/services/DatabaseService.js +4 -2
- package/dist/src/services/IndexerService.js +2 -2
- package/dist/src/services/LoggerService.d.ts +1 -1
- package/dist/src/services/LoggerService.js +26 -6
- package/dist/src/services/McpServer.d.ts +1 -1
- package/dist/src/services/McpServer.js +28 -21
- package/dist/src/services/SearcherService.d.ts +2 -2
- package/dist/src/services/SearcherService.js +54 -9
- package/dist/src/setup.d.ts +2 -1
- package/dist/src/setup.js +103 -43
- package/docker/docker-compose.yml +1 -1
- package/package.json +3 -2
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
|
|
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/
|
|
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/
|
|
110
|
-
npx pino-pretty < ~/.config/codesearch-mcp/
|
|
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
|
-
|
|
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
|
-
| `
|
|
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
|
|
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
|
-
|
|
12
|
-
|
|
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 => {
|
|
@@ -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;
|
package/dist/src/container.d.ts
CHANGED
package/dist/src/container.js
CHANGED
|
@@ -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);
|
|
@@ -10,7 +10,7 @@ export interface SymbolEntry {
|
|
|
10
10
|
project: string;
|
|
11
11
|
filePath: string;
|
|
12
12
|
name: string;
|
|
13
|
-
kind: 'function' | 'class' | 'interface' | 'type' | 'variable';
|
|
13
|
+
kind: 'function' | 'class' | 'interface' | 'type' | 'variable' | 'method';
|
|
14
14
|
signature: string | null;
|
|
15
15
|
line: number | null;
|
|
16
16
|
col: number | null;
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
56
|
+
await fs.promises.writeFile(filePath, [...names].sort().join('\n') + '\n');
|
|
54
57
|
}
|
|
55
|
-
else
|
|
56
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
67
|
-
|
|
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 (
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
this.
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
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) {
|
|
@@ -116,7 +128,8 @@ class AstParserService {
|
|
|
116
128
|
const resolved = decl.getModuleSpecifierSourceFile();
|
|
117
129
|
if (!resolved) {
|
|
118
130
|
const specifier = decl.getModuleSpecifierValue();
|
|
119
|
-
|
|
131
|
+
const isCodeImport = !/\.(json|css|scss|less|svg|png|jpg|gif|ico|woff2?|ttf|eot)(\?.*)?$/.test(specifier);
|
|
132
|
+
if (specifier.startsWith('.') && isCodeImport) {
|
|
120
133
|
this.logger.debug('AstParser: unresolved import', { specifier, file: filePath });
|
|
121
134
|
}
|
|
122
135
|
continue;
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
}
|
|
@@ -80,7 +80,7 @@ class IndexerService {
|
|
|
80
80
|
}
|
|
81
81
|
try {
|
|
82
82
|
const { symbols: rawSymbols } = await this.ast.analyzeFile(absPath, serviceName, projectRoot);
|
|
83
|
-
const symbols = rawSymbols.map(s => ({ ...s, filePath }));
|
|
83
|
+
const symbols = [...new Map(rawSymbols.map(s => ({ ...s, filePath })).map(s => [`${s.name}:${s.kind}`, s])).values()];
|
|
84
84
|
await this.db.transaction(async (manager) => {
|
|
85
85
|
await manager.delete(db_1.CodeSymbol, { project: serviceName, filePath });
|
|
86
86
|
if (symbols.length > 0) {
|
|
@@ -223,7 +223,7 @@ class IndexerService {
|
|
|
223
223
|
toFile: path.relative(this.config.workspaceRoot, e.toFile),
|
|
224
224
|
}));
|
|
225
225
|
// normalize symbols: replace absolute filePath with workspace-relative
|
|
226
|
-
const symbols = rawSymbols.map(s => ({ ...s, filePath: relFilePath }));
|
|
226
|
+
const symbols = [...new Map(rawSymbols.map(s => ({ ...s, filePath: relFilePath })).map(s => [`${s.name}:${s.kind}`, s])).values()];
|
|
227
227
|
const depsMs = Date.now() - t0;
|
|
228
228
|
const code = fs.readFileSync(filePath, 'utf-8');
|
|
229
229
|
const chunks = await this.getSplitter(filePath).createDocuments([code]);
|
|
@@ -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
|
|
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,15 +50,35 @@ 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
|
|
56
|
-
const enabled =
|
|
57
|
-
|
|
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
|
|
61
|
-
? pino_1.default.transport({
|
|
67
|
+
? pino_1.default.transport({
|
|
68
|
+
targets: [{
|
|
69
|
+
target: 'pino-roll',
|
|
70
|
+
options: {
|
|
71
|
+
file: filePath,
|
|
72
|
+
frequency: 'daily',
|
|
73
|
+
dateFormat: 'yyyy-MM-dd',
|
|
74
|
+
mkdir: true,
|
|
75
|
+
symlink: true,
|
|
76
|
+
limit: { count: 30 },
|
|
77
|
+
size: '50m',
|
|
78
|
+
},
|
|
79
|
+
level,
|
|
80
|
+
}],
|
|
81
|
+
})
|
|
62
82
|
: undefined;
|
|
63
83
|
const logger = transport
|
|
64
84
|
? (0, pino_1.default)({ level }, transport)
|
|
@@ -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;
|
|
@@ -58,36 +58,29 @@ You have access to a local semantic code search server. Use these tools to navig
|
|
|
58
58
|
- **read_file_range** — read a specific line range from any indexed file. Use this to expand context around a search result.
|
|
59
59
|
- **get_dependencies** — show what files a given file imports (direct or transitive). Use to understand what a file depends on.
|
|
60
60
|
- **get_dependents** — show which files import a given file (reverse graph). Use before refactoring to find all callers.
|
|
61
|
-
- **find_symbol** — find where a symbol (function, class, type, interface, variable) is defined by exact name. Faster than semantic search when you know the name.
|
|
61
|
+
- **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
62
|
- **list_files** — browse the directory tree of a project path. Use to understand project structure before reading files.
|
|
63
63
|
- **read_file** — read a complete file by absolute path with line numbers. Truncates at max_lines with a hint to continue.
|
|
64
64
|
|
|
65
65
|
## Recommended workflow
|
|
66
66
|
|
|
67
67
|
1. Use \`list_projects\` to discover available projects before filtering by \`project\`.
|
|
68
|
-
2.
|
|
69
|
-
3.
|
|
70
|
-
4.
|
|
71
|
-
5. Use \`find_symbol\` when you know the exact name of a function or
|
|
68
|
+
2. **Before searching**: if your query uses an ambiguous term (e.g. "subscription" could mean pub/sub or billing), pass a lower \`threshold\` (e.g. \`0.35\`) to \`search_codebase\` to filter out weak matches. Default threshold is permissive — tighten it when semantic drift is likely.
|
|
69
|
+
3. Use \`search_codebase\` to find relevant code by meaning.
|
|
70
|
+
4. If a result **starts mid-function or mid-class** (e.g. you see a closing brace or a method body without the declaration), re-run the same search with \`include_neighbors: true\` — it automatically fetches the chunk before and after each match. If you need a specific line range, use \`read_file_range\` instead.
|
|
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
72
|
6. Use \`list_files\` to explore project structure (src/, components/, etc.) before diving into files.
|
|
73
73
|
7. Use \`read_file\` to read a complete file when you have its absolute path from \`search_codebase\` results.
|
|
74
74
|
|
|
75
|
-
##
|
|
75
|
+
## Dependency graph
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
- **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.
|
|
77
|
+
\`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).
|
|
79
78
|
|
|
80
|
-
|
|
81
|
-
- **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.
|
|
82
|
-
- **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.
|
|
79
|
+
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.
|
|
83
80
|
|
|
84
81
|
## Relevance scores
|
|
85
82
|
|
|
86
|
-
Lower score = better match (cosine distance). Results above the configured threshold are filtered out automatically.
|
|
87
|
-
|
|
88
|
-
## Dependency graph
|
|
89
|
-
|
|
90
|
-
\`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).
|
|
83
|
+
Lower score = better match (cosine distance). Results above the configured threshold are filtered out automatically. Pass \`threshold\` to \`search_codebase\` to override per-query (e.g. \`0.35\` for strict, \`0.55\` for broad).
|
|
91
84
|
`.trim();
|
|
92
85
|
class McpServer {
|
|
93
86
|
searcher;
|
|
@@ -98,7 +91,7 @@ class McpServer {
|
|
|
98
91
|
this.config = config;
|
|
99
92
|
this.logger = logger;
|
|
100
93
|
}
|
|
101
|
-
async start() {
|
|
94
|
+
async start(startupError) {
|
|
102
95
|
const server = new index_js_1.Server({ name: 'local-rag', version: PKG_VERSION }, { capabilities: { tools: {} }, instructions: SERVER_INSTRUCTIONS });
|
|
103
96
|
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
|
|
104
97
|
tools: [
|
|
@@ -112,6 +105,7 @@ class McpServer {
|
|
|
112
105
|
limit: { type: 'number', description: `Max results (default ${this.config.search.defaultLimit}, max ${this.config.search.maxLimit})` },
|
|
113
106
|
project: { type: 'string', description: 'Filter by project name (optional). Use list_projects to see available names.' },
|
|
114
107
|
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.' },
|
|
108
|
+
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.` },
|
|
115
109
|
},
|
|
116
110
|
required: ['query'],
|
|
117
111
|
},
|
|
@@ -164,13 +158,13 @@ class McpServer {
|
|
|
164
158
|
},
|
|
165
159
|
{
|
|
166
160
|
name: 'find_symbol',
|
|
167
|
-
description: 'Find TypeScript/JavaScript symbol definitions (functions, classes, types, interfaces, variables) by exact name. Use when you know the symbol name and want its definition location fast.',
|
|
161
|
+
description: 'Find TypeScript/JavaScript symbol definitions (functions, classes, types, interfaces, variables, methods) by exact name. Use when you know the symbol name and want its definition location fast.',
|
|
168
162
|
inputSchema: {
|
|
169
163
|
type: 'object',
|
|
170
164
|
properties: {
|
|
171
165
|
name: { type: 'string', description: 'Exact symbol name (case-sensitive, whole-word match)' },
|
|
172
166
|
project: { type: 'string', description: 'Limit search to this project (optional). Use list_projects to find names.' },
|
|
173
|
-
kind: { type: 'string', enum: ['function', 'class', 'type', 'interface', 'variable', 'all'], description: 'Symbol kind filter. Default: all.' },
|
|
167
|
+
kind: { type: 'string', enum: ['function', 'class', 'type', 'interface', 'variable', 'method', 'all'], description: 'Symbol kind filter. Default: all.' },
|
|
174
168
|
},
|
|
175
169
|
required: ['name'],
|
|
176
170
|
},
|
|
@@ -204,6 +198,14 @@ class McpServer {
|
|
|
204
198
|
],
|
|
205
199
|
}));
|
|
206
200
|
server.setRequestHandler(types_js_1.CallToolRequestSchema, async (req) => {
|
|
201
|
+
if (startupError) {
|
|
202
|
+
return {
|
|
203
|
+
content: [{
|
|
204
|
+
type: 'text',
|
|
205
|
+
text: `MCP server is unavailable: ${startupError.message}\n\nCheck the log file and fix the issue, then restart the MCP server.`,
|
|
206
|
+
}],
|
|
207
|
+
};
|
|
208
|
+
}
|
|
207
209
|
try {
|
|
208
210
|
if (req.params.name === 'list_projects') {
|
|
209
211
|
const rows = await this.searcher.listProjects();
|
|
@@ -232,8 +234,13 @@ class McpServer {
|
|
|
232
234
|
const limit = Math.min(Math.max(1, Number(req.params.arguments?.limit) || this.config.search.defaultLimit), this.config.search.maxLimit);
|
|
233
235
|
const project = req.params.arguments?.project ? String(req.params.arguments.project) : null;
|
|
234
236
|
const includeNeighbors = Boolean(req.params.arguments?.include_neighbors);
|
|
237
|
+
const rawThreshold = req.params.arguments?.threshold;
|
|
238
|
+
const parsedThreshold = Number(rawThreshold);
|
|
239
|
+
const threshold = rawThreshold != null && !isNaN(parsedThreshold)
|
|
240
|
+
? Math.min(1, Math.max(0, parsedThreshold))
|
|
241
|
+
: undefined;
|
|
235
242
|
const { query, translated } = await this.searcher.prepareQuery(raw);
|
|
236
|
-
const results = await this.searcher.searchChunks(query, limit, project);
|
|
243
|
+
const results = await this.searcher.searchChunks(query, limit, project, threshold);
|
|
237
244
|
if (results.length === 0) {
|
|
238
245
|
return { content: [{ type: 'text', text: 'No relevant results found.' }] };
|
|
239
246
|
}
|
|
@@ -318,7 +325,7 @@ class McpServer {
|
|
|
318
325
|
const name = String(req.params.arguments?.name ?? '');
|
|
319
326
|
const project = req.params.arguments?.project ? String(req.params.arguments.project) : null;
|
|
320
327
|
const rawKind = req.params.arguments?.kind;
|
|
321
|
-
const validKinds = ['function', 'class', 'type', 'interface', 'variable', 'all'];
|
|
328
|
+
const validKinds = ['function', 'class', 'type', 'interface', 'variable', 'method', 'all'];
|
|
322
329
|
const kind = validKinds.includes(rawKind) ? rawKind : 'all';
|
|
323
330
|
const { hits, truncated } = await this.searcher.findSymbol(name, project, kind);
|
|
324
331
|
if (hits.length === 0) {
|
|
@@ -45,7 +45,7 @@ export interface GrepMatch {
|
|
|
45
45
|
filePath: string;
|
|
46
46
|
line: number;
|
|
47
47
|
}
|
|
48
|
-
export type SymbolKind = 'function' | 'class' | 'type' | 'interface' | 'variable' | 'all';
|
|
48
|
+
export type SymbolKind = 'function' | 'class' | 'type' | 'interface' | 'variable' | 'method' | 'all';
|
|
49
49
|
export interface SymbolHit {
|
|
50
50
|
filePath: string;
|
|
51
51
|
line: number;
|
|
@@ -64,7 +64,7 @@ export declare class SearcherService {
|
|
|
64
64
|
translated: boolean;
|
|
65
65
|
}>;
|
|
66
66
|
listProjects(): Promise<ProjectRow[]>;
|
|
67
|
-
searchChunks(query: string, limit: number, project: string | null): Promise<SearchResult[]>;
|
|
67
|
+
searchChunks(query: string, limit: number, project: string | null, threshold?: number): Promise<SearchResult[]>;
|
|
68
68
|
fetchNeighbors(results: SearchResult[]): Promise<NeighborResult[]>;
|
|
69
69
|
getDependencies(filePath: string, project: string, symbol?: string | null, transitive?: boolean): Promise<DependencyRow[]>;
|
|
70
70
|
getDependents(filePath: string, project: string, symbol?: string | null, transitive?: boolean): Promise<DependencyRow[]>;
|
|
@@ -59,8 +59,11 @@ class SearcherService {
|
|
|
59
59
|
}
|
|
60
60
|
async prepareQuery(raw) {
|
|
61
61
|
if (this.config.search.autoTranslate && this.isNonEnglish(raw) && this.embedder.translate) {
|
|
62
|
-
|
|
62
|
+
const query = await this.embedder.translate(raw, this.config.search.translateModel);
|
|
63
|
+
this.logger.debug('SearcherService: prepareQuery', { translated: true, raw: raw.slice(0, 120), query: query.slice(0, 120) });
|
|
64
|
+
return { query, translated: true };
|
|
63
65
|
}
|
|
66
|
+
this.logger.debug('SearcherService: prepareQuery', { translated: false, query: raw.slice(0, 120) });
|
|
64
67
|
return { query: raw, translated: false };
|
|
65
68
|
}
|
|
66
69
|
async listProjects() {
|
|
@@ -73,25 +76,36 @@ class SearcherService {
|
|
|
73
76
|
return { ...r, ...(typeof meta === 'object' ? meta : EMPTY_META) };
|
|
74
77
|
});
|
|
75
78
|
}
|
|
76
|
-
async searchChunks(query, limit, project) {
|
|
79
|
+
async searchChunks(query, limit, project, threshold) {
|
|
80
|
+
const effectiveThreshold = threshold ?? this.config.search.similarityThreshold;
|
|
81
|
+
this.logger.debug('SearcherService: searchChunks', {
|
|
82
|
+
query: query.slice(0, 120), limit, project, threshold: effectiveThreshold,
|
|
83
|
+
});
|
|
77
84
|
const embedding = await this.embedder.embed(query);
|
|
78
|
-
const params = [`[${embedding}]`,
|
|
85
|
+
const params = [`[${embedding}]`, effectiveThreshold, limit];
|
|
79
86
|
let projectFilter = '';
|
|
80
87
|
if (project) {
|
|
81
88
|
params.push(project);
|
|
82
89
|
projectFilter = `AND "serviceName" = $${params.length}`;
|
|
83
90
|
}
|
|
84
|
-
|
|
91
|
+
const results = await this.db.query(`SELECT "serviceName", "filePath", "startLine", content,
|
|
85
92
|
ROUND((embedding <=> $1)::numeric, 4)::float8 AS score
|
|
86
93
|
FROM code_chunk
|
|
87
94
|
WHERE embedding <=> $1 < $2
|
|
88
95
|
${projectFilter}
|
|
89
96
|
ORDER BY embedding <=> $1
|
|
90
97
|
LIMIT $3`, params);
|
|
98
|
+
this.logger.debug('SearcherService: searchChunks results', {
|
|
99
|
+
count: results.length,
|
|
100
|
+
scores: results.map(r => r.score),
|
|
101
|
+
files: results.map(r => r.filePath),
|
|
102
|
+
});
|
|
103
|
+
return results;
|
|
91
104
|
}
|
|
92
105
|
async fetchNeighbors(results) {
|
|
93
106
|
if (results.length === 0)
|
|
94
107
|
return [];
|
|
108
|
+
this.logger.debug('SearcherService: fetchNeighbors', { count: results.length });
|
|
95
109
|
const values = results.map((_, i) => `(${i}, $${i * 2 + 1}, $${i * 2 + 2}::int)`).join(',');
|
|
96
110
|
const params = results.flatMap(r => [r.filePath, r.startLine]);
|
|
97
111
|
const rows = await this.db.query(`SELECT r.idx,
|
|
@@ -109,13 +123,19 @@ class SearcherService {
|
|
|
109
123
|
ORDER BY "startLine" ASC LIMIT 1
|
|
110
124
|
) next ON true
|
|
111
125
|
ORDER BY r.idx`, params);
|
|
112
|
-
|
|
126
|
+
const neighbors = rows.map((row, i) => ({
|
|
113
127
|
result: results[i],
|
|
114
128
|
prevChunk: row.prev_fp ? { serviceName: row.prev_sn, filePath: row.prev_fp, startLine: row.prev_sl, content: row.prev_c } : null,
|
|
115
129
|
nextChunk: row.next_fp ? { serviceName: row.next_sn, filePath: row.next_fp, startLine: row.next_sl, content: row.next_c } : null,
|
|
116
130
|
}));
|
|
131
|
+
this.logger.debug('SearcherService: fetchNeighbors results', {
|
|
132
|
+
withPrev: neighbors.filter(n => n.prevChunk).length,
|
|
133
|
+
withNext: neighbors.filter(n => n.nextChunk).length,
|
|
134
|
+
});
|
|
135
|
+
return neighbors;
|
|
117
136
|
}
|
|
118
137
|
async getDependencies(filePath, project, symbol = null, transitive = false) {
|
|
138
|
+
this.logger.debug('SearcherService: getDependencies', { filePath, project, symbol, transitive });
|
|
119
139
|
const params = [filePath, project];
|
|
120
140
|
let symbolFilter = '';
|
|
121
141
|
if (symbol) {
|
|
@@ -127,8 +147,10 @@ class SearcherService {
|
|
|
127
147
|
WHERE "fromFile" = $1 AND project = $2
|
|
128
148
|
${symbolFilter}
|
|
129
149
|
ORDER BY "toFile"`, params);
|
|
130
|
-
if (!transitive)
|
|
150
|
+
if (!transitive) {
|
|
151
|
+
this.logger.debug('SearcherService: getDependencies result', { direct: direct.length });
|
|
131
152
|
return direct;
|
|
153
|
+
}
|
|
132
154
|
const indirect = await this.db.query(`WITH RECURSIVE deps AS (
|
|
133
155
|
SELECT "toFile", 1 AS depth, "toFile" AS via
|
|
134
156
|
FROM code_dependency
|
|
@@ -151,9 +173,11 @@ class SearcherService {
|
|
|
151
173
|
WHERE depth > 1
|
|
152
174
|
GROUP BY "toFile"
|
|
153
175
|
ORDER BY MIN(depth), "toFile"`, [filePath, project]);
|
|
176
|
+
this.logger.debug('SearcherService: getDependencies result', { direct: direct.length, indirect: indirect.length });
|
|
154
177
|
return [...direct, ...indirect];
|
|
155
178
|
}
|
|
156
179
|
async getDependents(filePath, project, symbol = null, transitive = false) {
|
|
180
|
+
this.logger.debug('SearcherService: getDependents', { filePath, project, symbol, transitive });
|
|
157
181
|
const params = [filePath, project];
|
|
158
182
|
let symbolFilter = '';
|
|
159
183
|
if (symbol) {
|
|
@@ -165,8 +189,10 @@ class SearcherService {
|
|
|
165
189
|
WHERE "toFile" = $1 AND project = $2
|
|
166
190
|
${symbolFilter}
|
|
167
191
|
ORDER BY "fromFile"`, params);
|
|
168
|
-
if (!transitive)
|
|
192
|
+
if (!transitive) {
|
|
193
|
+
this.logger.debug('SearcherService: getDependents result', { direct: direct.length });
|
|
169
194
|
return direct;
|
|
195
|
+
}
|
|
170
196
|
const indirect = await this.db.query(`WITH RECURSIVE rdeps AS (
|
|
171
197
|
SELECT "fromFile", 1 AS depth, "fromFile" AS via
|
|
172
198
|
FROM code_dependency
|
|
@@ -189,9 +215,11 @@ class SearcherService {
|
|
|
189
215
|
WHERE depth > 1
|
|
190
216
|
GROUP BY "fromFile"
|
|
191
217
|
ORDER BY MIN(depth), "fromFile"`, [filePath, project]);
|
|
218
|
+
this.logger.debug('SearcherService: getDependents result', { direct: direct.length, indirect: indirect.length });
|
|
192
219
|
return [...direct, ...indirect];
|
|
193
220
|
}
|
|
194
221
|
grepSymbol(symbol, project, excludeFile) {
|
|
222
|
+
this.logger.debug('SearcherService: grepSymbol', { symbol, project, excludeFile });
|
|
195
223
|
const projectDir = path.join(this.config.workspaceRoot, project);
|
|
196
224
|
const result = (0, child_process_1.spawnSync)('grep', [
|
|
197
225
|
'-rn', '-F', '-w',
|
|
@@ -216,16 +244,21 @@ class SearcherService {
|
|
|
216
244
|
return [];
|
|
217
245
|
return [{ filePath: path.relative(this.config.workspaceRoot, absFile), line: parseInt(lineNum, 10) }];
|
|
218
246
|
});
|
|
219
|
-
|
|
247
|
+
const truncated = all.length > GREP_MAX_RESULTS;
|
|
248
|
+
this.logger.debug('SearcherService: grepSymbol result', { matches: Math.min(all.length, GREP_MAX_RESULTS), truncated });
|
|
249
|
+
return { matches: all.slice(0, GREP_MAX_RESULTS), truncated };
|
|
220
250
|
}
|
|
221
251
|
// Sync wrapper: validates name (throws synchronously), then returns Promise.
|
|
222
|
-
//
|
|
252
|
+
// Methods are never indexed — always resolved via grep. Other kinds use index with grep fallback.
|
|
223
253
|
findSymbol(name, project, kind) {
|
|
224
254
|
if (!/^[\w$]+$/.test(name))
|
|
225
255
|
throw new Error('Invalid symbol name');
|
|
256
|
+
if (kind === 'method')
|
|
257
|
+
return Promise.resolve(this.grepFindSymbol(name, project, kind));
|
|
226
258
|
return this.findSymbolFromIndex(name, project, kind);
|
|
227
259
|
}
|
|
228
260
|
async findSymbolFromIndex(name, project, kind) {
|
|
261
|
+
this.logger.debug('SearcherService: findSymbol', { name, project, kind, source: 'index' });
|
|
229
262
|
try {
|
|
230
263
|
const params = [name];
|
|
231
264
|
let projectFilter = '';
|
|
@@ -252,6 +285,7 @@ class SearcherService {
|
|
|
252
285
|
kind: row.kind,
|
|
253
286
|
signature: row.signature ?? row.name,
|
|
254
287
|
}));
|
|
288
|
+
this.logger.debug('SearcherService: findSymbol result', { source: 'index', hits: hits.length, truncated });
|
|
255
289
|
return { hits, truncated };
|
|
256
290
|
}
|
|
257
291
|
catch {
|
|
@@ -260,6 +294,7 @@ class SearcherService {
|
|
|
260
294
|
}
|
|
261
295
|
}
|
|
262
296
|
grepFindSymbol(name, project, kind) {
|
|
297
|
+
this.logger.debug('SearcherService: findSymbol', { name, project, kind, source: 'grep' });
|
|
263
298
|
const searchDir = project
|
|
264
299
|
? path.join(this.config.workspaceRoot, project)
|
|
265
300
|
: this.config.workspaceRoot;
|
|
@@ -300,9 +335,11 @@ class SearcherService {
|
|
|
300
335
|
signature: matchedLine.trim(),
|
|
301
336
|
});
|
|
302
337
|
}
|
|
338
|
+
this.logger.debug('SearcherService: findSymbol result', { source: 'grep', hits: hits.length, truncated: hits.length >= FIND_SYMBOL_LIMIT });
|
|
303
339
|
return { hits, truncated: hits.length >= FIND_SYMBOL_LIMIT };
|
|
304
340
|
}
|
|
305
341
|
listFiles(relativePath, project, depth, pattern) {
|
|
342
|
+
this.logger.debug('SearcherService: listFiles', { relativePath, project, depth, pattern });
|
|
306
343
|
const projectRoot = path.join(this.config.workspaceRoot, project);
|
|
307
344
|
const resolvedTarget = path.resolve(projectRoot, relativePath);
|
|
308
345
|
const resolvedProject = path.resolve(projectRoot);
|
|
@@ -318,12 +355,14 @@ class SearcherService {
|
|
|
318
355
|
const clampedDepth = Math.min(5, Math.max(1, depth));
|
|
319
356
|
const lines = [];
|
|
320
357
|
this.walkDir(resolvedTarget, 0, clampedDepth - 1, pattern, lines);
|
|
358
|
+
this.logger.debug('SearcherService: listFiles result', { entries: lines.length });
|
|
321
359
|
if (lines.length === 0) {
|
|
322
360
|
return { tree: pattern ? `No files matching "${pattern}" found.` : '(empty directory)' };
|
|
323
361
|
}
|
|
324
362
|
return { tree: lines.join('\n') };
|
|
325
363
|
}
|
|
326
364
|
async readFileAbsolute(absPath, maxLines) {
|
|
365
|
+
this.logger.debug('SearcherService: readFileAbsolute', { absPath, maxLines });
|
|
327
366
|
const resolvedRoot = path.resolve(this.config.workspaceRoot);
|
|
328
367
|
const resolved = path.resolve(absPath);
|
|
329
368
|
if (resolved !== resolvedRoot && !resolved.startsWith(resolvedRoot + path.sep)) {
|
|
@@ -356,9 +395,11 @@ class SearcherService {
|
|
|
356
395
|
throw err;
|
|
357
396
|
}
|
|
358
397
|
const relPath = path.relative(resolvedRoot, resolved);
|
|
398
|
+
this.logger.debug('SearcherService: readFileAbsolute result', { linesRead: lines.length, hasMore });
|
|
359
399
|
return { ok: true, filePath: relPath, startLine: 1, endLine: lines.length, lines, hasMore };
|
|
360
400
|
}
|
|
361
401
|
async readFileRange(filePath, startLine, endLine) {
|
|
402
|
+
this.logger.debug('SearcherService: readFileRange', { filePath, startLine, endLine });
|
|
362
403
|
const resolvedRoot = path.resolve(this.config.workspaceRoot);
|
|
363
404
|
const absPath = path.resolve(resolvedRoot, filePath);
|
|
364
405
|
if (absPath !== resolvedRoot && !absPath.startsWith(resolvedRoot + path.sep)) {
|
|
@@ -388,6 +429,7 @@ class SearcherService {
|
|
|
388
429
|
break;
|
|
389
430
|
}
|
|
390
431
|
}
|
|
432
|
+
this.logger.debug('SearcherService: readFileRange result', { linesRead: lines.length, hasMore });
|
|
391
433
|
return { ok: true, filePath, startLine, endLine: Math.min(endLine, lastRead), lines, hasMore };
|
|
392
434
|
}
|
|
393
435
|
isNonEnglish(text) {
|
|
@@ -432,6 +474,7 @@ class SearcherService {
|
|
|
432
474
|
type: `(export[[:space:]]+)?type[[:space:]]+${name}[[:space:]=<]`,
|
|
433
475
|
interface: `(export[[:space:]]+)?interface[[:space:]]+${name}[[:space:]{<]`,
|
|
434
476
|
variable: `(export[[:space:]]+)?(const|let|var)[[:space:]]+${name}[[:space:]=:]`,
|
|
477
|
+
method: `(public[[:space:]]+|async[[:space:]]+|static[[:space:]]+|override[[:space:]]+)+(async[[:space:]]+)?${name}[[:space:]]*[(<]`,
|
|
435
478
|
};
|
|
436
479
|
if (kind === 'all')
|
|
437
480
|
return Object.values(patterns).join('|');
|
|
@@ -447,6 +490,8 @@ class SearcherService {
|
|
|
447
490
|
return 'interface';
|
|
448
491
|
if (/\btype\s+$/.test(before))
|
|
449
492
|
return 'type';
|
|
493
|
+
if (/\b(public|async|static|override)\s+$/.test(before))
|
|
494
|
+
return 'method';
|
|
450
495
|
return 'variable';
|
|
451
496
|
}
|
|
452
497
|
}
|
package/dist/src/setup.d.ts
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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('\
|
|
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.
|
|
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
|
-
|
|
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',
|
|
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
|
-
|
|
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
|
|
160
|
-
vars.DB_PORT = await
|
|
161
|
-
vars.DB_USER = await
|
|
162
|
-
vars.DB_PASS = await
|
|
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
|
-
|
|
178
|
-
|
|
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
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
|
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
|
|
199
|
-
const
|
|
200
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
216
|
-
|
|
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}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codesearch-mcp",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.14-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",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@inquirer/prompts": "^8.4.1",
|
|
32
|
-
"@langchain/textsplitters": "^0.1
|
|
32
|
+
"@langchain/textsplitters": "^1.0.1",
|
|
33
33
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
34
34
|
"cli-progress": "^3.12.0",
|
|
35
35
|
"dotenv": "^17.4.2",
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"ollama": "^0.5.0",
|
|
38
38
|
"pg": "^8.13.0",
|
|
39
39
|
"pino": "^10.3.1",
|
|
40
|
+
"pino-roll": "^4.0.0",
|
|
40
41
|
"reflect-metadata": "^0.2.2",
|
|
41
42
|
"ts-morph": "^28.0.0",
|
|
42
43
|
"typeorm": "^0.3.20",
|