@@ -4,7 +4,23 @@ import { searchContent, findSimilarFiles, getFileContent, getChunkContent } from
44import { getIndexStatus , SQLiteStorage , initializeStorage } from './storage.js' ;
55import { validateIndexPrerequisites , validateSearchPrerequisites } from './prerequisites.js' ;
66import { validatePathWithinIndexedDirs , resolveIndexedDirectories } from './path-validation.js' ;
7+ import { log } from './logger.js' ;
78import { CallToolResult } from '@modelcontextprotocol/sdk/types.js' ;
9+ import type { Server } from '@modelcontextprotocol/sdk/server/index.js' ;
10+
11+ // MCP server reference for sending client-visible log notifications
12+ let mcpServer : Server | null = null ;
13+
14+ /**
15+ * Set the MCP server reference for logging notifications.
16+ * Called from startMcpServer() after server creation.
17+ */
18+ export function setMcpServer ( server : Server ) : void {
19+ mcpServer = server ;
20+ }
21+
22+ // Workspace-level indexing mutex: keyed by normalized directory path
23+ const indexingMutex = new Map < string , Promise < void > > ( ) ;
824
925// Cached set of resolved indexed directory paths for path validation
1026let indexedDirsCache : Set < string > = new Set ( ) ;
@@ -94,34 +110,71 @@ export async function handleIndexTool(args: unknown, config: Config): Promise<Ca
94110 await validateIndexPrerequisites ( config ) ;
95111
96112 const paths = args . directory_paths . map ( ( p : string ) => p . trim ( ) ) ;
97- const result = await indexDirectories ( paths , config ) ;
98113
99- // Refresh the indexed directories cache after successful indexing
100- const { sqlite } = await initializeStorage ( config ) ;
114+ log ( 'info' , 'Index start' , { directories : paths } ) ;
115+ mcpServer ?. sendLoggingMessage ( { level : 'info' , data : { event : 'index_start' , directories : paths } } ) ;
116+
117+ // Per-directory mutex: serialize concurrent calls targeting the same directory
118+ for ( const dirPath of paths ) {
119+ const existing = indexingMutex . get ( dirPath ) ;
120+ if ( existing ) {
121+ log ( 'info' , 'Waiting for ongoing indexing' , { directory : dirPath } ) ;
122+ await existing ;
123+ }
124+ }
125+
126+ // Create a deferred promise for this indexing operation
127+ let resolveIndexing : ( ) => void ;
128+ const indexingPromise = new Promise < void > ( ( resolve ) => { resolveIndexing = resolve ; } ) ;
129+ for ( const dirPath of paths ) {
130+ indexingMutex . set ( dirPath , indexingPromise ) ;
131+ }
132+
101133 try {
102- refreshIndexedDirsCache ( sqlite ) ;
134+ const result = await indexDirectories ( paths , config ) ;
135+
136+ // Refresh the indexed directories cache after successful indexing
137+ const { sqlite } = await initializeStorage ( config ) ;
138+ try {
139+ refreshIndexedDirsCache ( sqlite ) ;
140+ } finally {
141+ sqlite . close ( ) ;
142+ }
143+
144+ log ( 'info' , 'Index complete' , { result } ) ;
145+ mcpServer ?. sendLoggingMessage ( { level : 'info' , data : { event : 'index_complete' , result } } ) ;
146+
147+ let responseText = `Indexed ${ result . indexed } files, skipped ${ result . skipped } files, cleaned up ${ result . deleted } deleted files, ${ result . failed } failed` ;
148+
149+ if ( result . errors . length > 0 ) {
150+ responseText += `\nErrors: [\n` ;
151+ result . errors . forEach ( error => {
152+ responseText += ` '${ error } '\n` ;
153+ } ) ;
154+ responseText += `]` ;
155+ }
156+
157+ return {
158+ content : [
159+ {
160+ type : 'text' ,
161+ text : responseText
162+ }
163+ ]
164+ } ;
165+ } catch ( error ) {
166+ const errorMessage = error instanceof Error ? error . message : String ( error ) ;
167+ log ( 'error' , 'Index error' , { error : errorMessage , directories : paths } ) ;
168+ mcpServer ?. sendLoggingMessage ( { level : 'error' , data : { event : 'index_error' , error : errorMessage } } ) ;
169+ throw new Error (
170+ `Indexing failed for ${ paths . join ( ', ' ) } . Verify the directory exists and is readable. Use 'server_info' to check current status.`
171+ ) ;
103172 } finally {
104- sqlite . close ( ) ;
105- }
106-
107- let responseText = `Indexed ${ result . indexed } files, skipped ${ result . skipped } files, cleaned up ${ result . deleted } deleted files, ${ result . failed } failed` ;
108-
109- if ( result . errors . length > 0 ) {
110- responseText += `\nErrors: [\n` ;
111- result . errors . forEach ( error => {
112- responseText += ` '${ error } '\n` ;
113- } ) ;
114- responseText += `]` ;
173+ resolveIndexing ! ( ) ;
174+ for ( const dirPath of paths ) {
175+ indexingMutex . delete ( dirPath ) ;
176+ }
115177 }
116-
117- return {
118- content : [
119- {
120- type : 'text' ,
121- text : responseText
122- }
123- ]
124- } ;
125178}
126179
127180async function validateWorkspace ( workspace ?: string ) : Promise < { workspace ?: string ; message ?: string } > {
@@ -195,16 +248,26 @@ export async function handleGetContentTool(args: unknown, config?: Config): Prom
195248 await ensureIndexedDirsCache ( resolvedConfig ) ;
196249 validatePathWithinIndexedDirs ( args . file_path , indexedDirsCache ) ;
197250
198- const content = await getFileContent ( args . file_path , args . chunks ) ;
199-
200- return {
201- content : [
202- {
203- type : 'text' ,
204- text : content
205- }
206- ]
207- } ;
251+ try {
252+ const content = await getFileContent ( args . file_path , args . chunks ) ;
253+
254+ return {
255+ content : [
256+ {
257+ type : 'text' ,
258+ text : content
259+ }
260+ ]
261+ } ;
262+ } catch ( error ) {
263+ const msg = error instanceof Error ? error . message : String ( error ) ;
264+ if ( msg . includes ( 'ENOENT' ) || msg . toLowerCase ( ) . includes ( 'not found' ) || msg . toLowerCase ( ) . includes ( 'no such file' ) ) {
265+ throw new Error (
266+ `File not found: ${ args . file_path } . The file may have been moved or deleted. Use 'search' to find similar content.`
267+ ) ;
268+ }
269+ throw error ;
270+ }
208271}
209272
210273export async function handleGetChunkTool ( args : unknown , config ?: Config ) : Promise < CallToolResult > {
@@ -217,16 +280,26 @@ export async function handleGetChunkTool(args: unknown, config?: Config): Promis
217280 await ensureIndexedDirsCache ( resolvedConfig ) ;
218281 validatePathWithinIndexedDirs ( args . file_path , indexedDirsCache ) ;
219282
220- const content = await getChunkContent ( args . file_path , args . chunk_id ) ;
221-
222- return {
223- content : [
224- {
225- type : 'text' ,
226- text : content
227- }
228- ]
229- } ;
283+ try {
284+ const content = await getChunkContent ( args . file_path , args . chunk_id ) ;
285+
286+ return {
287+ content : [
288+ {
289+ type : 'text' ,
290+ text : content
291+ }
292+ ]
293+ } ;
294+ } catch ( error ) {
295+ const msg = error instanceof Error ? error . message : String ( error ) ;
296+ if ( msg . includes ( 'ENOENT' ) || msg . toLowerCase ( ) . includes ( 'not found' ) || msg . toLowerCase ( ) . includes ( 'no such file' ) ) {
297+ throw new Error (
298+ `File not found: ${ args . file_path } . The file may have been moved or deleted. Use 'search' to find similar content.`
299+ ) ;
300+ }
301+ throw error ;
302+ }
230303}
231304
232305export async function handleServerInfoTool ( version : string ) : Promise < CallToolResult > {
@@ -248,6 +321,7 @@ export async function handleServerInfoTool(version: string): Promise<CallToolRes
248321
249322export function formatErrorResponse ( error : unknown ) : CallToolResult {
250323 const errorMessage = error instanceof Error ? error . message : 'Unknown error' ;
324+ log ( 'error' , 'Tool error' , { error : errorMessage } ) ;
251325 return {
252326 content : [
253327 {
0 commit comments