Skip to content

Commit d4ae7a5

Browse files
committed
fix: harden multi-project routing
1 parent 2189ba0 commit d4ae7a5

File tree

6 files changed

+179
-65
lines changed

6 files changed

+179
-65
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@
165165
"overrides": {
166166
"@modelcontextprotocol/sdk>ajv": "8.18.0",
167167
"@modelcontextprotocol/sdk>@hono/node-server": "1.19.11",
168+
"@modelcontextprotocol/sdk>express-rate-limit": "8.2.2",
168169
"@huggingface/transformers>onnxruntime-node": "1.24.2",
169170
"minimatch": "10.2.3",
170171
"rollup": "4.59.0"

pnpm-lock.yaml

Lines changed: 9 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/index.ts

Lines changed: 88 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ export const INDEX_CONSUMING_RESOURCE_NAMES = ['Codebase Intelligence'] as const
173173

174174
type IndexStatus = 'ready' | 'rebuild-required' | 'indexing' | 'unknown';
175175
type IndexConfidence = 'high' | 'low';
176-
type IndexAction = 'served' | 'rebuild-started' | 'rebuilt-and-served' | 'rebuild-failed';
176+
type IndexAction = 'served' | 'rebuild-started' | 'rebuilt-and-served';
177177

178178
export type IndexSignal = {
179179
status: IndexStatus;
@@ -230,6 +230,25 @@ async function ensureValidIndexOrAutoHeal(project: ProjectState): Promise<IndexS
230230
}
231231
}
232232

233+
async function validateProjectDirectory(rootPath: string): Promise<ToolResponse | undefined> {
234+
try {
235+
const stats = await fs.stat(rootPath);
236+
if (stats.isDirectory()) {
237+
return undefined;
238+
}
239+
240+
return buildProjectSelectionError(
241+
'unknown_project',
242+
`project_directory is not a directory: ${rootPath}`
243+
);
244+
} catch {
245+
return buildProjectSelectionError(
246+
'unknown_project',
247+
`project_directory does not exist: ${rootPath}`
248+
);
249+
}
250+
}
251+
233252
/**
234253
* Check if file/directory exists
235254
*/
@@ -349,14 +368,6 @@ async function generateCodebaseContext(project: ProjectState): Promise<string> {
349368
(index.reason ? `\nReason: ${index.reason}` : '')
350369
);
351370
}
352-
if (index.action === 'rebuild-failed') {
353-
return (
354-
'# Codebase Intelligence\n\n' +
355-
'Index rebuild required before intelligence can be served.\n\n' +
356-
`Index: ${index.status} (${index.confidence}, ${index.action})` +
357-
(index.reason ? `\nReason: ${index.reason}` : '')
358-
);
359-
}
360371

361372
try {
362373
const content = await fs.readFile(intelligencePath, 'utf-8');
@@ -685,9 +696,16 @@ async function resolveProjectForTool(args: Record<string, unknown>): Promise<Pro
685696
};
686697
}
687698

688-
const rootPath = knownRootPath ?? registerKnownRoot(requestedProjectDirectory);
699+
const rootPath = knownRootPath ?? requestedProjectDirectory;
700+
const invalidProjectResponse = await validateProjectDirectory(rootPath);
701+
if (invalidProjectResponse) {
702+
return { ok: false, response: invalidProjectResponse };
703+
}
704+
689705
const project = getOrCreateProject(rootPath);
690-
await initProject(project.rootPath, watcherDebounceMs);
706+
await initProject(project.rootPath, watcherDebounceMs, {
707+
enableWatcher: knownRootPath !== undefined
708+
});
691709
return { ok: true, project };
692710
}
693711

@@ -703,7 +721,7 @@ async function resolveProjectForTool(args: Record<string, unknown>): Promise<Pro
703721

704722
const [rootPath] = availableRoots;
705723
const project = getOrCreateProject(rootPath);
706-
await initProject(project.rootPath, watcherDebounceMs);
724+
await initProject(project.rootPath, watcherDebounceMs, { enableWatcher: true });
707725
return { ok: true, project };
708726
}
709727

@@ -715,7 +733,7 @@ async function resolveProjectForResource(): Promise<ProjectState | undefined> {
715733

716734
const [rootPath] = availableRoots;
717735
const project = getOrCreateProject(rootPath);
718-
await initProject(project.rootPath, watcherDebounceMs);
736+
await initProject(project.rootPath, watcherDebounceMs, { enableWatcher: true });
719737
return project;
720738
}
721739

@@ -768,7 +786,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
768786
};
769787
}
770788
indexSignal = await ensureValidIndexOrAutoHeal(project);
771-
if (indexSignal.action === 'rebuild-started' || indexSignal.action === 'rebuild-failed') {
789+
if (indexSignal.action === 'rebuild-started') {
772790
return {
773791
content: [
774792
{
@@ -817,45 +835,58 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
817835
* Initialize a project: migrate legacy structure, check index, start watcher.
818836
* Deduplicates via normalized root key.
819837
*/
820-
async function initProject(rootPath: string, debounceMs: number): Promise<void> {
821-
const project = getOrCreateProject(rootPath);
838+
type InitProjectOptions = {
839+
enableWatcher: boolean;
840+
};
822841

823-
// Skip if already initialized
824-
if (
825-
project.indexState.status === 'indexing' ||
826-
project.indexState.status === 'ready' ||
827-
project.stopWatcher
828-
) {
842+
async function ensureProjectInitialized(project: ProjectState): Promise<void> {
843+
if (project.initPromise) {
844+
await project.initPromise;
829845
return;
830846
}
831847

832-
// Migrate legacy structure
833-
try {
834-
const legacyPaths = makeLegacyPaths(project.rootPath);
835-
const migrated = await migrateToNewStructure(project.paths, legacyPaths);
836-
if (migrated && process.env.CODEBASE_CONTEXT_DEBUG) {
837-
console.error(`[DEBUG] Migrated to .codebase-context/ structure: ${project.rootPath}`);
838-
}
839-
} catch {
840-
// Non-fatal
848+
if (project.indexState.status !== 'idle') {
849+
return;
841850
}
842851

843-
// Check if indexing is needed
844-
const needsIndex = await shouldReindex(project.paths);
845-
if (needsIndex) {
846-
if (process.env.CODEBASE_CONTEXT_DEBUG) {
847-
console.error(`[DEBUG] Starting indexing: ${project.rootPath}`);
852+
project.initPromise = (async () => {
853+
// Migrate legacy structure
854+
try {
855+
const legacyPaths = makeLegacyPaths(project.rootPath);
856+
const migrated = await migrateToNewStructure(project.paths, legacyPaths);
857+
if (migrated && process.env.CODEBASE_CONTEXT_DEBUG) {
858+
console.error(`[DEBUG] Migrated to .codebase-context/ structure: ${project.rootPath}`);
859+
}
860+
} catch {
861+
// Non-fatal
848862
}
849-
void performIndexing(project);
850-
} else {
851-
if (process.env.CODEBASE_CONTEXT_DEBUG) {
852-
console.error(`[DEBUG] Index found. Ready: ${project.rootPath}`);
863+
864+
// Check if indexing is needed
865+
const needsIndex = await shouldReindex(project.paths);
866+
if (needsIndex) {
867+
if (process.env.CODEBASE_CONTEXT_DEBUG) {
868+
console.error(`[DEBUG] Starting indexing: ${project.rootPath}`);
869+
}
870+
void performIndexing(project);
871+
} else {
872+
if (process.env.CODEBASE_CONTEXT_DEBUG) {
873+
console.error(`[DEBUG] Index found. Ready: ${project.rootPath}`);
874+
}
875+
project.indexState.status = 'ready';
876+
project.indexState.lastIndexed = new Date();
853877
}
854-
project.indexState.status = 'ready';
855-
project.indexState.lastIndexed = new Date();
878+
})().finally(() => {
879+
project.initPromise = undefined;
880+
});
881+
882+
await project.initPromise;
883+
}
884+
885+
function ensureProjectWatcher(project: ProjectState, debounceMs: number): void {
886+
if (project.stopWatcher) {
887+
return;
856888
}
857889

858-
// Start file watcher
859890
project.stopWatcher = startFileWatcher({
860891
rootPath: project.rootPath,
861892
debounceMs,
@@ -881,6 +912,19 @@ async function initProject(rootPath: string, debounceMs: number): Promise<void>
881912
});
882913
}
883914

915+
async function initProject(
916+
rootPath: string,
917+
debounceMs: number,
918+
options: InitProjectOptions
919+
): Promise<void> {
920+
const project = getOrCreateProject(rootPath);
921+
await ensureProjectInitialized(project);
922+
923+
if (options.enableWatcher) {
924+
ensureProjectWatcher(project, debounceMs);
925+
}
926+
}
927+
884928
async function main() {
885929
// Validate root path exists and is a directory
886930
try {
@@ -928,7 +972,7 @@ async function main() {
928972
// Preserve current single-project startup behavior without eagerly indexing every root.
929973
const startupRoots = getKnownRootPaths();
930974
if (startupRoots.length === 1) {
931-
await initProject(startupRoots[0], watcherDebounceMs);
975+
await initProject(startupRoots[0], watcherDebounceMs, { enableWatcher: true });
932976
}
933977

934978
// Subscribe to root changes

src/project-state.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export interface ProjectState {
1515
paths: ToolPaths;
1616
indexState: IndexState;
1717
autoRefresh: AutoRefreshController;
18+
initPromise?: Promise<void>;
1819
stopWatcher?: () => void;
1920
}
2021

src/tools/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import type { ToolContext, ToolResponse } from './types.js';
1818
const PROJECT_DIRECTORY_PROPERTY: Record<string, string> = {
1919
type: 'string',
2020
description:
21-
'Optional absolute path or file:// URI for the project root to use when multiple roots are available.'
21+
'Optional absolute path or file:// URI for the project root to use for this call. Must point to an existing directory.'
2222
};
2323

2424
function withProjectDirectory(definition: Tool): Tool {

0 commit comments

Comments
 (0)