Skip to content

Commit f0707e6

Browse files
committed
Add workspace validation
1 parent 5a4973f commit f0707e6

File tree

4 files changed

+117
-26
lines changed

4 files changed

+117
-26
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "directory-indexer",
3-
"version": "0.1.2",
3+
"version": "0.1.3",
44
"description": "AI-powered directory indexing with semantic search for MCP servers",
55
"main": "dist/cli.js",
66
"bin": {

src/mcp-handlers.ts

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -86,25 +86,39 @@ export async function handleIndexTool(args: unknown, config: Config): Promise<Ca
8686
};
8787
}
8888

89+
async function validateWorkspace(workspace?: string): Promise<{ workspace?: string; message?: string }> {
90+
if (!workspace) return { workspace };
91+
92+
const config = (await import('./config.js')).loadConfig();
93+
const { getAvailableWorkspaces } = await import('./config.js');
94+
const availableWorkspaces = getAvailableWorkspaces(config);
95+
96+
if (availableWorkspaces.includes(workspace)) {
97+
return { workspace };
98+
}
99+
100+
// Invalid workspace - search all content with informative message
101+
const message = availableWorkspaces.length > 0
102+
? `Note: Workspace '${workspace}' not found. Searching all content instead. Available workspaces: ${availableWorkspaces.join(', ')}. Use server_info tool to see workspace details.`
103+
: `Note: Workspace '${workspace}' not found and no workspaces are configured. Searching all indexed content.`;
104+
105+
return { workspace: undefined, message };
106+
}
107+
89108
export async function handleSearchTool(args: unknown): Promise<CallToolResult> {
90109
if (!isSearchToolArgs(args)) {
91110
throw new Error('query is required');
92111
}
93112

94-
const options = {
95-
limit: args.limit || 10,
96-
workspace: args.workspace
97-
};
113+
const { workspace, message } = await validateWorkspace(args.workspace);
114+
const results = await searchContent(args.query, { limit: args.limit || 10, workspace });
98115

99-
const results = await searchContent(args.query, options);
116+
const response = message
117+
? `${message}\n\n${JSON.stringify(results, null, 2)}`
118+
: JSON.stringify(results, null, 2);
100119

101120
return {
102-
content: [
103-
{
104-
type: 'text',
105-
text: JSON.stringify(results, null, 2)
106-
}
107-
]
121+
content: [{ type: 'text', text: response }]
108122
};
109123
}
110124

@@ -113,19 +127,15 @@ export async function handleSimilarFilesTool(args: unknown): Promise<CallToolRes
113127
throw new Error('file_path is required');
114128
}
115129

116-
const results = await findSimilarFiles(
117-
args.file_path,
118-
args.limit || 10,
119-
args.workspace
120-
);
130+
const { workspace, message } = await validateWorkspace(args.workspace);
131+
const results = await findSimilarFiles(args.file_path, args.limit || 10, workspace);
132+
133+
const response = message
134+
? `${message}\n\n${JSON.stringify(results, null, 2)}`
135+
: JSON.stringify(results, null, 2);
121136

122137
return {
123-
content: [
124-
{
125-
type: 'text',
126-
text: JSON.stringify(results, null, 2)
127-
}
128-
]
138+
content: [{ type: 'text', text: response }]
129139
};
130140
}
131141

src/mcp.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ Example queries:
108108
},
109109
workspace: {
110110
type: 'string',
111-
description: 'Optional workspace name to filter search results. Only files within the workspace directories will be searched. Use server_info to see available workspaces.'
111+
description: 'Optional workspace name to filter search results. Only files within the workspace directories will be searched. IMPORTANT: Use server_info tool first to discover available workspace names - using invalid workspace names will result in empty results.'
112112
}
113113
},
114114
required: ['query']
@@ -150,7 +150,7 @@ Returns file paths with similarity scores. Use get_content to read full files or
150150
},
151151
workspace: {
152152
type: 'string',
153-
description: 'Optional workspace name to filter results. Only files within the workspace directories will be considered. Use server_info to see available workspaces.'
153+
description: 'Optional workspace name to filter results. Only files within the workspace directories will be considered. IMPORTANT: Use server_info tool first to discover available workspace names - using invalid workspace names will result in empty results.'
154154
}
155155
},
156156
required: ['file_path']
@@ -235,6 +235,7 @@ Returns chunk content as text. Use this with chunk IDs from search results to ge
235235
description: `Get information about server status and indexed content. Shows what directories and files are available for search.
236236
237237
When to use this tool:
238+
- REQUIRED: Check available workspace names before using workspace parameter in search or similar_files tools
238239
- Check what content is already indexed before performing searches
239240
- Verify system is working properly
240241
- See indexing statistics and status
@@ -244,14 +245,16 @@ How it works:
244245
- Reports total indexed directories, files, and chunks
245246
- Shows database size and last indexing time
246247
- Lists all indexed directories with file counts
248+
- Lists all configured workspaces with their paths and file counts
247249
- Reports any errors or issues
248250
249251
Examples:
252+
- Check workspaces before searching: "What workspaces are available?"
250253
- Check before searching: "What content is indexed?"
251254
- Verify after indexing: "Did the indexing complete successfully?"
252255
- Monitor system: "How many files are searchable?"
253256
254-
Returns server version, indexing statistics, directory list, and any errors. Use this to understand what content is available for search and similar_files tools.`,
257+
Returns server version, indexing statistics, directory list, workspace information, and any errors. IMPORTANT: Always use this tool first to discover available workspace names when you need to search within specific workspaces.`,
255258
inputSchema: {
256259
type: 'object',
257260
properties: {},

tests/mcp-handlers.test.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ vi.mock('../src/storage.js', () => ({
2626
getIndexStatus: vi.fn()
2727
}));
2828

29+
vi.mock('../src/config.js', () => ({
30+
...vi.importActual('../src/config.js'),
31+
loadConfig: vi.fn(),
32+
getAvailableWorkspaces: vi.fn()
33+
}));
34+
2935
describe('MCP Handlers Unit Tests', () => {
3036
beforeEach(() => {
3137
vi.clearAllMocks();
@@ -70,8 +76,11 @@ Errors: [
7076
describe('handleSearchTool', () => {
7177
it('should handle valid search request', async () => {
7278
const { searchContent } = await import('../src/search.js');
79+
const { loadConfig, getAvailableWorkspaces } = await import('../src/config.js');
7380
const mockResults = [{ filePath: '/test.md', score: 0.9, fileSizeBytes: 1024, matchingChunks: 2, chunks: [] }];
7481
vi.mocked(searchContent).mockResolvedValue(mockResults);
82+
vi.mocked(loadConfig).mockReturnValue({ workspaces: { docs: { paths: ['/docs'], isValid: true } } } as any);
83+
vi.mocked(getAvailableWorkspaces).mockReturnValue(['docs']);
7584

7685
const args = { query: 'test search', limit: 5, workspace: 'docs' };
7786

@@ -97,6 +106,39 @@ Errors: [
97106
expect(searchContent).toHaveBeenCalledWith('test', { limit: 10, workspace: undefined });
98107
});
99108

109+
it('should handle invalid workspace by searching all content', async () => {
110+
const { searchContent } = await import('../src/search.js');
111+
const { loadConfig, getAvailableWorkspaces } = await import('../src/config.js');
112+
const mockResults = [{ filePath: '/test.md', score: 0.9, fileSizeBytes: 1024, matchingChunks: 2, chunks: [] }];
113+
vi.mocked(searchContent).mockResolvedValue(mockResults);
114+
vi.mocked(loadConfig).mockReturnValue({ workspaces: { docs: { paths: ['/docs'], isValid: true } } } as any);
115+
vi.mocked(getAvailableWorkspaces).mockReturnValue(['docs']);
116+
117+
const args = { query: 'test search', workspace: 'invalid' };
118+
119+
const result = await handleSearchTool(args);
120+
121+
expect(searchContent).toHaveBeenCalledWith('test search', { limit: 10, workspace: undefined });
122+
expect(result.content[0].text).toContain('Workspace \'invalid\' not found');
123+
expect(result.content[0].text).toContain('Available workspaces: docs');
124+
});
125+
126+
it('should handle invalid workspace when no workspaces configured', async () => {
127+
const { searchContent } = await import('../src/search.js');
128+
const { loadConfig, getAvailableWorkspaces } = await import('../src/config.js');
129+
const mockResults = [{ filePath: '/test.md', score: 0.9, fileSizeBytes: 1024, matchingChunks: 2, chunks: [] }];
130+
vi.mocked(searchContent).mockResolvedValue(mockResults);
131+
vi.mocked(loadConfig).mockReturnValue({ workspaces: {} } as any);
132+
vi.mocked(getAvailableWorkspaces).mockReturnValue([]);
133+
134+
const args = { query: 'test search', workspace: 'invalid' };
135+
136+
const result = await handleSearchTool(args);
137+
138+
expect(searchContent).toHaveBeenCalledWith('test search', { limit: 10, workspace: undefined });
139+
expect(result.content[0].text).toContain('no workspaces are configured');
140+
});
141+
100142
it('should throw error for missing query', async () => {
101143
await expect(handleSearchTool({})).rejects.toThrow('query is required');
102144
await expect(handleSearchTool(null)).rejects.toThrow('query is required');
@@ -107,8 +149,11 @@ Errors: [
107149
describe('handleSimilarFilesTool', () => {
108150
it('should handle valid similar files request', async () => {
109151
const { findSimilarFiles } = await import('../src/search.js');
152+
const { loadConfig, getAvailableWorkspaces } = await import('../src/config.js');
110153
const mockResults = [{ filePath: '/similar.md', score: 0.8, fileSizeBytes: 512 }];
111154
vi.mocked(findSimilarFiles).mockResolvedValue(mockResults);
155+
vi.mocked(loadConfig).mockReturnValue({ workspaces: { code: { paths: ['/code'], isValid: true } } } as any);
156+
vi.mocked(getAvailableWorkspaces).mockReturnValue(['code']);
112157

113158
const args = { file_path: '/test.md', limit: 3, workspace: 'code' };
114159

@@ -134,6 +179,39 @@ Errors: [
134179
expect(findSimilarFiles).toHaveBeenCalledWith('/test.md', 10, undefined);
135180
});
136181

182+
it('should handle invalid workspace by searching all content', async () => {
183+
const { findSimilarFiles } = await import('../src/search.js');
184+
const { loadConfig, getAvailableWorkspaces } = await import('../src/config.js');
185+
const mockResults = [{ filePath: '/similar.md', score: 0.8, fileSizeBytes: 512 }];
186+
vi.mocked(findSimilarFiles).mockResolvedValue(mockResults);
187+
vi.mocked(loadConfig).mockReturnValue({ workspaces: { code: { paths: ['/code'], isValid: true } } } as any);
188+
vi.mocked(getAvailableWorkspaces).mockReturnValue(['code']);
189+
190+
const args = { file_path: '/test.md', workspace: 'invalid' };
191+
192+
const result = await handleSimilarFilesTool(args);
193+
194+
expect(findSimilarFiles).toHaveBeenCalledWith('/test.md', 10, undefined);
195+
expect(result.content[0].text).toContain('Workspace \'invalid\' not found');
196+
expect(result.content[0].text).toContain('Available workspaces: code');
197+
});
198+
199+
it('should handle invalid workspace when no workspaces configured', async () => {
200+
const { findSimilarFiles } = await import('../src/search.js');
201+
const { loadConfig, getAvailableWorkspaces } = await import('../src/config.js');
202+
const mockResults = [{ filePath: '/similar.md', score: 0.8, fileSizeBytes: 512 }];
203+
vi.mocked(findSimilarFiles).mockResolvedValue(mockResults);
204+
vi.mocked(loadConfig).mockReturnValue({ workspaces: {} } as any);
205+
vi.mocked(getAvailableWorkspaces).mockReturnValue([]);
206+
207+
const args = { file_path: '/test.md', workspace: 'invalid' };
208+
209+
const result = await handleSimilarFilesTool(args);
210+
211+
expect(findSimilarFiles).toHaveBeenCalledWith('/test.md', 10, undefined);
212+
expect(result.content[0].text).toContain('no workspaces are configured');
213+
});
214+
137215
it('should throw error for missing file_path', async () => {
138216
await expect(handleSimilarFilesTool({})).rejects.toThrow('file_path is required');
139217
await expect(handleSimilarFilesTool(null)).rejects.toThrow('file_path is required');

0 commit comments

Comments
 (0)