mcpower-proxy 0.0.58__py3-none-any.whl → 0.0.73__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.
Files changed (36) hide show
  1. ide_tools/__init__.py +12 -0
  2. ide_tools/common/__init__.py +6 -0
  3. ide_tools/common/hooks/__init__.py +6 -0
  4. ide_tools/common/hooks/init.py +125 -0
  5. ide_tools/common/hooks/output.py +64 -0
  6. ide_tools/common/hooks/prompt_submit.py +186 -0
  7. ide_tools/common/hooks/read_file.py +170 -0
  8. ide_tools/common/hooks/shell_execution.py +196 -0
  9. ide_tools/common/hooks/types.py +35 -0
  10. ide_tools/common/hooks/utils.py +276 -0
  11. ide_tools/cursor/__init__.py +11 -0
  12. ide_tools/cursor/constants.py +58 -0
  13. ide_tools/cursor/format.py +35 -0
  14. ide_tools/cursor/router.py +100 -0
  15. ide_tools/router.py +48 -0
  16. main.py +11 -4
  17. {mcpower_proxy-0.0.58.dist-info → mcpower_proxy-0.0.73.dist-info}/METADATA +15 -3
  18. mcpower_proxy-0.0.73.dist-info/RECORD +59 -0
  19. {mcpower_proxy-0.0.58.dist-info → mcpower_proxy-0.0.73.dist-info}/top_level.txt +1 -0
  20. modules/apis/security_policy.py +11 -6
  21. modules/decision_handler.py +219 -0
  22. modules/logs/audit_trail.py +22 -17
  23. modules/logs/logger.py +14 -18
  24. modules/redaction/redactor.py +112 -107
  25. modules/ui/__init__.py +1 -1
  26. modules/ui/confirmation.py +0 -1
  27. modules/utils/cli.py +36 -6
  28. modules/utils/ids.py +55 -10
  29. modules/utils/json.py +3 -3
  30. wrapper/__version__.py +1 -1
  31. wrapper/middleware.py +121 -210
  32. wrapper/server.py +19 -11
  33. mcpower_proxy-0.0.58.dist-info/RECORD +0 -43
  34. {mcpower_proxy-0.0.58.dist-info → mcpower_proxy-0.0.73.dist-info}/WHEEL +0 -0
  35. {mcpower_proxy-0.0.58.dist-info → mcpower_proxy-0.0.73.dist-info}/entry_points.txt +0 -0
  36. {mcpower_proxy-0.0.58.dist-info → mcpower_proxy-0.0.73.dist-info}/licenses/LICENSE +0 -0
@@ -20,7 +20,6 @@ from .constants import (
20
20
  )
21
21
  from .pii_rules import detect_pii
22
22
 
23
-
24
23
  # Type alias for JSONPath cache values: (matches_set, prefix_tree)
25
24
  JSONPathCacheValue = Tuple[Set[str], Dict[str, Any]]
26
25
 
@@ -30,11 +29,11 @@ class LRUCache:
30
29
  Least Recently Used cache with size limit to prevent unbounded memory growth.
31
30
  Thread-safe for single-threaded usage (not thread-safe for concurrent access).
32
31
  """
33
-
32
+
34
33
  def __init__(self, max_size: int = 128):
35
34
  self.cache = OrderedDict()
36
35
  self.max_size = max_size
37
-
36
+
38
37
  def get(self, key) -> Optional[JSONPathCacheValue]:
39
38
  """Get value and mark as recently used."""
40
39
  if key in self.cache:
@@ -42,7 +41,7 @@ class LRUCache:
42
41
  self.cache.move_to_end(key)
43
42
  return self.cache[key]
44
43
  return None
45
-
44
+
46
45
  def set(self, key, value: JSONPathCacheValue) -> None:
47
46
  """Set value and evict oldest if necessary."""
48
47
  if key in self.cache:
@@ -55,11 +54,11 @@ class LRUCache:
55
54
  # Evict oldest if over limit
56
55
  if len(self.cache) > self.max_size:
57
56
  self.cache.popitem(last=False) # Remove oldest (first) item
58
-
57
+
59
58
  def clear(self) -> None:
60
59
  """Clear all cached entries."""
61
60
  self.cache.clear()
62
-
61
+
63
62
  def size(self) -> int:
64
63
  """Get current cache size."""
65
64
  return len(self.cache)
@@ -73,7 +72,7 @@ class RedactionSpan:
73
72
  end: int
74
73
  replacement: str
75
74
  source: str # 'secrets' or 'pii'
76
-
75
+
77
76
  @property
78
77
  def length(self) -> int:
79
78
  return self.end - self.start
@@ -81,20 +80,20 @@ class RedactionSpan:
81
80
 
82
81
  class RedactionEngine:
83
82
  """Core redaction engine using PII detection and Gitleaks."""
84
-
83
+
85
84
  # Performance optimization: Simple bounded cache for compiled JSONPath expressions
86
85
  _jsonpath_expr_cache = {}
87
86
  _MAX_EXPR_CACHE = 64
88
-
87
+
89
88
  # Performance optimization: LRU cache for JSONPath matches to prevent memory leaks
90
89
  _jsonpath_cache = LRUCache(max_size=128)
91
-
90
+
92
91
  # Performance optimization: Pre-compiled regex patterns
93
92
  _compiled_regexes = {
94
93
  'placeholder': re.compile(REDACTION_PLACEHOLDER_PATTERN),
95
94
  'path_brackets': re.compile(r'\.\[(\d+)\]')
96
95
  }
97
-
96
+
98
97
  @staticmethod
99
98
  def redact(value: Any, ignored_keys: List[str] = None, include_keys: List[str] = None) -> Any:
100
99
  """
@@ -113,55 +112,56 @@ class RedactionEngine:
113
112
  """
114
113
  if ignored_keys and include_keys:
115
114
  raise ValueError("Cannot specify both ignored_keys and include_keys - use one or the other")
116
-
115
+
117
116
  return RedactionEngine._redact_with_path(value, ignored_keys or [], include_keys or [], "", value)
118
-
117
+
119
118
  @staticmethod
120
- def _redact_with_path(value: Any, ignored_keys: List[str], include_keys: List[str], current_path: str, root_data: Any) -> Any:
119
+ def _redact_with_path(value: Any, ignored_keys: List[str], include_keys: List[str], current_path: str,
120
+ root_data: Any) -> Any:
121
121
  """
122
122
  Internal redaction function that tracks the current path.
123
123
  """
124
124
  # Performance optimization: fastest type checks first
125
125
  if value is None:
126
126
  return None
127
-
127
+
128
128
  if isinstance(value, bool):
129
129
  return value # Booleans can never contain sensitive data - fastest check
130
-
130
+
131
131
  if isinstance(value, (int, float)):
132
132
  # Preserve type if no redaction needed
133
133
  str_value = str(value)
134
134
  redacted_str = RedactionEngine._redact_string(str_value)
135
135
  return value if str_value == redacted_str else redacted_str
136
-
136
+
137
137
  if isinstance(value, str):
138
138
  return RedactionEngine._redact_string(value)
139
-
139
+
140
140
  if isinstance(value, dict):
141
141
  return RedactionEngine._redact_dict(value, ignored_keys, include_keys, current_path, root_data)
142
-
142
+
143
143
  if isinstance(value, list):
144
144
  return RedactionEngine._redact_list(value, ignored_keys, include_keys, current_path, root_data)
145
-
145
+
146
146
  # For other types, convert to string and redact
147
147
  return RedactionEngine._redact_string(str(value))
148
-
148
+
149
149
  @staticmethod
150
150
  def _normalize_text(text: str) -> str:
151
151
  """Strip zero-width characters efficiently."""
152
152
  # Fast path - check if normalization is needed
153
153
  if not any(char in text for char in ZERO_WIDTH_CHARS):
154
154
  return text
155
-
155
+
156
156
  # Single pass with translation table (much faster)
157
157
  translation_table = str.maketrans('', '', ''.join(ZERO_WIDTH_CHARS))
158
158
  return text.translate(translation_table)
159
-
159
+
160
160
  @staticmethod
161
161
  def _detect_secrets(text: str) -> List[RedactionSpan]:
162
162
  """Detect secrets using Gitleaks-based patterns."""
163
163
  spans = []
164
-
164
+
165
165
  # Use compiled rules (based on Gitleaks patterns) for secrets detection
166
166
  from . import gitleaks_rules as GL
167
167
  # Scan line-by-line for performance and to reduce pathological matches
@@ -181,7 +181,7 @@ class RedactionEngine:
181
181
  s, e = match.span(0)
182
182
  except (IndexError, AttributeError):
183
183
  s, e = match.span(0)
184
-
184
+
185
185
  if s < e:
186
186
  spans.append(RedactionSpan(
187
187
  start=offset + s,
@@ -190,47 +190,47 @@ class RedactionEngine:
190
190
  source='secrets'
191
191
  ))
192
192
  offset += len(line) + 1 # +1 for the split '\n'
193
-
193
+
194
194
  return spans
195
-
195
+
196
196
  @staticmethod
197
197
  def _detect_pii(text: str) -> List[RedactionSpan]:
198
198
  """Detect PII using regex patterns."""
199
199
  spans = []
200
-
200
+
201
201
  # Use PII detection
202
202
  results = detect_pii(text)
203
-
203
+
204
204
  # Convert results to redaction spans
205
205
  for result in results:
206
206
  placeholder = PII_PLACEHOLDERS.get(
207
- result.entity_type,
207
+ result.entity_type,
208
208
  PII_PLACEHOLDERS["DEFAULT"]
209
209
  )
210
-
210
+
211
211
  spans.append(RedactionSpan(
212
212
  start=result.start,
213
213
  end=result.end,
214
214
  replacement=placeholder,
215
215
  source='pii'
216
216
  ))
217
-
217
+
218
218
  return spans
219
-
219
+
220
220
  @staticmethod
221
221
  def _resolve_overlaps(spans: List[RedactionSpan]) -> List[RedactionSpan]:
222
222
  """Resolve overlapping spans - longest span wins."""
223
223
  if not spans:
224
224
  return []
225
-
225
+
226
226
  # Sort by start position
227
227
  sorted_spans = sorted(spans, key=lambda s: s.start)
228
228
  resolved = []
229
-
229
+
230
230
  for current_span in sorted_spans:
231
231
  # Check for overlaps with already resolved spans
232
232
  overlaps = False
233
-
233
+
234
234
  for i, existing_span in enumerate(resolved):
235
235
  if RedactionEngine._spans_overlap(current_span, existing_span):
236
236
  overlaps = True
@@ -238,121 +238,125 @@ class RedactionEngine:
238
238
  if current_span.length > existing_span.length:
239
239
  resolved[i] = current_span
240
240
  break
241
-
241
+
242
242
  if not overlaps:
243
243
  resolved.append(current_span)
244
-
244
+
245
245
  return resolved
246
-
246
+
247
247
  @staticmethod
248
248
  def _spans_overlap(span1: RedactionSpan, span2: RedactionSpan) -> bool:
249
249
  """Check if two spans overlap."""
250
250
  return not (span1.end <= span2.start or span2.end <= span1.start)
251
-
251
+
252
252
  @staticmethod
253
253
  def _apply_idempotency_guard(text: str, spans: List[RedactionSpan]) -> List[RedactionSpan]:
254
254
  """Remove spans that would redact inside existing placeholders."""
255
255
  if not spans:
256
256
  return []
257
-
257
+
258
258
  # Find existing redaction placeholders
259
259
  placeholder_spans = []
260
260
  # Performance optimization: Use pre-compiled regex
261
261
  for match in RedactionEngine._compiled_regexes['placeholder'].finditer(text):
262
262
  placeholder_spans.append((match.start(), match.end()))
263
-
263
+
264
264
  # Filter out spans that overlap with existing placeholders
265
265
  filtered_spans = []
266
266
  for span in spans:
267
267
  overlaps_placeholder = False
268
-
268
+
269
269
  for ph_start, ph_end in placeholder_spans:
270
270
  if not (span.end <= ph_start or span.start >= ph_end):
271
271
  overlaps_placeholder = True
272
272
  break
273
-
273
+
274
274
  if not overlaps_placeholder:
275
275
  filtered_spans.append(span)
276
-
276
+
277
277
  return filtered_spans
278
-
278
+
279
279
  @staticmethod
280
280
  def _apply_redactions(text: str, spans: List[RedactionSpan]) -> str:
281
281
  """Apply redactions right-to-left to avoid index shifting, preserving JSON structure."""
282
282
  if not spans:
283
283
  return text
284
-
284
+
285
285
  # Sort spans by start position (descending) for right-to-left processing
286
286
  # Processing right-to-left ensures earlier spans' positions remain valid
287
287
  # since replacements to the right don't affect positions to the left
288
288
  sorted_spans = sorted(spans, key=lambda s: s.start, reverse=True)
289
-
289
+
290
290
  result = text
291
-
291
+
292
292
  for span in sorted_spans:
293
293
  # Replacements to the right don't shift positions to the left
294
294
  start_pos = span.start
295
295
  end_pos = span.end
296
-
296
+
297
297
  # Bounds check
298
298
  if start_pos < 0 or end_pos > len(result) or start_pos >= len(result):
299
299
  continue
300
-
300
+
301
301
  replacement = span.replacement
302
-
302
+
303
303
  # Simply replace the matched text with the redaction placeholder
304
304
  # Do NOT expand to surrounding quotes - this breaks nested JSON
305
305
  # The redaction placeholders are designed to be valid string content as-is
306
-
306
+
307
307
  # Apply the redaction
308
308
  result = result[:start_pos] + replacement + result[end_pos:]
309
-
309
+
310
310
  return result
311
311
 
312
312
  @staticmethod
313
- def _redact_dict(d: dict, ignored_keys: List[str], include_keys: List[str], current_path: str, root_data: Any) -> dict:
313
+ def _redact_dict(d: dict, ignored_keys: List[str], include_keys: List[str], current_path: str,
314
+ root_data: Any) -> dict:
314
315
  """Redact dictionary values, respecting ignored_keys or include_keys."""
315
316
  result = {}
316
-
317
+
317
318
  for key, value in d.items():
318
319
  # Build the path for this key
319
320
  key_path = RedactionEngine._build_path(current_path, str(key))
320
-
321
+
321
322
  # Determine if this path should be redacted
322
- if RedactionEngine._should_redact_path_enhanced(root_data, key_path, ignored_keys, include_keys, current_path):
323
+ if RedactionEngine._should_redact_path_enhanced(root_data, key_path, ignored_keys, include_keys,
324
+ current_path):
323
325
  # Recursively redact the value with updated path context
324
326
  result[key] = RedactionEngine._redact_with_path(value, ignored_keys, include_keys, key_path, root_data)
325
327
  else:
326
328
  # Keep the value as-is (no redaction for this path or any nested paths)
327
329
  result[key] = value
328
-
330
+
329
331
  return result
330
-
332
+
331
333
  @staticmethod
332
- def _redact_list(lst: list, ignored_keys: List[str], include_keys: List[str], current_path: str, root_data: Any) -> list:
334
+ def _redact_list(lst: list, ignored_keys: List[str], include_keys: List[str], current_path: str,
335
+ root_data: Any) -> list:
333
336
  """Redact list items, respecting ignored_keys or include_keys."""
334
337
  result = []
335
-
338
+
336
339
  for i, item in enumerate(lst):
337
340
  # Build the path for this index
338
341
  item_path = RedactionEngine._build_path(current_path, str(i))
339
-
342
+
340
343
  # Determine if this path should be redacted
341
- if RedactionEngine._should_redact_path_enhanced(root_data, item_path, ignored_keys, include_keys, current_path):
344
+ if RedactionEngine._should_redact_path_enhanced(root_data, item_path, ignored_keys, include_keys,
345
+ current_path):
342
346
  # Recursively redact the item with updated path context
343
347
  result.append(RedactionEngine._redact_with_path(item, ignored_keys, include_keys, item_path, root_data))
344
348
  else:
345
349
  # Keep the item as-is (no redaction for this path or any nested paths)
346
350
  result.append(item)
347
-
351
+
348
352
  return result
349
-
353
+
350
354
  @staticmethod
351
355
  def _redact_string(text: str) -> str:
352
356
  """Redact a string using the existing redaction pipeline."""
353
357
  if not text or not isinstance(text, str):
354
358
  return text
355
-
359
+
356
360
  normalized_text = RedactionEngine._normalize_text(text)
357
361
  redaction_spans: List[RedactionSpan] = []
358
362
  redaction_spans.extend(RedactionEngine._detect_secrets(normalized_text))
@@ -360,7 +364,7 @@ class RedactionEngine:
360
364
  resolved_spans = RedactionEngine._resolve_overlaps(redaction_spans)
361
365
  final_spans = RedactionEngine._apply_idempotency_guard(normalized_text, resolved_spans)
362
366
  return RedactionEngine._apply_redactions(normalized_text, final_spans)
363
-
367
+
364
368
  @staticmethod
365
369
  def _normalize_numeric_key(key: str) -> str:
366
370
  """Normalize numeric keys efficiently - only if needed."""
@@ -368,7 +372,7 @@ class RedactionEngine:
368
372
  # Only normalize if there are leading zeros
369
373
  return str(int(key))
370
374
  return key
371
-
375
+
372
376
  @staticmethod
373
377
  def _build_path(current_path: str, key: str) -> str:
374
378
  """
@@ -378,19 +382,19 @@ class RedactionEngine:
378
382
  """
379
383
  # Normalize numeric keys efficiently
380
384
  normalized_key = RedactionEngine._normalize_numeric_key(key)
381
-
385
+
382
386
  # Handle empty keys to prevent paths like "user..email"
383
387
  if not normalized_key:
384
388
  return current_path
385
-
389
+
386
390
  if not current_path:
387
391
  return normalized_key
388
-
392
+
389
393
  return f"{current_path}.{normalized_key}"
390
-
391
-
394
+
392
395
  @staticmethod
393
- def _should_redact_path_enhanced(root_data: dict, path: str, ignored_keys: List[str], include_keys: List[str], current_path: str) -> bool:
396
+ def _should_redact_path_enhanced(root_data: dict, path: str, ignored_keys: List[str], include_keys: List[str],
397
+ current_path: str) -> bool:
394
398
  """
395
399
  Enhanced path matching using JSONPath when available, fallback to custom logic.
396
400
 
@@ -405,9 +409,10 @@ class RedactionEngine:
405
409
  True if the path should be redacted, False otherwise
406
410
  """
407
411
  return RedactionEngine._should_redact_path_jsonpath(root_data, path, ignored_keys, include_keys)
408
-
412
+
409
413
  @staticmethod
410
- def _should_redact_path_jsonpath(root_data: dict, path: str, ignored_keys: List[str], include_keys: List[str]) -> bool:
414
+ def _should_redact_path_jsonpath(root_data: dict, path: str, ignored_keys: List[str],
415
+ include_keys: List[str]) -> bool:
411
416
  """
412
417
  JSONPath-based path matching with LRU cache to prevent memory leaks.
413
418
  """
@@ -417,21 +422,21 @@ class RedactionEngine:
417
422
  frozenset(ignored_keys) if ignored_keys else None,
418
423
  frozenset(include_keys) if include_keys else None
419
424
  )
420
-
425
+
421
426
  # Try to get from LRU cache
422
427
  cached_result = RedactionEngine._jsonpath_cache.get(cache_key)
423
-
428
+
424
429
  if cached_result is None:
425
430
  # Pre-compute all matches for this data/pattern combination
426
431
  all_matches = set()
427
432
  patterns = include_keys or ignored_keys or []
428
-
433
+
429
434
  for pattern in patterns:
430
435
  try:
431
436
  # Expect proper JSONPath format (starting with $)
432
437
  if not pattern.startswith('$'):
433
438
  raise ValueError(f"Pattern must be in JSONPath format (start with $): {pattern}")
434
-
439
+
435
440
  # Performance optimization: Use cached compiled expressions
436
441
  if pattern not in RedactionEngine._jsonpath_expr_cache:
437
442
  # Simple FIFO eviction if cache is full
@@ -441,29 +446,29 @@ class RedactionEngine:
441
446
  del RedactionEngine._jsonpath_expr_cache[oldest_key]
442
447
  RedactionEngine._jsonpath_expr_cache[pattern] = jsonpath_parse(pattern)
443
448
  jsonpath_expr = RedactionEngine._jsonpath_expr_cache[pattern]
444
-
449
+
445
450
  matches = jsonpath_expr.find(root_data)
446
-
451
+
447
452
  for match in matches:
448
453
  # Performance optimization: Single regex for path conversion
449
454
  match_path = RedactionEngine._compiled_regexes['path_brackets'].sub(
450
455
  r'.\1', str(match.full_path)
451
456
  )
452
457
  all_matches.add(match_path)
453
-
458
+
454
459
  except Exception:
455
460
  # Fallback for invalid JSONPath patterns
456
461
  continue
457
-
462
+
458
463
  # Build prefix tree for O(k) child matching
459
464
  prefix_tree = RedactionEngine._build_prefix_tree(all_matches)
460
465
  cached_result = (all_matches, prefix_tree)
461
-
466
+
462
467
  # Store in LRU cache (will evict oldest if necessary)
463
468
  RedactionEngine._jsonpath_cache.set(cache_key, cached_result)
464
-
469
+
465
470
  matches, prefix_tree = cached_result
466
-
471
+
467
472
  if include_keys:
468
473
  # include_keys mode: only redact if path matches or has matching children
469
474
  return RedactionEngine._path_matches_or_has_children(path, prefix_tree)
@@ -473,7 +478,7 @@ class RedactionEngine:
473
478
  else:
474
479
  # No filtering: redact everything
475
480
  return True
476
-
481
+
477
482
  @staticmethod
478
483
  def _build_prefix_tree(matches: Set[str]) -> Dict[str, Any]:
479
484
  """
@@ -481,23 +486,23 @@ class RedactionEngine:
481
486
  Stores both exact matches and children for single-traversal lookup.
482
487
  """
483
488
  prefix_tree = {}
484
-
489
+
485
490
  for match in matches:
486
491
  parts = match.split('.')
487
492
  node = prefix_tree
488
-
493
+
489
494
  # Build tree path
490
495
  for i, part in enumerate(parts):
491
496
  if part not in node:
492
497
  node[part] = {'_children': {}, '_is_match': False}
493
-
498
+
494
499
  if i == len(parts) - 1:
495
500
  node[part]['_is_match'] = True # Mark exact matches
496
-
501
+
497
502
  node = node[part]['_children']
498
-
503
+
499
504
  return prefix_tree
500
-
505
+
501
506
  @staticmethod
502
507
  def _has_matching_children_optimized(path: str, prefix_tree: Dict[str, Any]) -> bool:
503
508
  """
@@ -507,20 +512,20 @@ class RedactionEngine:
507
512
  if not path:
508
513
  # Root path - check if tree has any entries
509
514
  return bool(prefix_tree)
510
-
515
+
511
516
  # Navigate to the node representing this path
512
517
  parts = path.split('.')
513
518
  node = prefix_tree
514
-
519
+
515
520
  for part in parts:
516
521
  if part not in node:
517
522
  # Path doesn't exist in tree
518
523
  return False
519
524
  node = node[part]['_children']
520
-
525
+
521
526
  # Check if there are any children under this path
522
527
  return bool(node)
523
-
528
+
524
529
  @staticmethod
525
530
  def _path_matches_or_has_children(path: str, prefix_tree: Dict[str, Any]) -> bool:
526
531
  """
@@ -529,22 +534,22 @@ class RedactionEngine:
529
534
  """
530
535
  if not path:
531
536
  return bool(prefix_tree)
532
-
537
+
533
538
  parts = path.split('.')
534
539
  node = prefix_tree
535
-
540
+
536
541
  for i, part in enumerate(parts):
537
542
  if part not in node:
538
543
  return False
539
-
544
+
540
545
  if i == len(parts) - 1:
541
546
  # Check if this exact path matches OR has children
542
547
  return node[part].get('_is_match', False) or bool(node[part]['_children'])
543
-
548
+
544
549
  node = node[part]['_children']
545
-
550
+
546
551
  return False
547
-
552
+
548
553
  @staticmethod
549
554
  def _has_matching_children_cached(path: str, matches: Set[str]) -> bool:
550
555
  """
@@ -553,7 +558,7 @@ class RedactionEngine:
553
558
  """
554
559
  path_prefix = path + "." if path else ""
555
560
  return any(match.startswith(path_prefix) for match in matches)
556
-
561
+
557
562
  @classmethod
558
563
  def clear_caches(cls) -> None:
559
564
  """
@@ -561,7 +566,7 @@ class RedactionEngine:
561
566
  """
562
567
  cls._jsonpath_expr_cache.clear()
563
568
  cls._jsonpath_cache.clear()
564
-
569
+
565
570
  @classmethod
566
571
  def get_cache_stats(cls) -> Dict[str, int]:
567
572
  """
modules/ui/__init__.py CHANGED
@@ -1 +1 @@
1
- # UI modules for user interaction
1
+ # UI modules for user interaction
@@ -9,7 +9,6 @@ from datetime import datetime, timezone
9
9
  from typing import Optional
10
10
 
11
11
  from mcpower_shared.mcp_types import UserDecision
12
-
13
12
  from modules.logs.audit_trail import AuditTrailLogger
14
13
  from modules.logs.logger import MCPLogger
15
14
  from . import xdialog
modules/utils/cli.py CHANGED
@@ -7,7 +7,7 @@ import argparse
7
7
  def parse_args():
8
8
  """Parse command line arguments"""
9
9
  parser = argparse.ArgumentParser(
10
- description="MCPower - Transparent 1:1 MCP Wrapper with security enforcement",
10
+ description="Transparent MCP wrapper with security middleware for real-time policy enforcement and monitoring.",
11
11
  formatter_class=argparse.RawDescriptionHelpFormatter,
12
12
  epilog="""
13
13
  Examples:
@@ -22,18 +22,34 @@ Examples:
22
22
 
23
23
  # With custom name
24
24
  %(prog)s --wrapped-config '{"command": "node", "args": ["server.js"]}' --name MyWrapper
25
+
26
+ # IDE Tools mode
27
+ %(prog)s --ide-tool --ide cursor --context beforeShellExecution
25
28
 
26
29
  Reference Links:
27
- FastMCP Proxy: https://gofastmcp.com/servers/proxy
28
- • FastMCP Middleware: https://gofastmcp.com/servers/middleware
30
+ MCPower Proxy: https://github.com/ai-mcpower/mcpower-proxy
29
31
  • MCP Official: https://modelcontextprotocol.io
30
- • Claude MCP Config: https://docs.anthropic.com/en/docs/claude-code/mcp
31
32
  """
32
33
  )
33
34
 
35
+ parser.add_argument(
36
+ '--ide-tool',
37
+ action='store_true',
38
+ help='Run in IDE tools mode'
39
+ )
40
+
41
+ parser.add_argument(
42
+ '--ide',
43
+ help='IDE name (required with --ide-tool, e.g., "cursor")'
44
+ )
45
+
46
+ parser.add_argument(
47
+ '--context',
48
+ help='Additional context (to be verified as optional by the associated --ide handler)'
49
+ )
50
+
34
51
  parser.add_argument(
35
52
  '--wrapped-config',
36
- required=True,
37
53
  help='JSON/JSONC configuration for the wrapped MCP server (FastMCP will handle validation)'
38
54
  )
39
55
 
@@ -43,4 +59,18 @@ Reference Links:
43
59
  help='Name for the wrapper MCP server (default: MCPWrapper)'
44
60
  )
45
61
 
46
- return parser.parse_args()
62
+ args = parser.parse_args()
63
+
64
+ # Validate: either --ide-tool or --wrapped-config is required
65
+ if not args.ide_tool and not args.wrapped_config:
66
+ parser.error("either --ide-tool or --wrapped-config is required")
67
+
68
+ # Validate: --ide-tool and --wrapped-config are mutually exclusive
69
+ if args.ide_tool and args.wrapped_config:
70
+ parser.error("--ide-tool and --wrapped-config are mutually exclusive")
71
+
72
+ # Validate: --ide-tool requires --ide
73
+ if args.ide_tool and not args.ide:
74
+ parser.error("--ide-tool requires --ide argument")
75
+
76
+ return args