codesearch-mcp 0.0.13-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/project-selector.js +32 -20
- package/dist/src/services/AstParserService.js +40 -28
- package/dist/src/services/DatabaseService.js +4 -2
- package/dist/src/services/LoggerService.d.ts +1 -1
- package/dist/src/services/LoggerService.js +11 -5
- package/dist/src/services/McpServer.d.ts +1 -1
- package/dist/src/services/McpServer.js +16 -16
- 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 +2 -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);
|
|
@@ -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) {
|
|
@@ -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
|
}
|
|
@@ -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,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
|
|
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
|
|
@@ -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;
|
|
@@ -65,30 +65,22 @@ You have access to a local semantic code search server. Use these tools to navig
|
|
|
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.
|
|
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
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
|
-
8. If \`search_codebase\` returns results that seem off-topic, pass a lower \`threshold\` (e.g. \`0.35\`) to filter out weak semantic matches.
|
|
75
74
|
|
|
76
|
-
##
|
|
75
|
+
## Dependency graph
|
|
77
76
|
|
|
78
|
-
|
|
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.
|
|
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).
|
|
80
78
|
|
|
81
|
-
|
|
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.
|
|
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.
|
|
84
80
|
|
|
85
81
|
## Relevance scores
|
|
86
82
|
|
|
87
|
-
Lower score = better match (cosine distance). Results above the configured threshold are filtered out automatically. Pass \`threshold\` to \`search_codebase\` to override
|
|
88
|
-
|
|
89
|
-
## Dependency graph
|
|
90
|
-
|
|
91
|
-
\`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).
|
|
92
84
|
`.trim();
|
|
93
85
|
class McpServer {
|
|
94
86
|
searcher;
|
|
@@ -99,7 +91,7 @@ class McpServer {
|
|
|
99
91
|
this.config = config;
|
|
100
92
|
this.logger = logger;
|
|
101
93
|
}
|
|
102
|
-
async start() {
|
|
94
|
+
async start(startupError) {
|
|
103
95
|
const server = new index_js_1.Server({ name: 'local-rag', version: PKG_VERSION }, { capabilities: { tools: {} }, instructions: SERVER_INSTRUCTIONS });
|
|
104
96
|
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
|
|
105
97
|
tools: [
|
|
@@ -206,6 +198,14 @@ class McpServer {
|
|
|
206
198
|
],
|
|
207
199
|
}));
|
|
208
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
|
+
}
|
|
209
209
|
try {
|
|
210
210
|
if (req.params.name === 'list_projects') {
|
|
211
211
|
const rows = await this.searcher.listProjects();
|
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",
|