mcp-code-indexer 3.1.4__py3-none-any.whl → 3.1.6__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.
@@ -15,7 +15,12 @@ from typing import Dict, List, Optional, Any
15
15
  from pathlib import Path
16
16
 
17
17
  import aiohttp
18
- from tenacity import retry, wait_exponential, stop_after_attempt, retry_if_exception_type
18
+ from tenacity import (
19
+ retry,
20
+ wait_exponential,
21
+ stop_after_attempt,
22
+ retry_if_exception_type,
23
+ )
19
24
 
20
25
  from .database.database import DatabaseManager
21
26
  from .token_counter import TokenCounter
@@ -23,11 +28,13 @@ from .token_counter import TokenCounter
23
28
 
24
29
  class ClaudeAPIError(Exception):
25
30
  """Base exception for Claude API operations."""
31
+
26
32
  pass
27
33
 
28
34
 
29
35
  class ClaudeRateLimitError(ClaudeAPIError):
30
36
  """Exception for rate limiting scenarios."""
37
+
31
38
  def __init__(self, message: str, retry_after: int = 60):
32
39
  super().__init__(message)
33
40
  self.retry_after = retry_after
@@ -35,12 +42,14 @@ class ClaudeRateLimitError(ClaudeAPIError):
35
42
 
36
43
  class ClaudeValidationError(ClaudeAPIError):
37
44
  """Exception for response validation failures."""
45
+
38
46
  pass
39
47
 
40
48
 
41
49
  @dataclass
42
50
  class ClaudeConfig:
43
51
  """Configuration for Claude API calls."""
52
+
44
53
  model: str = "anthropic/claude-sonnet-4"
45
54
  max_tokens: int = 24000
46
55
  temperature: float = 0.3
@@ -51,6 +60,7 @@ class ClaudeConfig:
51
60
  @dataclass
52
61
  class ClaudeResponse:
53
62
  """Structured response from Claude API."""
63
+
54
64
  content: str
55
65
  usage: Optional[Dict[str, Any]] = None
56
66
  model: Optional[str] = None
@@ -59,20 +69,25 @@ class ClaudeResponse:
59
69
  class ClaudeAPIHandler:
60
70
  """
61
71
  Base handler for Claude API interactions via OpenRouter.
62
-
72
+
63
73
  Provides shared functionality for:
64
74
  - Token counting and limit validation
65
75
  - API request/response handling with retry logic
66
76
  - Response validation and parsing
67
77
  - Error handling and logging
68
78
  """
69
-
79
+
70
80
  OPENROUTER_API_URL = "https://openrouter.ai/api/v1/chat/completions"
71
-
72
- def __init__(self, db_manager: DatabaseManager, cache_dir: Path, logger: Optional[logging.Logger] = None):
81
+
82
+ def __init__(
83
+ self,
84
+ db_manager: DatabaseManager,
85
+ cache_dir: Path,
86
+ logger: Optional[logging.Logger] = None,
87
+ ):
73
88
  """
74
89
  Initialize Claude API Handler.
75
-
90
+
76
91
  Args:
77
92
  db_manager: Database manager instance
78
93
  cache_dir: Cache directory for temporary files
@@ -82,64 +97,68 @@ class ClaudeAPIHandler:
82
97
  self.cache_dir = cache_dir
83
98
  self.logger = logger if logger is not None else logging.getLogger(__name__)
84
99
  self.token_counter = TokenCounter()
85
-
100
+
86
101
  # Initialize configuration
87
102
  self.config = ClaudeConfig(
88
103
  model=os.getenv("MCP_CLAUDE_MODEL", "anthropic/claude-sonnet-4"),
89
104
  max_tokens=int(os.getenv("MCP_CLAUDE_MAX_TOKENS", "24000")),
90
105
  temperature=float(os.getenv("MCP_CLAUDE_TEMPERATURE", "0.3")),
91
106
  timeout=int(os.getenv("MCP_CLAUDE_TIMEOUT", "600")), # 10 minutes
92
- token_limit=int(os.getenv("MCP_CLAUDE_TOKEN_LIMIT", "180000"))
107
+ token_limit=int(os.getenv("MCP_CLAUDE_TOKEN_LIMIT", "180000")),
93
108
  )
94
-
109
+
95
110
  # Validate API key
96
111
  self.api_key = os.getenv("OPENROUTER_API_KEY")
97
112
  if not self.api_key:
98
113
  raise ClaudeAPIError("OPENROUTER_API_KEY environment variable is required")
99
-
114
+
100
115
  def validate_token_limit(self, prompt: str, context: str = "") -> bool:
101
116
  """
102
117
  Validate that prompt + context fits within token limit.
103
-
118
+
104
119
  Args:
105
120
  prompt: Main prompt text
106
121
  context: Additional context (project overview, file descriptions, etc.)
107
-
122
+
108
123
  Returns:
109
124
  True if within limits, False otherwise
110
125
  """
111
126
  combined_text = f"{prompt}\n\n{context}"
112
127
  token_count = self.token_counter.count_tokens(combined_text)
113
-
114
- self.logger.debug(f"Token count validation: {token_count}/{self.config.token_limit}")
115
-
128
+
129
+ self.logger.debug(
130
+ f"Token count validation: {token_count}/{self.config.token_limit}"
131
+ )
132
+
116
133
  if token_count > self.config.token_limit:
117
134
  self.logger.warning(
118
135
  f"Token limit exceeded: {token_count} > {self.config.token_limit}. "
119
136
  f"Consider using shorter context or ask for a more specific question."
120
137
  )
121
138
  return False
122
-
139
+
123
140
  return True
124
-
141
+
125
142
  def get_token_count(self, text: str) -> int:
126
143
  """Get token count for given text."""
127
144
  return self.token_counter.count_tokens(text)
128
-
145
+
129
146
  @retry(
130
147
  wait=wait_exponential(multiplier=1, min=1, max=60),
131
148
  stop=stop_after_attempt(5),
132
149
  retry=retry_if_exception_type(ClaudeRateLimitError),
133
- reraise=True
150
+ reraise=True,
134
151
  )
135
- async def _call_claude_api(self, prompt: str, system_prompt: Optional[str] = None) -> ClaudeResponse:
152
+ async def _call_claude_api(
153
+ self, prompt: str, system_prompt: Optional[str] = None
154
+ ) -> ClaudeResponse:
136
155
  """
137
156
  Make API call to Claude via OpenRouter with retry logic.
138
-
157
+
139
158
  Args:
140
159
  prompt: User prompt
141
160
  system_prompt: Optional system prompt
142
-
161
+
143
162
  Returns:
144
163
  ClaudeResponse with parsed response data
145
164
  """
@@ -147,113 +166,126 @@ class ClaudeAPIHandler:
147
166
  "Authorization": f"Bearer {self.api_key}",
148
167
  "HTTP-Referer": "https://github.com/fluffypony/mcp-code-indexer",
149
168
  "X-Title": "MCP Code Indexer",
150
- "Content-Type": "application/json"
169
+ "Content-Type": "application/json",
151
170
  }
152
-
171
+
153
172
  messages = []
154
173
  if system_prompt:
155
174
  messages.append({"role": "system", "content": system_prompt})
156
175
  messages.append({"role": "user", "content": prompt})
157
-
176
+
158
177
  payload = {
159
178
  "model": self.config.model,
160
179
  "messages": messages,
161
180
  "temperature": self.config.temperature,
162
181
  "max_tokens": self.config.max_tokens,
163
182
  }
164
-
183
+
165
184
  timeout = aiohttp.ClientTimeout(total=self.config.timeout)
166
-
167
- self.logger.info(f"Sending request to Claude API via OpenRouter...")
185
+
186
+ self.logger.info("Sending request to Claude API via OpenRouter...")
168
187
  self.logger.info(f" Model: {self.config.model}")
169
188
  self.logger.info(f" Temperature: {self.config.temperature}")
170
189
  self.logger.info(f" Max tokens: {self.config.max_tokens}")
171
190
  self.logger.info(f" Timeout: {self.config.timeout}s")
172
-
191
+
173
192
  try:
174
193
  async with aiohttp.ClientSession(timeout=timeout) as session:
175
194
  async with session.post(
176
- self.OPENROUTER_API_URL,
177
- headers=headers,
178
- json=payload
195
+ self.OPENROUTER_API_URL, headers=headers, json=payload
179
196
  ) as response:
180
-
197
+
181
198
  self.logger.info(f"Claude API response status: {response.status}")
182
-
199
+
183
200
  if response.status == 429:
184
201
  retry_after = int(response.headers.get("Retry-After", 60))
185
- self.logger.warning(f"Rate limited by OpenRouter, retry after {retry_after}s")
186
- raise ClaudeRateLimitError(f"Rate limited. Retry after {retry_after}s", retry_after)
187
-
202
+ self.logger.warning(
203
+ f"Rate limited by OpenRouter, retry after {retry_after}s"
204
+ )
205
+ raise ClaudeRateLimitError(
206
+ f"Rate limited. Retry after {retry_after}s", retry_after
207
+ )
208
+
188
209
  response.raise_for_status()
189
-
210
+
190
211
  response_data = await response.json()
191
-
212
+
192
213
  if "choices" not in response_data:
193
- self.logger.error(f"Invalid API response format: {response_data}")
194
- raise ClaudeAPIError(f"Invalid API response format: {response_data}")
195
-
214
+ self.logger.error(
215
+ f"Invalid API response format: {response_data}"
216
+ )
217
+ raise ClaudeAPIError(
218
+ f"Invalid API response format: {response_data}"
219
+ )
220
+
196
221
  content = response_data["choices"][0]["message"]["content"]
197
222
  usage = response_data.get("usage")
198
223
  model = response_data.get("model")
199
-
200
- self.logger.info(f"Claude response content length: {len(content)} characters")
224
+
225
+ self.logger.info(
226
+ f"Claude response content length: {len(content)} characters"
227
+ )
201
228
  if usage:
202
229
  self.logger.info(f"Token usage: {usage}")
203
-
230
+
204
231
  return ClaudeResponse(content=content, usage=usage, model=model)
205
-
232
+
206
233
  except aiohttp.ClientError as e:
207
234
  self.logger.error(f"Claude API request failed: {e}")
208
235
  raise ClaudeAPIError(f"Claude API request failed: {e}")
209
- except asyncio.TimeoutError as e:
210
- self.logger.error(f"Claude API request timed out after {self.config.timeout}s")
236
+ except asyncio.TimeoutError:
237
+ self.logger.error(
238
+ f"Claude API request timed out after {self.config.timeout}s"
239
+ )
211
240
  raise ClaudeAPIError("Claude API request timed out")
212
-
213
- def validate_json_response(self, response_text: str, required_keys: List[str] = None) -> Dict[str, Any]:
241
+
242
+ def validate_json_response(
243
+ self, response_text: str, required_keys: List[str] = None
244
+ ) -> Dict[str, Any]:
214
245
  """
215
246
  Validate and parse JSON response from Claude.
216
-
247
+
217
248
  Args:
218
249
  response_text: Raw response content
219
250
  required_keys: List of required keys in the JSON response
220
-
251
+
221
252
  Returns:
222
253
  Validated JSON data
223
254
  """
255
+
224
256
  def extract_json_from_response(text: str) -> str:
225
257
  """Extract JSON from response that might have extra text before/after."""
226
258
  text = text.strip()
227
-
259
+
228
260
  # Try to find JSON in the response
229
261
  json_start = -1
230
262
  json_end = -1
231
-
263
+
232
264
  # Look for opening brace
233
265
  for i, char in enumerate(text):
234
- if char == '{':
266
+ if char == "{":
235
267
  json_start = i
236
268
  break
237
-
269
+
238
270
  if json_start == -1:
239
271
  return text # No JSON found, return original
240
-
272
+
241
273
  # Find matching closing brace
242
274
  brace_count = 0
243
275
  for i in range(json_start, len(text)):
244
- if text[i] == '{':
276
+ if text[i] == "{":
245
277
  brace_count += 1
246
- elif text[i] == '}':
278
+ elif text[i] == "}":
247
279
  brace_count -= 1
248
280
  if brace_count == 0:
249
281
  json_end = i + 1
250
282
  break
251
-
283
+
252
284
  if json_end == -1:
253
285
  return text # No matching brace found, return original
254
-
286
+
255
287
  return text[json_start:json_end]
256
-
288
+
257
289
  try:
258
290
  # First try parsing as-is
259
291
  try:
@@ -264,15 +296,17 @@ class ClaudeAPIHandler:
264
296
  if extracted_json != response_text.strip():
265
297
  self.logger.debug(f"Extracted JSON from response: {extracted_json}")
266
298
  data = json.loads(extracted_json)
267
-
299
+
268
300
  # Validate required keys if specified
269
301
  if required_keys:
270
302
  missing_keys = [key for key in required_keys if key not in data]
271
303
  if missing_keys:
272
- raise ClaudeValidationError(f"Missing required keys in response: {missing_keys}")
273
-
304
+ raise ClaudeValidationError(
305
+ f"Missing required keys in response: {missing_keys}"
306
+ )
307
+
274
308
  return data
275
-
309
+
276
310
  except json.JSONDecodeError as e:
277
311
  self.logger.error(f"Failed to parse JSON response: {e}")
278
312
  self.logger.error(f"Response text: {response_text}")
@@ -280,46 +314,51 @@ class ClaudeAPIHandler:
280
314
  except Exception as e:
281
315
  self.logger.error(f"Response validation failed: {e}")
282
316
  raise ClaudeValidationError(f"Response validation failed: {e}")
283
-
317
+
284
318
  def format_error_response(self, error: Exception, context: str = "") -> str:
285
319
  """
286
320
  Format error for user-friendly display.
287
-
321
+
288
322
  Args:
289
323
  error: The exception that occurred
290
324
  context: Additional context about the operation
291
-
325
+
292
326
  Returns:
293
327
  Formatted error message
294
328
  """
295
329
  if isinstance(error, ClaudeRateLimitError):
296
- return f"Rate limited by Claude API. Please wait {error.retry_after} seconds and try again."
330
+ return (
331
+ f"Rate limited by Claude API. Please wait {error.retry_after} "
332
+ "seconds and try again."
333
+ )
297
334
  elif isinstance(error, ClaudeValidationError):
298
335
  return f"Invalid response from Claude API: {str(error)}"
299
336
  elif isinstance(error, ClaudeAPIError):
300
337
  return f"Claude API error: {str(error)}"
301
338
  else:
302
339
  return f"Unexpected error during {context}: {str(error)}"
303
-
340
+
304
341
  async def find_existing_project_by_name(self, project_name: str) -> Optional[Any]:
305
342
  """
306
343
  Find existing project by name for CLI usage.
307
-
344
+
308
345
  Args:
309
346
  project_name: Name of the project to find
310
-
347
+
311
348
  Returns:
312
349
  Project object if found, None otherwise
313
350
  """
314
351
  try:
315
352
  all_projects = await self.db_manager.get_all_projects()
316
353
  normalized_name = project_name.lower()
317
-
354
+
318
355
  for project in all_projects:
319
356
  if project.name.lower() == normalized_name:
320
- self.logger.info(f"Found existing project: {project.name} (ID: {project.id})")
357
+ self.logger.info(
358
+ f"Found existing project: {project.name} (ID: {project.id})"
359
+ )
321
360
  return project
322
-
361
+
323
362
  self.logger.warning(f"No existing project found with name: {project_name}")
324
363
  return None
325
364
  except Exception as e:
@@ -329,21 +368,25 @@ class ClaudeAPIHandler:
329
368
  async def get_project_overview(self, project_info: Dict[str, str]) -> str:
330
369
  """
331
370
  Get project overview from database.
332
-
371
+
333
372
  Args:
334
373
  project_info: Project information dict with projectName, folderPath, etc.
335
-
374
+
336
375
  Returns:
337
376
  Project overview text or empty string if not found
338
377
  """
339
378
  try:
340
379
  # Try to find existing project by name first
341
- project = await self.find_existing_project_by_name(project_info["projectName"])
342
-
380
+ project = await self.find_existing_project_by_name(
381
+ project_info["projectName"]
382
+ )
383
+
343
384
  if not project:
344
- self.logger.warning(f"Project '{project_info['projectName']}' not found in database")
385
+ self.logger.warning(
386
+ f"Project '{project_info['projectName']}' not found in database"
387
+ )
345
388
  return ""
346
-
389
+
347
390
  # Get overview for the project using project.id
348
391
  overview_result = await self.db_manager.get_project_overview(project.id)
349
392
  if overview_result: