Skip to content

Commit 2d7237f

Browse files
authored
Add pre-requisite check to mcp failure responses (#4)
* Add pre-requisite check to mcp failure responses * Only update codecov on main branch ci
1 parent 816650b commit 2d7237f

File tree

6 files changed

+105
-28
lines changed

6 files changed

+105
-28
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ jobs:
228228
DIRECTORY_INDEXER_QDRANT_COLLECTION: directory-indexer-test
229229

230230
- name: Upload coverage to Codecov
231+
if: github.ref == 'refs/heads/main'
231232
uses: codecov/codecov-action@v4
232233
with:
233234
token: ${{ secrets.CODECOV_TOKEN }}
@@ -237,7 +238,7 @@ jobs:
237238
fail_ci_if_error: false
238239

239240
- name: Upload test results to Codecov
240-
if: ${{ !cancelled() }}
241+
if: github.ref == 'refs/heads/main' && !cancelled()
241242
uses: codecov/test-results-action@v1
242243
with:
243244
token: ${{ secrets.CODECOV_TOKEN }}

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.5",
3+
"version": "0.1.6",
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: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Config } from './config.js';
22
import { indexDirectories } from './indexing.js';
33
import { searchContent, findSimilarFiles, getFileContent, getChunkContent } from './search.js';
44
import { getIndexStatus } from './storage.js';
5+
import { validateIndexPrerequisites, validateSearchPrerequisites } from './prerequisites.js';
56
import { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
67

78
// Type-safe interfaces for MCP tool arguments
@@ -63,6 +64,9 @@ export async function handleIndexTool(args: unknown, config: Config): Promise<Ca
6364
throw new Error('directory_path is required');
6465
}
6566

67+
// Validate prerequisites before proceeding
68+
await validateIndexPrerequisites(config);
69+
6670
const paths = args.directory_path.split(',').map((p: string) => p.trim());
6771
const result = await indexDirectories(paths, config);
6872

@@ -110,6 +114,10 @@ export async function handleSearchTool(args: unknown): Promise<CallToolResult> {
110114
throw new Error('query is required');
111115
}
112116

117+
// Validate prerequisites before proceeding
118+
const config = (await import('./config.js')).loadConfig();
119+
await validateSearchPrerequisites(config);
120+
113121
const { workspace, message } = await validateWorkspace(args.workspace);
114122
const results = await searchContent(args.query, { limit: args.limit || 10, workspace });
115123

@@ -127,6 +135,10 @@ export async function handleSimilarFilesTool(args: unknown): Promise<CallToolRes
127135
throw new Error('file_path is required');
128136
}
129137

138+
// Validate prerequisites before proceeding
139+
const config = (await import('./config.js')).loadConfig();
140+
await validateSearchPrerequisites(config);
141+
130142
const { workspace, message } = await validateWorkspace(args.workspace);
131143
const results = await findSimilarFiles(args.file_path, args.limit || 10, workspace);
132144

src/prerequisites.ts

Lines changed: 82 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -71,52 +71,111 @@ export async function checkOpenAI(config: Config): Promise<boolean> {
7171
}
7272
}
7373

74+
export interface ServiceStatus {
75+
service: string;
76+
status: 'available' | 'unavailable';
77+
details?: string;
78+
}
79+
80+
export interface PrerequisiteValidationResult {
81+
allPassed: boolean;
82+
services: ServiceStatus[];
83+
}
84+
7485
/**
75-
* Generate error message for failed prerequisites
86+
* Check all prerequisites and return structured status
7687
*/
77-
function createErrorMessage(qdrantOk: boolean, embeddingOk: boolean, provider: string): string {
78-
const errors: string[] = [];
88+
export async function checkAllPrerequisitesDetailed(config: Config): Promise<PrerequisiteValidationResult> {
89+
const services: ServiceStatus[] = [];
7990

80-
if (!qdrantOk) {
81-
errors.push('Qdrant database is inaccessible');
82-
}
91+
// Check Qdrant
92+
const qdrantOk = await checkQdrant(config);
93+
services.push({
94+
service: 'qdrant',
95+
status: qdrantOk ? 'available' : 'unavailable',
96+
details: qdrantOk ? undefined : `Cannot connect to Qdrant at ${config.storage.qdrantEndpoint}`
97+
});
8398

84-
if (!embeddingOk) {
85-
if (provider === 'ollama') {
86-
errors.push('Ollama embedding service is inaccessible or model unavailable');
87-
} else if (provider === 'openai') {
88-
errors.push('OpenAI API is inaccessible or key invalid');
99+
// Check embedding service
100+
let embeddingOk = false;
101+
let embeddingDetails: string | undefined;
102+
103+
if (config.embedding.provider === 'ollama') {
104+
const ollamaOk = await checkOllama(config);
105+
const modelOk = ollamaOk ? await checkOllamaModel(config) : false;
106+
embeddingOk = ollamaOk && modelOk;
107+
108+
if (!ollamaOk) {
109+
embeddingDetails = `Cannot connect to Ollama at ${config.embedding.endpoint}`;
110+
} else if (!modelOk) {
111+
embeddingDetails = `Model "${config.embedding.model}" not available in Ollama`;
112+
}
113+
} else if (config.embedding.provider === 'openai') {
114+
embeddingOk = await checkOpenAI(config);
115+
if (!embeddingOk) {
116+
embeddingDetails = process.env.OPENAI_API_KEY ?
117+
'OpenAI API request failed' :
118+
'OPENAI_API_KEY environment variable not set';
89119
}
120+
} else if (config.embedding.provider === 'mock') {
121+
embeddingOk = true;
122+
} else {
123+
embeddingDetails = `Unknown embedding provider: ${config.embedding.provider}`;
90124
}
91125

92-
errors.push('');
93-
errors.push('For setup instructions, see: https://github.com/peteretelej/directory-indexer#setup');
126+
services.push({
127+
service: config.embedding.provider,
128+
status: embeddingOk ? 'available' : 'unavailable',
129+
details: embeddingDetails
130+
});
94131

95-
return errors.join('\n');
132+
return {
133+
allPassed: services.every(s => s.status === 'available'),
134+
services
135+
};
136+
}
137+
138+
/**
139+
* Generate comprehensive error message that lists all missing services
140+
*/
141+
function createComprehensiveErrorMessage(result: PrerequisiteValidationResult): string {
142+
const unavailableServices = result.services.filter(s => s.status === 'unavailable');
143+
144+
const serviceDescriptions = unavailableServices.map(service => {
145+
return `${service.service} (${service.details})`;
146+
});
147+
148+
const message = `Required services are not available to use directory-indexer features: ${serviceDescriptions.join(', ')}.`;
149+
const setup = 'For setup instructions, see: https://github.com/peteretelej/directory-indexer#setup';
150+
151+
return `${message}\n\n${setup}`;
96152
}
97153

98154
/**
99155
* Validate all prerequisites for indexing (needs both Qdrant and embedding service)
100156
*/
101157
export async function validateIndexPrerequisites(config: Config): Promise<void> {
102-
const [qdrantOk, embeddingOk] = await Promise.all([
103-
checkQdrant(config),
104-
checkEmbeddingService(config)
105-
]);
158+
const result = await checkAllPrerequisitesDetailed(config);
106159

107-
if (!qdrantOk || !embeddingOk) {
108-
throw new PrerequisiteError(createErrorMessage(qdrantOk, embeddingOk, config.embedding.provider));
160+
if (!result.allPassed) {
161+
throw new PrerequisiteError(createComprehensiveErrorMessage(result));
109162
}
110163
}
111164

112165
/**
113166
* Validate prerequisites for search (needs only Qdrant)
114167
*/
115168
export async function validateSearchPrerequisites(config: Config): Promise<void> {
116-
const qdrantOk = await checkQdrant(config);
169+
const result = await checkAllPrerequisitesDetailed(config);
170+
const qdrantService = result.services.find(s => s.service === 'qdrant');
117171

118-
if (!qdrantOk) {
119-
throw new PrerequisiteError(createErrorMessage(false, true, config.embedding.provider));
172+
if (qdrantService?.status === 'unavailable') {
173+
// Create a result with only Qdrant for error message
174+
const qdrantOnlyResult: PrerequisiteValidationResult = {
175+
allPassed: false,
176+
services: [qdrantService]
177+
};
178+
throw new PrerequisiteError(createComprehensiveErrorMessage(qdrantOnlyResult));
120179
}
121180
}
122181

tests/mcp-handlers.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ vi.mock('../src/config.js', () => ({
3232
getAvailableWorkspaces: vi.fn()
3333
}));
3434

35+
vi.mock('../src/prerequisites.js', () => ({
36+
validateIndexPrerequisites: vi.fn(),
37+
validateSearchPrerequisites: vi.fn()
38+
}));
39+
3540
describe('MCP Handlers Unit Tests', () => {
3641
beforeEach(() => {
3742
vi.clearAllMocks();

tests/prerequisites.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ describe('Prerequisites Tests', () => {
3636
};
3737

3838
await expect(validateSearchPrerequisites(invalidConfig)).rejects.toThrow(PrerequisiteError);
39-
await expect(validateSearchPrerequisites(invalidConfig)).rejects.toThrow('Qdrant database is inaccessible');
39+
await expect(validateSearchPrerequisites(invalidConfig)).rejects.toThrow('Required services are not available');
4040
});
4141

4242
it('should fail when Ollama is unreachable', async () => {
@@ -46,7 +46,7 @@ describe('Prerequisites Tests', () => {
4646
};
4747

4848
await expect(validateIndexPrerequisites(invalidConfig)).rejects.toThrow(PrerequisiteError);
49-
await expect(validateIndexPrerequisites(invalidConfig)).rejects.toThrow('Ollama embedding service is inaccessible');
49+
await expect(validateIndexPrerequisites(invalidConfig)).rejects.toThrow('Required services are not available');
5050
});
5151

5252
it('should fail when OpenAI API key is missing', async () => {
@@ -60,7 +60,7 @@ describe('Prerequisites Tests', () => {
6060
};
6161

6262
await expect(validateIndexPrerequisites(openaiConfig)).rejects.toThrow(PrerequisiteError);
63-
await expect(validateIndexPrerequisites(openaiConfig)).rejects.toThrow('OpenAI API is inaccessible');
63+
await expect(validateIndexPrerequisites(openaiConfig)).rejects.toThrow('Required services are not available');
6464
} finally {
6565
if (originalKey) process.env.OPENAI_API_KEY = originalKey;
6666
}

0 commit comments

Comments
 (0)