mcp-code-indexer 2.0.2__py3-none-any.whl → 2.2.0__py3-none-any.whl
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.
- mcp_code_indexer/ask_handler.py +217 -0
- mcp_code_indexer/claude_api_handler.py +355 -0
- mcp_code_indexer/database/connection_health.py +187 -3
- mcp_code_indexer/database/database.py +94 -68
- mcp_code_indexer/database/exceptions.py +303 -0
- mcp_code_indexer/database/retry_executor.py +359 -0
- mcp_code_indexer/deepask_handler.py +465 -0
- mcp_code_indexer/server/mcp_server.py +79 -12
- {mcp_code_indexer-2.0.2.dist-info → mcp_code_indexer-2.2.0.dist-info}/METADATA +3 -3
- {mcp_code_indexer-2.0.2.dist-info → mcp_code_indexer-2.2.0.dist-info}/RECORD +14 -10
- mcp_code_indexer/database/retry_handler.py +0 -344
- {mcp_code_indexer-2.0.2.dist-info → mcp_code_indexer-2.2.0.dist-info}/WHEEL +0 -0
- {mcp_code_indexer-2.0.2.dist-info → mcp_code_indexer-2.2.0.dist-info}/entry_points.txt +0 -0
- {mcp_code_indexer-2.0.2.dist-info → mcp_code_indexer-2.2.0.dist-info}/licenses/LICENSE +0 -0
- {mcp_code_indexer-2.0.2.dist-info → mcp_code_indexer-2.2.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,465 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
DeepAsk Handler for MCP Code Indexer
|
4
|
+
|
5
|
+
Handles enhanced question-answering with two-stage processing:
|
6
|
+
1. Extract search terms and compress overview
|
7
|
+
2. Search file descriptions and provide enhanced answer
|
8
|
+
"""
|
9
|
+
|
10
|
+
import logging
|
11
|
+
from pathlib import Path
|
12
|
+
from typing import Dict, List, Optional, Any, Tuple
|
13
|
+
|
14
|
+
from .claude_api_handler import ClaudeAPIHandler, ClaudeAPIError
|
15
|
+
from .database.database import DatabaseManager
|
16
|
+
|
17
|
+
|
18
|
+
class DeepAskError(ClaudeAPIError):
|
19
|
+
"""Exception specific to DeepAsk operations."""
|
20
|
+
pass
|
21
|
+
|
22
|
+
|
23
|
+
class DeepAskHandler(ClaudeAPIHandler):
|
24
|
+
"""
|
25
|
+
Handler for enhanced Q&A operations using two-stage Claude API processing.
|
26
|
+
|
27
|
+
Stage 1: Extract search terms and compress project overview
|
28
|
+
Stage 2: Search file descriptions and provide enhanced answer with context
|
29
|
+
"""
|
30
|
+
|
31
|
+
def __init__(self, db_manager: DatabaseManager, cache_dir: Path, logger: Optional[logging.Logger] = None):
|
32
|
+
"""
|
33
|
+
Initialize DeepAskHandler.
|
34
|
+
|
35
|
+
Args:
|
36
|
+
db_manager: Database manager instance
|
37
|
+
cache_dir: Cache directory for temporary files
|
38
|
+
logger: Logger instance to use (optional, creates default if not provided)
|
39
|
+
"""
|
40
|
+
super().__init__(db_manager, cache_dir, logger)
|
41
|
+
self.logger = logger if logger is not None else logging.getLogger(__name__)
|
42
|
+
|
43
|
+
async def find_existing_project_by_name(self, project_name: str) -> Optional[Any]:
|
44
|
+
"""
|
45
|
+
Find existing project by name for CLI usage.
|
46
|
+
|
47
|
+
Args:
|
48
|
+
project_name: Name of the project to find
|
49
|
+
|
50
|
+
Returns:
|
51
|
+
Project object if found, None otherwise
|
52
|
+
"""
|
53
|
+
try:
|
54
|
+
all_projects = await self.db_manager.get_all_projects()
|
55
|
+
normalized_name = project_name.lower()
|
56
|
+
|
57
|
+
for project in all_projects:
|
58
|
+
if project.name.lower() == normalized_name:
|
59
|
+
self.logger.info(f"Found existing project: {project.name} (ID: {project.id})")
|
60
|
+
return project
|
61
|
+
|
62
|
+
self.logger.warning(f"No existing project found with name: {project_name}")
|
63
|
+
return None
|
64
|
+
except Exception as e:
|
65
|
+
self.logger.error(f"Error finding project by name: {e}")
|
66
|
+
return None
|
67
|
+
|
68
|
+
async def deepask_question(
|
69
|
+
self,
|
70
|
+
project_info: Dict[str, str],
|
71
|
+
question: str,
|
72
|
+
max_file_results: int = 10
|
73
|
+
) -> Dict[str, Any]:
|
74
|
+
"""
|
75
|
+
Ask an enhanced question about the project using two-stage Claude API processing.
|
76
|
+
|
77
|
+
Args:
|
78
|
+
project_info: Project information dict with projectName, folderPath, branch, etc.
|
79
|
+
question: User's question about the project
|
80
|
+
max_file_results: Maximum number of file descriptions to include
|
81
|
+
|
82
|
+
Returns:
|
83
|
+
Dict containing enhanced response and metadata
|
84
|
+
"""
|
85
|
+
try:
|
86
|
+
self.logger.info(f"Processing deepask question for project: {project_info['projectName']}")
|
87
|
+
self.logger.info(f"Question: {question}")
|
88
|
+
|
89
|
+
# Validate inputs
|
90
|
+
if not question or not question.strip():
|
91
|
+
raise DeepAskError("Question cannot be empty")
|
92
|
+
|
93
|
+
if not project_info.get("projectName"):
|
94
|
+
raise DeepAskError("Project name is required")
|
95
|
+
|
96
|
+
# Stage 1: Extract search terms and compress overview
|
97
|
+
stage1_result = await self._stage1_extract_search_terms(project_info, question)
|
98
|
+
|
99
|
+
# Stage 2: Search files and provide enhanced answer
|
100
|
+
stage2_result = await self._stage2_enhanced_answer(
|
101
|
+
project_info,
|
102
|
+
question,
|
103
|
+
stage1_result["search_terms"],
|
104
|
+
stage1_result["compressed_overview"],
|
105
|
+
max_file_results
|
106
|
+
)
|
107
|
+
|
108
|
+
# Combine results
|
109
|
+
result = {
|
110
|
+
"answer": stage2_result["answer"],
|
111
|
+
"project_name": project_info["projectName"],
|
112
|
+
"question": question,
|
113
|
+
"search_terms": stage1_result["search_terms"],
|
114
|
+
"compressed_overview": stage1_result["compressed_overview"],
|
115
|
+
"relevant_files": stage2_result["relevant_files"],
|
116
|
+
"metadata": {
|
117
|
+
"model": self.config.model,
|
118
|
+
"stage1_tokens": stage1_result["token_usage"],
|
119
|
+
"stage2_tokens": stage2_result["token_usage"],
|
120
|
+
"total_files_found": stage2_result["total_files_found"],
|
121
|
+
"files_included": len(stage2_result["relevant_files"]),
|
122
|
+
"branch": project_info.get("branch", "unknown")
|
123
|
+
}
|
124
|
+
}
|
125
|
+
|
126
|
+
self.logger.info(f"DeepAsk question completed successfully")
|
127
|
+
self.logger.info(f"Search terms: {stage1_result['search_terms']}")
|
128
|
+
self.logger.info(f"Files found: {stage2_result['total_files_found']}")
|
129
|
+
self.logger.info(f"Files included: {len(stage2_result['relevant_files'])}")
|
130
|
+
|
131
|
+
return result
|
132
|
+
|
133
|
+
except Exception as e:
|
134
|
+
error_msg = f"Failed to process deepask question: {str(e)}"
|
135
|
+
self.logger.error(error_msg)
|
136
|
+
if isinstance(e, (ClaudeAPIError, DeepAskError)):
|
137
|
+
raise
|
138
|
+
else:
|
139
|
+
raise DeepAskError(error_msg)
|
140
|
+
|
141
|
+
async def _stage1_extract_search_terms(
|
142
|
+
self,
|
143
|
+
project_info: Dict[str, str],
|
144
|
+
question: str
|
145
|
+
) -> Dict[str, Any]:
|
146
|
+
"""
|
147
|
+
Stage 1: Extract search terms and compress project overview.
|
148
|
+
|
149
|
+
Args:
|
150
|
+
project_info: Project information
|
151
|
+
question: User's question
|
152
|
+
|
153
|
+
Returns:
|
154
|
+
Dict with search_terms, compressed_overview, and token_usage
|
155
|
+
"""
|
156
|
+
self.logger.info("Stage 1: Extracting search terms and compressing overview")
|
157
|
+
|
158
|
+
# Get project overview
|
159
|
+
overview = await self.get_project_overview(project_info)
|
160
|
+
if not overview:
|
161
|
+
overview = "No project overview available."
|
162
|
+
|
163
|
+
# Build stage 1 prompt
|
164
|
+
prompt = self._build_stage1_prompt(project_info, question, overview)
|
165
|
+
|
166
|
+
# Validate token limits for stage 1
|
167
|
+
if not self.validate_token_limit(prompt):
|
168
|
+
raise DeepAskError(
|
169
|
+
f"Stage 1 prompt exceeds token limit of {self.config.token_limit}. "
|
170
|
+
"Project overview may be too large."
|
171
|
+
)
|
172
|
+
|
173
|
+
# Call Claude API for stage 1
|
174
|
+
system_prompt = self._get_stage1_system_prompt()
|
175
|
+
response = await self._call_claude_api(prompt, system_prompt)
|
176
|
+
|
177
|
+
# Parse and validate response
|
178
|
+
response_data = self.validate_json_response(
|
179
|
+
response.content,
|
180
|
+
required_keys=["search_terms", "compressed_overview"]
|
181
|
+
)
|
182
|
+
|
183
|
+
token_usage = {
|
184
|
+
"prompt_tokens": self.get_token_count(prompt),
|
185
|
+
"response_tokens": response.usage.get("completion_tokens") if response.usage else None,
|
186
|
+
"total_tokens": response.usage.get("total_tokens") if response.usage else None
|
187
|
+
}
|
188
|
+
|
189
|
+
return {
|
190
|
+
"search_terms": response_data["search_terms"],
|
191
|
+
"compressed_overview": response_data["compressed_overview"],
|
192
|
+
"token_usage": token_usage
|
193
|
+
}
|
194
|
+
|
195
|
+
async def _stage2_enhanced_answer(
|
196
|
+
self,
|
197
|
+
project_info: Dict[str, str],
|
198
|
+
question: str,
|
199
|
+
search_terms: List[str],
|
200
|
+
compressed_overview: str,
|
201
|
+
max_file_results: int
|
202
|
+
) -> Dict[str, Any]:
|
203
|
+
"""
|
204
|
+
Stage 2: Search file descriptions and provide enhanced answer.
|
205
|
+
|
206
|
+
Args:
|
207
|
+
project_info: Project information
|
208
|
+
question: User's question
|
209
|
+
search_terms: Search terms from stage 1
|
210
|
+
compressed_overview: Compressed overview from stage 1
|
211
|
+
max_file_results: Maximum number of files to include
|
212
|
+
|
213
|
+
Returns:
|
214
|
+
Dict with answer, relevant_files, total_files_found, and token_usage
|
215
|
+
"""
|
216
|
+
self.logger.info(f"Stage 2: Searching files and generating enhanced answer")
|
217
|
+
self.logger.info(f"Search terms: {search_terms}")
|
218
|
+
|
219
|
+
# Search for relevant files
|
220
|
+
relevant_files = []
|
221
|
+
total_files_found = 0
|
222
|
+
|
223
|
+
try:
|
224
|
+
# Find existing project by name only (don't create new ones for Q&A)
|
225
|
+
project = await self.find_existing_project_by_name(project_info["projectName"])
|
226
|
+
|
227
|
+
if not project:
|
228
|
+
self.logger.warning(f"Project '{project_info['projectName']}' not found in database")
|
229
|
+
return {
|
230
|
+
"answer": f"Project '{project_info['projectName']}' not found in database. Please check the project name.",
|
231
|
+
"relevant_files": [],
|
232
|
+
"total_files_found": 0,
|
233
|
+
"token_usage": {"prompt_tokens": 0, "response_tokens": 0, "total_tokens": 0}
|
234
|
+
}
|
235
|
+
|
236
|
+
for search_term in search_terms:
|
237
|
+
try:
|
238
|
+
search_results = await self.db_manager.search_file_descriptions(
|
239
|
+
project_id=project.id,
|
240
|
+
branch=project_info["branch"],
|
241
|
+
query=search_term,
|
242
|
+
max_results=max_file_results
|
243
|
+
)
|
244
|
+
|
245
|
+
total_files_found += len(search_results)
|
246
|
+
|
247
|
+
# Add unique files to relevant_files
|
248
|
+
for result in search_results:
|
249
|
+
if not any(f["filePath"] == result.file_path for f in relevant_files):
|
250
|
+
relevant_files.append({
|
251
|
+
"filePath": result.file_path,
|
252
|
+
"description": result.description,
|
253
|
+
"search_term": search_term,
|
254
|
+
"relevance_score": result.relevance_score
|
255
|
+
})
|
256
|
+
|
257
|
+
# Stop if we have enough files
|
258
|
+
if len(relevant_files) >= max_file_results:
|
259
|
+
break
|
260
|
+
|
261
|
+
if len(relevant_files) >= max_file_results:
|
262
|
+
break
|
263
|
+
|
264
|
+
except Exception as e:
|
265
|
+
self.logger.warning(f"Search failed for term '{search_term}': {e}")
|
266
|
+
continue
|
267
|
+
|
268
|
+
except Exception as e:
|
269
|
+
self.logger.warning(f"Failed to search files: {e}")
|
270
|
+
# Continue with empty relevant_files list
|
271
|
+
|
272
|
+
# Build stage 2 prompt with file context
|
273
|
+
prompt = self._build_stage2_prompt(
|
274
|
+
project_info,
|
275
|
+
question,
|
276
|
+
compressed_overview,
|
277
|
+
relevant_files
|
278
|
+
)
|
279
|
+
|
280
|
+
# Validate token limits for stage 2
|
281
|
+
if not self.validate_token_limit(prompt):
|
282
|
+
# Try reducing file context
|
283
|
+
self.logger.warning("Stage 2 prompt exceeds token limit, reducing file context")
|
284
|
+
reduced_files = relevant_files[:max_file_results//2]
|
285
|
+
prompt = self._build_stage2_prompt(
|
286
|
+
project_info,
|
287
|
+
question,
|
288
|
+
compressed_overview,
|
289
|
+
reduced_files
|
290
|
+
)
|
291
|
+
|
292
|
+
if not self.validate_token_limit(prompt):
|
293
|
+
raise DeepAskError(
|
294
|
+
f"Stage 2 prompt still exceeds token limit even with reduced context. "
|
295
|
+
"Try a more specific question."
|
296
|
+
)
|
297
|
+
|
298
|
+
relevant_files = reduced_files
|
299
|
+
|
300
|
+
# Call Claude API for stage 2
|
301
|
+
system_prompt = self._get_stage2_system_prompt()
|
302
|
+
response = await self._call_claude_api(prompt, system_prompt)
|
303
|
+
|
304
|
+
token_usage = {
|
305
|
+
"prompt_tokens": self.get_token_count(prompt),
|
306
|
+
"response_tokens": response.usage.get("completion_tokens") if response.usage else None,
|
307
|
+
"total_tokens": response.usage.get("total_tokens") if response.usage else None
|
308
|
+
}
|
309
|
+
|
310
|
+
return {
|
311
|
+
"answer": response.content,
|
312
|
+
"relevant_files": relevant_files,
|
313
|
+
"total_files_found": total_files_found,
|
314
|
+
"token_usage": token_usage
|
315
|
+
}
|
316
|
+
|
317
|
+
def _build_stage1_prompt(
|
318
|
+
self,
|
319
|
+
project_info: Dict[str, str],
|
320
|
+
question: str,
|
321
|
+
overview: str
|
322
|
+
) -> str:
|
323
|
+
"""Build stage 1 prompt for extracting search terms."""
|
324
|
+
project_name = project_info["projectName"]
|
325
|
+
branch = project_info.get("branch", "unknown")
|
326
|
+
|
327
|
+
return f"""I need to answer a question about the codebase "{project_name}" (branch: {branch}). To provide the best answer, I need to search for relevant files and then answer the question.
|
328
|
+
|
329
|
+
PROJECT OVERVIEW:
|
330
|
+
{overview}
|
331
|
+
|
332
|
+
QUESTION:
|
333
|
+
{question}
|
334
|
+
|
335
|
+
Please analyze the question and project overview, then provide:
|
336
|
+
|
337
|
+
1. A list of 3-5 search terms that would help find relevant files to answer this question
|
338
|
+
2. A compressed version of the project overview (2-3 sentences max) that captures the most relevant information for this question
|
339
|
+
|
340
|
+
Respond with valid JSON in this format:
|
341
|
+
{{
|
342
|
+
"search_terms": ["term1", "term2", "term3"],
|
343
|
+
"compressed_overview": "Brief summary focusing on aspects relevant to the question..."
|
344
|
+
}}"""
|
345
|
+
|
346
|
+
def _build_stage2_prompt(
|
347
|
+
self,
|
348
|
+
project_info: Dict[str, str],
|
349
|
+
question: str,
|
350
|
+
compressed_overview: str,
|
351
|
+
relevant_files: List[Dict[str, Any]]
|
352
|
+
) -> str:
|
353
|
+
"""Build stage 2 prompt for enhanced answer."""
|
354
|
+
project_name = project_info["projectName"]
|
355
|
+
branch = project_info.get("branch", "unknown")
|
356
|
+
|
357
|
+
# Format file descriptions
|
358
|
+
file_context = ""
|
359
|
+
if relevant_files:
|
360
|
+
file_context = "\n\nRELEVANT FILES:\n"
|
361
|
+
for i, file_info in enumerate(relevant_files, 1):
|
362
|
+
file_context += f"\n{i}. {file_info['filePath']}\n"
|
363
|
+
file_context += f" Description: {file_info['description']}\n"
|
364
|
+
file_context += f" Found via search: {file_info['search_term']}\n"
|
365
|
+
else:
|
366
|
+
file_context = "\n\nNo relevant files found in the search."
|
367
|
+
|
368
|
+
return f"""Please answer the following question about the codebase "{project_name}" (branch: {branch}).
|
369
|
+
|
370
|
+
PROJECT OVERVIEW (COMPRESSED):
|
371
|
+
{compressed_overview}
|
372
|
+
{file_context}
|
373
|
+
|
374
|
+
QUESTION:
|
375
|
+
{question}
|
376
|
+
|
377
|
+
Please provide a comprehensive answer based on the project overview and relevant file descriptions above. Reference specific files when appropriate and explain how they relate to the question. If the available information is insufficient, clearly state what additional details would be needed."""
|
378
|
+
|
379
|
+
def _get_stage1_system_prompt(self) -> str:
|
380
|
+
"""Get system prompt for stage 1."""
|
381
|
+
return """You are a technical assistant that analyzes software projects to extract relevant search terms and compress information.
|
382
|
+
|
383
|
+
Your task:
|
384
|
+
1. Analyze the user's question about a codebase
|
385
|
+
2. Extract 3-5 search terms that would help find relevant files to answer the question
|
386
|
+
3. Compress the project overview to focus on information relevant to the question
|
387
|
+
|
388
|
+
Search terms should be:
|
389
|
+
- Technical keywords (function names, class names, concepts)
|
390
|
+
- File types or directory names if relevant
|
391
|
+
- Domain-specific terminology from the question
|
392
|
+
|
393
|
+
The compressed overview should:
|
394
|
+
- Be 2-3 sentences maximum
|
395
|
+
- Focus only on aspects relevant to answering the question
|
396
|
+
- Preserve the most important architectural or functional details
|
397
|
+
|
398
|
+
Always respond with valid JSON matching the requested format."""
|
399
|
+
|
400
|
+
def _get_stage2_system_prompt(self) -> str:
|
401
|
+
"""Get system prompt for stage 2."""
|
402
|
+
return """You are a software engineering expert that provides detailed answers about codebases using available context.
|
403
|
+
|
404
|
+
When answering:
|
405
|
+
1. Use the compressed project overview for high-level context
|
406
|
+
2. Reference specific files from the relevant files list when they relate to the question
|
407
|
+
3. Explain how different files work together if relevant
|
408
|
+
4. Be specific and technical when appropriate
|
409
|
+
5. If information is incomplete, clearly state what's missing and suggest next steps
|
410
|
+
6. Provide actionable insights when possible
|
411
|
+
|
412
|
+
Your answer should be comprehensive but focused on the specific question asked."""
|
413
|
+
|
414
|
+
def format_response(self, result: Dict[str, Any], format_type: str = "text") -> str:
|
415
|
+
"""
|
416
|
+
Format response for CLI output.
|
417
|
+
|
418
|
+
Args:
|
419
|
+
result: Result from deepask_question
|
420
|
+
format_type: Output format ("text" or "json")
|
421
|
+
|
422
|
+
Returns:
|
423
|
+
Formatted response string
|
424
|
+
"""
|
425
|
+
if format_type == "json":
|
426
|
+
import json
|
427
|
+
return json.dumps(result, indent=2)
|
428
|
+
|
429
|
+
# Text format
|
430
|
+
answer = result["answer"]
|
431
|
+
metadata = result["metadata"]
|
432
|
+
|
433
|
+
output = []
|
434
|
+
output.append(f"Question: {result['question']}")
|
435
|
+
output.append(f"Project: {result['project_name']} (branch: {metadata['branch']})")
|
436
|
+
output.append("")
|
437
|
+
output.append("Answer:")
|
438
|
+
output.append(answer)
|
439
|
+
output.append("")
|
440
|
+
|
441
|
+
# Show search terms used
|
442
|
+
output.append(f"Search terms: {', '.join(result['search_terms'])}")
|
443
|
+
output.append("")
|
444
|
+
|
445
|
+
# Show relevant files
|
446
|
+
if result["relevant_files"]:
|
447
|
+
output.append("Relevant files analyzed:")
|
448
|
+
for i, file_info in enumerate(result["relevant_files"], 1):
|
449
|
+
output.append(f" {i}. {file_info['filePath']}")
|
450
|
+
else:
|
451
|
+
output.append("No relevant files found.")
|
452
|
+
output.append("")
|
453
|
+
|
454
|
+
# Show metadata
|
455
|
+
output.append("Metadata:")
|
456
|
+
output.append(f" Model: {metadata['model']}")
|
457
|
+
output.append(f" Total files found: {metadata['total_files_found']}")
|
458
|
+
output.append(f" Files included: {metadata['files_included']}")
|
459
|
+
|
460
|
+
stage1_tokens = metadata['stage1_tokens']['total_tokens']
|
461
|
+
stage2_tokens = metadata['stage2_tokens']['total_tokens']
|
462
|
+
if stage1_tokens and stage2_tokens:
|
463
|
+
output.append(f" Total tokens: {stage1_tokens + stage2_tokens} (Stage 1: {stage1_tokens}, Stage 2: {stage2_tokens})")
|
464
|
+
|
465
|
+
return "\n".join(output)
|
@@ -54,7 +54,10 @@ class MCPCodeIndexServer:
|
|
54
54
|
db_retry_count: int = 5,
|
55
55
|
db_timeout: float = 10.0,
|
56
56
|
enable_wal_mode: bool = True,
|
57
|
-
health_check_interval: float = 30.0
|
57
|
+
health_check_interval: float = 30.0,
|
58
|
+
retry_min_wait: float = 0.1,
|
59
|
+
retry_max_wait: float = 2.0,
|
60
|
+
retry_jitter: float = 0.2
|
58
61
|
):
|
59
62
|
"""
|
60
63
|
Initialize the MCP Code Index Server.
|
@@ -68,6 +71,9 @@ class MCPCodeIndexServer:
|
|
68
71
|
db_timeout: Database transaction timeout in seconds
|
69
72
|
enable_wal_mode: Enable WAL mode for better concurrent access
|
70
73
|
health_check_interval: Database health check interval in seconds
|
74
|
+
retry_min_wait: Minimum wait time between retries in seconds
|
75
|
+
retry_max_wait: Maximum wait time between retries in seconds
|
76
|
+
retry_jitter: Maximum jitter to add to retry delays in seconds
|
71
77
|
"""
|
72
78
|
self.token_limit = token_limit
|
73
79
|
self.db_path = db_path or Path.home() / ".mcp-code-index" / "tracker.db"
|
@@ -79,7 +85,10 @@ class MCPCodeIndexServer:
|
|
79
85
|
"retry_count": db_retry_count,
|
80
86
|
"timeout": db_timeout,
|
81
87
|
"enable_wal_mode": enable_wal_mode,
|
82
|
-
"health_check_interval": health_check_interval
|
88
|
+
"health_check_interval": health_check_interval,
|
89
|
+
"retry_min_wait": retry_min_wait,
|
90
|
+
"retry_max_wait": retry_max_wait,
|
91
|
+
"retry_jitter": retry_jitter
|
83
92
|
}
|
84
93
|
|
85
94
|
# Initialize components
|
@@ -89,7 +98,10 @@ class MCPCodeIndexServer:
|
|
89
98
|
retry_count=db_retry_count,
|
90
99
|
timeout=db_timeout,
|
91
100
|
enable_wal_mode=enable_wal_mode,
|
92
|
-
health_check_interval=health_check_interval
|
101
|
+
health_check_interval=health_check_interval,
|
102
|
+
retry_min_wait=retry_min_wait,
|
103
|
+
retry_max_wait=retry_max_wait,
|
104
|
+
retry_jitter=retry_jitter
|
93
105
|
)
|
94
106
|
self.token_counter = TokenCounter(token_limit)
|
95
107
|
self.merge_handler = MergeHandler(self.db_manager)
|
@@ -312,7 +324,7 @@ class MCPCodeIndexServer:
|
|
312
324
|
),
|
313
325
|
types.Tool(
|
314
326
|
name="search_descriptions",
|
315
|
-
description="Searches through all file descriptions in a project to find files related to specific functionality. Use this for large codebases instead of loading the entire structure.",
|
327
|
+
description="Searches through all file descriptions in a project to find files related to specific functionality. Use this for large codebases instead of loading the entire structure. Always start with the fewest terms possible; if the tool returns a lot of results (more than 20) or the results are not relevant, then narrow it down by increasing the number of search terms. Start broad, then narrow the focus only if needed!",
|
316
328
|
inputSchema={
|
317
329
|
"type": "object",
|
318
330
|
"properties": {
|
@@ -1194,21 +1206,76 @@ src/
|
|
1194
1206
|
}
|
1195
1207
|
|
1196
1208
|
async def _handle_check_database_health(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
1197
|
-
"""
|
1198
|
-
|
1199
|
-
|
1209
|
+
"""
|
1210
|
+
Handle check_database_health tool calls with comprehensive diagnostics.
|
1211
|
+
|
1212
|
+
Returns detailed database health information including retry statistics,
|
1213
|
+
performance analysis, and resilience indicators.
|
1214
|
+
"""
|
1215
|
+
# Get comprehensive health diagnostics from the enhanced monitor
|
1216
|
+
if hasattr(self.db_manager, '_health_monitor') and self.db_manager._health_monitor:
|
1217
|
+
comprehensive_diagnostics = self.db_manager._health_monitor.get_comprehensive_diagnostics()
|
1218
|
+
else:
|
1219
|
+
# Fallback to basic health check if monitor not available
|
1220
|
+
health_check = await self.db_manager.check_health()
|
1221
|
+
comprehensive_diagnostics = {
|
1222
|
+
"basic_health_check": health_check,
|
1223
|
+
"note": "Enhanced health monitoring not available"
|
1224
|
+
}
|
1225
|
+
|
1226
|
+
# Get additional database-level statistics
|
1200
1227
|
database_stats = self.db_manager.get_database_stats()
|
1201
1228
|
|
1202
1229
|
return {
|
1203
|
-
"
|
1204
|
-
"
|
1205
|
-
"configuration":
|
1230
|
+
"comprehensive_diagnostics": comprehensive_diagnostics,
|
1231
|
+
"database_statistics": database_stats,
|
1232
|
+
"configuration": {
|
1233
|
+
**self.db_config,
|
1234
|
+
"retry_executor_config": (
|
1235
|
+
self.db_manager._retry_executor.config.__dict__
|
1236
|
+
if hasattr(self.db_manager, '_retry_executor') and self.db_manager._retry_executor
|
1237
|
+
else {}
|
1238
|
+
)
|
1239
|
+
},
|
1206
1240
|
"server_info": {
|
1207
1241
|
"token_limit": self.token_limit,
|
1208
1242
|
"db_path": str(self.db_path),
|
1209
|
-
"cache_dir": str(self.cache_dir)
|
1243
|
+
"cache_dir": str(self.cache_dir),
|
1244
|
+
"health_monitoring_enabled": (
|
1245
|
+
hasattr(self.db_manager, '_health_monitor') and
|
1246
|
+
self.db_manager._health_monitor is not None
|
1247
|
+
)
|
1210
1248
|
},
|
1211
|
-
"timestamp": datetime.utcnow().isoformat()
|
1249
|
+
"timestamp": datetime.utcnow().isoformat(),
|
1250
|
+
"status_summary": self._generate_health_summary(comprehensive_diagnostics)
|
1251
|
+
}
|
1252
|
+
|
1253
|
+
def _generate_health_summary(self, diagnostics: Dict[str, Any]) -> Dict[str, Any]:
|
1254
|
+
"""Generate a concise health summary from comprehensive diagnostics."""
|
1255
|
+
if "resilience_indicators" not in diagnostics:
|
1256
|
+
return {"status": "limited_diagnostics_available"}
|
1257
|
+
|
1258
|
+
resilience = diagnostics["resilience_indicators"]
|
1259
|
+
performance = diagnostics.get("performance_analysis", {})
|
1260
|
+
|
1261
|
+
# Overall status based on health score
|
1262
|
+
health_score = resilience.get("overall_health_score", 0)
|
1263
|
+
if health_score >= 90:
|
1264
|
+
status = "excellent"
|
1265
|
+
elif health_score >= 75:
|
1266
|
+
status = "good"
|
1267
|
+
elif health_score >= 50:
|
1268
|
+
status = "fair"
|
1269
|
+
else:
|
1270
|
+
status = "poor"
|
1271
|
+
|
1272
|
+
return {
|
1273
|
+
"overall_status": status,
|
1274
|
+
"health_score": health_score,
|
1275
|
+
"retry_effectiveness": resilience.get("retry_effectiveness", {}).get("is_effective", False),
|
1276
|
+
"connection_stability": resilience.get("connection_stability", {}).get("is_stable", False),
|
1277
|
+
"key_recommendations": resilience.get("recommendations", [])[:3], # Top 3 recommendations
|
1278
|
+
"performance_trend": performance.get("health_check_performance", {}).get("recent_performance_trend", "unknown")
|
1212
1279
|
}
|
1213
1280
|
|
1214
1281
|
async def _run_session_with_retry(self, read_stream, write_stream, initialization_options) -> None:
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: mcp-code-indexer
|
3
|
-
Version: 2.0
|
3
|
+
Version: 2.2.0
|
4
4
|
Summary: MCP server that tracks file descriptions across codebases, enabling AI agents to efficiently navigate and understand code through searchable summaries and token-aware overviews.
|
5
5
|
Author: MCP Code Indexer Contributors
|
6
6
|
Maintainer: MCP Code Indexer Contributors
|
@@ -59,8 +59,8 @@ Dynamic: requires-python
|
|
59
59
|
|
60
60
|
# MCP Code Indexer 🚀
|
61
61
|
|
62
|
-
[](https://badge.fury.io/py/mcp-code-indexer)
|
63
|
+
[](https://pypi.org/project/mcp-code-indexer/)
|
64
64
|
[](https://opensource.org/licenses/MIT)
|
65
65
|
|
66
66
|
A production-ready **Model Context Protocol (MCP) server** that revolutionizes how AI agents navigate and understand codebases. Built for high-concurrency environments with advanced database resilience, the server provides instant access to intelligent descriptions, semantic search, and context-aware recommendations while maintaining 800+ writes/sec throughput.
|
@@ -1,5 +1,8 @@
|
|
1
1
|
mcp_code_indexer/__init__.py,sha256=GhY2NLQ6lH3n5mxqw0t8T1gmZGKhM6KvjhZH8xW5O-A,1686
|
2
2
|
mcp_code_indexer/__main__.py,sha256=4Edinoe0ug43hobuLYcjTmGp2YJnlFYN4_8iKvUBJ0Q,213
|
3
|
+
mcp_code_indexer/ask_handler.py,sha256=71hwd3FLMBBxplmNMQMki03BFpeCy0sf4HtdTrVe4ks,8598
|
4
|
+
mcp_code_indexer/claude_api_handler.py,sha256=s69aZVQx4SUFXuGvqKVBLYtEsSFHY9_uTCzDT-kpgcE,13660
|
5
|
+
mcp_code_indexer/deepask_handler.py,sha256=bNwZFuiSzU8s7gYM_TB-3QyY7jRy38k752MRrj-s7_s,18634
|
3
6
|
mcp_code_indexer/error_handler.py,sha256=cNSUFFrGBMLDv4qa78c7495L1wSl_dXCRbzCJOidx-Q,11590
|
4
7
|
mcp_code_indexer/file_scanner.py,sha256=ctXeZMROgDThEtjzsANTK9TbK-fhTScMBd4iyuleBT4,11734
|
5
8
|
mcp_code_indexer/git_hook_handler.py,sha256=k6QpoLI-5D9EvrLQrHWMII2qNu21daRX_jXlk9U6bGI,36976
|
@@ -9,19 +12,20 @@ mcp_code_indexer/merge_handler.py,sha256=lJR8eVq2qSrF6MW9mR3Fy8UzrNAaQ7RsI2FMNXn
|
|
9
12
|
mcp_code_indexer/token_counter.py,sha256=WrifOkbF99nWWHlRlhCHAB2KN7qr83GOHl7apE-hJcE,8460
|
10
13
|
mcp_code_indexer/data/stop_words_english.txt,sha256=7Zdd9ameVgA6tN_zuXROvHXD4hkWeELVywPhb7FJEkw,6343
|
11
14
|
mcp_code_indexer/database/__init__.py,sha256=aPq_aaRp0aSwOBIq9GkuMNjmLxA411zg2vhdrAuHm-w,38
|
12
|
-
mcp_code_indexer/database/connection_health.py,sha256=
|
13
|
-
mcp_code_indexer/database/database.py,sha256=
|
15
|
+
mcp_code_indexer/database/connection_health.py,sha256=s2r9L_KipH5NlemAUDnhBQO90Dn4b_0Ht9UDs7F6QPk,24432
|
16
|
+
mcp_code_indexer/database/database.py,sha256=86XL1b49cTeTzkJ1mVbkYPq_QyQrVQOy8w_b1MxZR-E,50856
|
17
|
+
mcp_code_indexer/database/exceptions.py,sha256=AgpRA9Z5R-GoWYdQSPeSdYvAXDopFCQkLGN3jD7Ha4E,10215
|
14
18
|
mcp_code_indexer/database/models.py,sha256=_vCmJnPXZSiInRzyvs4c7QUWuNNW8qsOoDlGX8J-Gnk,7124
|
15
|
-
mcp_code_indexer/database/
|
19
|
+
mcp_code_indexer/database/retry_executor.py,sha256=QUayjkCk8OsckVMYiJ_HBQ9NTUss-H8GQeUIUbbw4_U,13419
|
16
20
|
mcp_code_indexer/middleware/__init__.py,sha256=p-mP0pMsfiU2yajCPvokCUxUEkh_lu4XJP1LyyMW2ug,220
|
17
21
|
mcp_code_indexer/middleware/error_middleware.py,sha256=5agJTAkkPogfPGnja1V9JtG9RG-BiOALIJYctK3byJQ,11730
|
18
22
|
mcp_code_indexer/server/__init__.py,sha256=16xMcuriUOBlawRqWNBk6niwrvtv_JD5xvI36X1Vsmk,41
|
19
|
-
mcp_code_indexer/server/mcp_server.py,sha256=
|
23
|
+
mcp_code_indexer/server/mcp_server.py,sha256=KJAGkhYIR3MVJZECKWL9rpMP3Yb8uO9k7gj5dQ3Wpbc,70436
|
20
24
|
mcp_code_indexer/tiktoken_cache/9b5ad71b2ce5302211f9c61530b329a4922fc6a4,sha256=Ijkht27pm96ZW3_3OFE-7xAPtR0YyTWXoRO8_-hlsqc,1681126
|
21
25
|
mcp_code_indexer/tools/__init__.py,sha256=m01mxML2UdD7y5rih_XNhNSCMzQTz7WQ_T1TeOcYlnE,49
|
22
|
-
mcp_code_indexer-2.0.
|
23
|
-
mcp_code_indexer-2.0.
|
24
|
-
mcp_code_indexer-2.0.
|
25
|
-
mcp_code_indexer-2.0.
|
26
|
-
mcp_code_indexer-2.0.
|
27
|
-
mcp_code_indexer-2.0.
|
26
|
+
mcp_code_indexer-2.2.0.dist-info/licenses/LICENSE,sha256=JN9dyPPgYwH9C-UjYM7FLNZjQ6BF7kAzpF3_4PwY4rY,1086
|
27
|
+
mcp_code_indexer-2.2.0.dist-info/METADATA,sha256=r40SpRrTsPmIGQu2z-2I0tEBzXQUM5U3qqAXnWBPbbE,20165
|
28
|
+
mcp_code_indexer-2.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
29
|
+
mcp_code_indexer-2.2.0.dist-info/entry_points.txt,sha256=8HqWOw1Is7jOP1bvIgaSwouvT9z_Boe-9hd4NzyJOhY,68
|
30
|
+
mcp_code_indexer-2.2.0.dist-info/top_level.txt,sha256=yKYCM-gMGt-cnupGfAhnZaoEsROLB6DQ1KFUuyKx4rw,17
|
31
|
+
mcp_code_indexer-2.2.0.dist-info/RECORD,,
|