mcp-ticketer 0.1.28__py3-none-any.whl → 0.1.29__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.

Potentially problematic release.


This version of mcp-ticketer might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
1
  """Version information for mcp-ticketer package."""
2
2
 
3
- __version__ = "0.1.28"
3
+ __version__ = "0.1.29"
4
4
  __version_info__ = tuple(int(part) for part in __version__.split("."))
5
5
 
6
6
  # Package metadata
@@ -0,0 +1,492 @@
1
+ """Comprehensive diagnostics and self-diagnosis functionality for MCP Ticketer."""
2
+
3
+ import asyncio
4
+ import json
5
+ import logging
6
+ import os
7
+ import sys
8
+ from datetime import datetime, timedelta
9
+ from pathlib import Path
10
+ from typing import Any, Dict, List, Optional, Tuple
11
+
12
+ import typer
13
+ from rich.console import Console
14
+ from rich.panel import Panel
15
+ from rich.table import Table
16
+ from rich.text import Text
17
+
18
+ try:
19
+ from ..core.config import get_config
20
+ except ImportError:
21
+ # Fallback for missing dependencies
22
+ def get_config():
23
+ raise ImportError("Configuration system not available")
24
+
25
+ try:
26
+ from ..core.registry import AdapterRegistry
27
+ except ImportError:
28
+ AdapterRegistry = None
29
+
30
+ try:
31
+ from ..queue.manager import QueueManager
32
+ except ImportError:
33
+ QueueManager = None
34
+
35
+ console = Console()
36
+ logger = logging.getLogger(__name__)
37
+
38
+
39
+ class SystemDiagnostics:
40
+ """Comprehensive system diagnostics and health reporting."""
41
+
42
+ def __init__(self):
43
+ try:
44
+ self.config = get_config()
45
+ except Exception as e:
46
+ self.config = None
47
+ console.print(f"⚠️ Could not load configuration: {e}")
48
+
49
+ try:
50
+ self.queue_manager = QueueManager() if QueueManager else None
51
+ except Exception as e:
52
+ self.queue_manager = None
53
+ console.print(f"⚠️ Could not initialize queue manager: {e}")
54
+
55
+ self.issues = []
56
+ self.warnings = []
57
+ self.successes = []
58
+
59
+ async def run_full_diagnosis(self) -> Dict[str, Any]:
60
+ """Run complete system diagnosis and return detailed report."""
61
+ console.print("\n🔍 [bold blue]MCP Ticketer System Diagnosis[/bold blue]")
62
+ console.print("=" * 60)
63
+
64
+ report = {
65
+ "timestamp": datetime.now().isoformat(),
66
+ "version": self._get_version(),
67
+ "system_info": self._get_system_info(),
68
+ "configuration": await self._diagnose_configuration(),
69
+ "adapters": await self._diagnose_adapters(),
70
+ "queue_system": await self._diagnose_queue_system(),
71
+ "recent_logs": await self._analyze_recent_logs(),
72
+ "performance": await self._analyze_performance(),
73
+ "recommendations": self._generate_recommendations(),
74
+ }
75
+
76
+ self._display_diagnosis_summary(report)
77
+ return report
78
+
79
+ def _get_version(self) -> str:
80
+ """Get current version information."""
81
+ try:
82
+ from ..__version__ import __version__
83
+ return __version__
84
+ except ImportError:
85
+ return "unknown"
86
+
87
+ def _get_system_info(self) -> Dict[str, Any]:
88
+ """Gather system information."""
89
+ return {
90
+ "python_version": sys.version,
91
+ "platform": sys.platform,
92
+ "working_directory": str(Path.cwd()),
93
+ "config_path": str(self.config.config_file) if hasattr(self.config, 'config_file') else "unknown",
94
+ }
95
+
96
+ async def _diagnose_configuration(self) -> Dict[str, Any]:
97
+ """Diagnose configuration issues."""
98
+ console.print("\n📋 [yellow]Configuration Analysis[/yellow]")
99
+
100
+ config_status = {
101
+ "status": "healthy",
102
+ "adapters_configured": 0,
103
+ "default_adapter": None,
104
+ "issues": [],
105
+ }
106
+
107
+ if not self.config:
108
+ issue = "Configuration system not available"
109
+ config_status["issues"].append(issue)
110
+ config_status["status"] = "critical"
111
+ self.issues.append(issue)
112
+ console.print(f"❌ {issue}")
113
+ return config_status
114
+
115
+ try:
116
+ # Check adapter configurations
117
+ adapters = self.config.get_enabled_adapters()
118
+ config_status["adapters_configured"] = len(adapters)
119
+ config_status["default_adapter"] = self.config.default_adapter
120
+
121
+ if not adapters:
122
+ issue = "No adapters configured"
123
+ config_status["issues"].append(issue)
124
+ config_status["status"] = "critical"
125
+ self.issues.append(issue)
126
+ console.print(f"❌ {issue}")
127
+ else:
128
+ console.print(f"✅ {len(adapters)} adapter(s) configured")
129
+
130
+ # Check each adapter configuration
131
+ for name, adapter_config in adapters.items():
132
+ try:
133
+ adapter_class = AdapterRegistry.get_adapter(adapter_config.type.value)
134
+ adapter = adapter_class(adapter_config.dict())
135
+
136
+ # Test adapter validation if available
137
+ if hasattr(adapter, 'validate_credentials'):
138
+ is_valid, error = adapter.validate_credentials()
139
+ if is_valid:
140
+ console.print(f"✅ {name}: credentials valid")
141
+ self.successes.append(f"{name} adapter configured correctly")
142
+ else:
143
+ issue = f"{name}: credential validation failed - {error}"
144
+ config_status["issues"].append(issue)
145
+ self.warnings.append(issue)
146
+ console.print(f"⚠️ {issue}")
147
+ else:
148
+ console.print(f"ℹ️ {name}: no credential validation available")
149
+
150
+ except Exception as e:
151
+ issue = f"{name}: configuration error - {str(e)}"
152
+ config_status["issues"].append(issue)
153
+ self.issues.append(issue)
154
+ console.print(f"❌ {issue}")
155
+
156
+ except Exception as e:
157
+ issue = f"Configuration loading failed: {str(e)}"
158
+ config_status["issues"].append(issue)
159
+ config_status["status"] = "critical"
160
+ self.issues.append(issue)
161
+ console.print(f"❌ {issue}")
162
+
163
+ return config_status
164
+
165
+ async def _diagnose_adapters(self) -> Dict[str, Any]:
166
+ """Diagnose adapter functionality."""
167
+ console.print("\n🔌 [yellow]Adapter Diagnosis[/yellow]")
168
+
169
+ adapter_status = {
170
+ "total_adapters": 0,
171
+ "healthy_adapters": 0,
172
+ "failed_adapters": 0,
173
+ "adapter_details": {},
174
+ }
175
+
176
+ try:
177
+ adapters = self.config.get_enabled_adapters()
178
+ adapter_status["total_adapters"] = len(adapters)
179
+
180
+ for name, adapter_config in adapters.items():
181
+ details = {
182
+ "type": adapter_config.type.value,
183
+ "status": "unknown",
184
+ "last_test": None,
185
+ "error": None,
186
+ }
187
+
188
+ try:
189
+ adapter_class = AdapterRegistry.get_adapter(adapter_config.type.value)
190
+ adapter = adapter_class(adapter_config.dict())
191
+
192
+ # Test basic adapter functionality
193
+ test_start = datetime.now()
194
+
195
+ # Try to list tickets (non-destructive test)
196
+ try:
197
+ await adapter.list(limit=1)
198
+ details["status"] = "healthy"
199
+ details["last_test"] = test_start.isoformat()
200
+ adapter_status["healthy_adapters"] += 1
201
+ console.print(f"✅ {name}: operational")
202
+ except Exception as e:
203
+ details["status"] = "failed"
204
+ details["error"] = str(e)
205
+ adapter_status["failed_adapters"] += 1
206
+ issue = f"{name}: functionality test failed - {str(e)}"
207
+ self.issues.append(issue)
208
+ console.print(f"❌ {issue}")
209
+
210
+ except Exception as e:
211
+ details["status"] = "failed"
212
+ details["error"] = str(e)
213
+ adapter_status["failed_adapters"] += 1
214
+ issue = f"{name}: initialization failed - {str(e)}"
215
+ self.issues.append(issue)
216
+ console.print(f"❌ {issue}")
217
+
218
+ adapter_status["adapter_details"][name] = details
219
+
220
+ except Exception as e:
221
+ issue = f"Adapter diagnosis failed: {str(e)}"
222
+ self.issues.append(issue)
223
+ console.print(f"❌ {issue}")
224
+
225
+ return adapter_status
226
+
227
+ async def _diagnose_queue_system(self) -> Dict[str, Any]:
228
+ """Diagnose queue system health."""
229
+ console.print("\n⚡ [yellow]Queue System Diagnosis[/yellow]")
230
+
231
+ queue_status = {
232
+ "worker_running": False,
233
+ "worker_pid": None,
234
+ "queue_stats": {},
235
+ "recent_failures": [],
236
+ "failure_rate": 0.0,
237
+ "health_score": 0,
238
+ }
239
+
240
+ try:
241
+ # Check worker status
242
+ worker_status = self.queue_manager.get_worker_status()
243
+ queue_status["worker_running"] = worker_status.get("running", False)
244
+ queue_status["worker_pid"] = worker_status.get("pid")
245
+
246
+ if queue_status["worker_running"]:
247
+ console.print(f"✅ Queue worker running (PID: {queue_status['worker_pid']})")
248
+ self.successes.append("Queue worker is running")
249
+ else:
250
+ issue = "Queue worker not running"
251
+ self.issues.append(issue)
252
+ console.print(f"❌ {issue}")
253
+
254
+ # Get queue statistics
255
+ stats = self.queue_manager.get_queue_stats()
256
+ queue_status["queue_stats"] = stats
257
+
258
+ total_items = stats.get("total", 0)
259
+ failed_items = stats.get("failed", 0)
260
+
261
+ if total_items > 0:
262
+ failure_rate = (failed_items / total_items) * 100
263
+ queue_status["failure_rate"] = failure_rate
264
+
265
+ if failure_rate > 50:
266
+ issue = f"High failure rate: {failure_rate:.1f}% ({failed_items}/{total_items})"
267
+ self.issues.append(issue)
268
+ console.print(f"❌ {issue}")
269
+ elif failure_rate > 20:
270
+ warning = f"Elevated failure rate: {failure_rate:.1f}% ({failed_items}/{total_items})"
271
+ self.warnings.append(warning)
272
+ console.print(f"⚠️ {warning}")
273
+ else:
274
+ console.print(f"✅ Queue failure rate: {failure_rate:.1f}% ({failed_items}/{total_items})")
275
+
276
+ # Calculate health score
277
+ health_score = 100
278
+ if not queue_status["worker_running"]:
279
+ health_score -= 50
280
+ health_score -= min(queue_status["failure_rate"], 50)
281
+ queue_status["health_score"] = max(0, health_score)
282
+
283
+ console.print(f"📊 Queue health score: {queue_status['health_score']}/100")
284
+
285
+ except Exception as e:
286
+ issue = f"Queue system diagnosis failed: {str(e)}"
287
+ self.issues.append(issue)
288
+ console.print(f"❌ {issue}")
289
+
290
+ return queue_status
291
+
292
+ async def _analyze_recent_logs(self) -> Dict[str, Any]:
293
+ """Analyze recent log entries for issues."""
294
+ console.print("\n📝 [yellow]Recent Log Analysis[/yellow]")
295
+
296
+ log_analysis = {
297
+ "log_files_found": [],
298
+ "recent_errors": [],
299
+ "recent_warnings": [],
300
+ "patterns": {},
301
+ }
302
+
303
+ try:
304
+ # Look for common log locations
305
+ log_paths = [
306
+ Path.home() / ".mcp-ticketer" / "logs",
307
+ Path.cwd() / ".mcp-ticketer" / "logs",
308
+ Path("/var/log/mcp-ticketer"),
309
+ ]
310
+
311
+ for log_path in log_paths:
312
+ if log_path.exists():
313
+ log_analysis["log_files_found"].append(str(log_path))
314
+ await self._analyze_log_directory(log_path, log_analysis)
315
+
316
+ if not log_analysis["log_files_found"]:
317
+ console.print("ℹ️ No log files found in standard locations")
318
+ else:
319
+ console.print(f"✅ Found logs in {len(log_analysis['log_files_found'])} location(s)")
320
+
321
+ except Exception as e:
322
+ issue = f"Log analysis failed: {str(e)}"
323
+ self.issues.append(issue)
324
+ console.print(f"❌ {issue}")
325
+
326
+ return log_analysis
327
+
328
+ async def _analyze_log_directory(self, log_path: Path, log_analysis: Dict[str, Any]):
329
+ """Analyze logs in a specific directory."""
330
+ try:
331
+ for log_file in log_path.glob("*.log"):
332
+ if log_file.stat().st_mtime > (datetime.now() - timedelta(hours=24)).timestamp():
333
+ await self._parse_log_file(log_file, log_analysis)
334
+ except Exception as e:
335
+ self.warnings.append(f"Could not analyze logs in {log_path}: {str(e)}")
336
+
337
+ async def _parse_log_file(self, log_file: Path, log_analysis: Dict[str, Any]):
338
+ """Parse individual log file for issues."""
339
+ try:
340
+ with open(log_file, 'r') as f:
341
+ lines = f.readlines()[-100:] # Last 100 lines
342
+
343
+ for line in lines:
344
+ if "ERROR" in line:
345
+ log_analysis["recent_errors"].append(line.strip())
346
+ elif "WARNING" in line:
347
+ log_analysis["recent_warnings"].append(line.strip())
348
+
349
+ except Exception as e:
350
+ self.warnings.append(f"Could not parse {log_file}: {str(e)}")
351
+
352
+ async def _analyze_performance(self) -> Dict[str, Any]:
353
+ """Analyze system performance metrics."""
354
+ console.print("\n⚡ [yellow]Performance Analysis[/yellow]")
355
+
356
+ performance = {
357
+ "response_times": {},
358
+ "throughput": {},
359
+ "resource_usage": {},
360
+ }
361
+
362
+ try:
363
+ # Test basic operations performance
364
+ start_time = datetime.now()
365
+
366
+ # Test configuration loading
367
+ config_start = datetime.now()
368
+ _ = get_config()
369
+ config_time = (datetime.now() - config_start).total_seconds()
370
+ performance["response_times"]["config_load"] = config_time
371
+
372
+ if config_time > 1.0:
373
+ self.warnings.append(f"Slow configuration loading: {config_time:.2f}s")
374
+
375
+ console.print(f"📊 Configuration load time: {config_time:.3f}s")
376
+
377
+ except Exception as e:
378
+ issue = f"Performance analysis failed: {str(e)}"
379
+ self.issues.append(issue)
380
+ console.print(f"❌ {issue}")
381
+
382
+ return performance
383
+
384
+ def _generate_recommendations(self) -> List[str]:
385
+ """Generate actionable recommendations based on diagnosis."""
386
+ recommendations = []
387
+
388
+ if self.issues:
389
+ recommendations.append("🚨 Critical issues detected - immediate attention required")
390
+
391
+ if any("Queue worker not running" in issue for issue in self.issues):
392
+ recommendations.append("• Restart queue worker: mcp-ticketer queue worker restart")
393
+
394
+ if any("failure rate" in issue.lower() for issue in self.issues):
395
+ recommendations.append("• Check queue system logs for error patterns")
396
+ recommendations.append("• Consider clearing failed queue items: mcp-ticketer queue clear --failed")
397
+
398
+ if any("No adapters configured" in issue for issue in self.issues):
399
+ recommendations.append("• Configure at least one adapter: mcp-ticketer init-aitrackdown")
400
+
401
+ if self.warnings:
402
+ recommendations.append("⚠️ Warnings detected - monitoring recommended")
403
+
404
+ if not self.issues and not self.warnings:
405
+ recommendations.append("✅ System appears healthy - no immediate action required")
406
+
407
+ return recommendations
408
+
409
+ def _display_diagnosis_summary(self, report: Dict[str, Any]):
410
+ """Display a comprehensive diagnosis summary."""
411
+ console.print("\n" + "=" * 60)
412
+ console.print("📋 [bold green]DIAGNOSIS SUMMARY[/bold green]")
413
+ console.print("=" * 60)
414
+
415
+ # Overall health status
416
+ if self.issues:
417
+ status_color = "red"
418
+ status_text = "CRITICAL"
419
+ status_icon = "🚨"
420
+ elif self.warnings:
421
+ status_color = "yellow"
422
+ status_text = "WARNING"
423
+ status_icon = "⚠️"
424
+ else:
425
+ status_color = "green"
426
+ status_text = "HEALTHY"
427
+ status_icon = "✅"
428
+
429
+ console.print(f"\n{status_icon} [bold {status_color}]System Status: {status_text}[/bold {status_color}]")
430
+
431
+ # Statistics
432
+ stats_table = Table(show_header=True, header_style="bold blue")
433
+ stats_table.add_column("Component")
434
+ stats_table.add_column("Status")
435
+ stats_table.add_column("Details")
436
+
437
+ # Add component statuses
438
+ config_status = "✅ OK" if not any("configuration" in issue.lower() for issue in self.issues) else "❌ FAILED"
439
+ stats_table.add_row("Configuration", config_status, f"{report['configuration']['adapters_configured']} adapters")
440
+
441
+ queue_health = report['queue_system']['health_score']
442
+ queue_status = "✅ OK" if queue_health > 80 else "⚠️ DEGRADED" if queue_health > 50 else "❌ FAILED"
443
+ stats_table.add_row("Queue System", queue_status, f"{queue_health}/100 health score")
444
+
445
+ adapter_stats = report['adapters']
446
+ adapter_status = "✅ OK" if adapter_stats['failed_adapters'] == 0 else "❌ FAILED"
447
+ stats_table.add_row("Adapters", adapter_status, f"{adapter_stats['healthy_adapters']}/{adapter_stats['total_adapters']} healthy")
448
+
449
+ console.print(stats_table)
450
+
451
+ # Issues and recommendations
452
+ if self.issues:
453
+ console.print(f"\n🚨 [bold red]Critical Issues ({len(self.issues)}):[/bold red]")
454
+ for issue in self.issues:
455
+ console.print(f" • {issue}")
456
+
457
+ if self.warnings:
458
+ console.print(f"\n⚠️ [bold yellow]Warnings ({len(self.warnings)}):[/bold yellow]")
459
+ for warning in self.warnings:
460
+ console.print(f" • {warning}")
461
+
462
+ if report['recommendations']:
463
+ console.print(f"\n💡 [bold blue]Recommendations:[/bold blue]")
464
+ for rec in report['recommendations']:
465
+ console.print(f" {rec}")
466
+
467
+ console.print(f"\n📊 [bold]Summary:[/bold] {len(self.successes)} successes, {len(self.warnings)} warnings, {len(self.issues)} critical issues")
468
+
469
+
470
+ async def run_diagnostics(
471
+ output_file: Optional[str] = None,
472
+ json_output: bool = False,
473
+ ) -> None:
474
+ """Run comprehensive system diagnostics."""
475
+ diagnostics = SystemDiagnostics()
476
+ report = await diagnostics.run_full_diagnosis()
477
+
478
+ if output_file:
479
+ with open(output_file, 'w') as f:
480
+ json.dump(report, f, indent=2)
481
+ console.print(f"\n📄 Full report saved to: {output_file}")
482
+
483
+ if json_output:
484
+ console.print("\n" + json.dumps(report, indent=2))
485
+
486
+ # Return exit code based on issues
487
+ if diagnostics.issues:
488
+ raise typer.Exit(1) # Critical issues found
489
+ elif diagnostics.warnings:
490
+ raise typer.Exit(2) # Warnings found
491
+ else:
492
+ raise typer.Exit(0) # All good
mcp_ticketer/cli/main.py CHANGED
@@ -22,6 +22,7 @@ from ..queue.ticket_registry import TicketRegistry
22
22
  # Import adapters module to trigger registration
23
23
  import mcp_ticketer.adapters # noqa: F401
24
24
  from .configure import configure_wizard, set_adapter_config, show_current_config
25
+ from .diagnostics import run_diagnostics
25
26
  from .discover import app as discover_app
26
27
  from .migrate_config import migrate_config_command
27
28
  from .queue_commands import app as queue_app
@@ -1260,6 +1261,48 @@ app.add_typer(queue_app, name="queue")
1260
1261
  # Add discover command to main app
1261
1262
  app.add_typer(discover_app, name="discover")
1262
1263
 
1264
+ # Add diagnostics command
1265
+ @app.command()
1266
+ def diagnose(
1267
+ output_file: Optional[str] = typer.Option(None, "--output", "-o", help="Save full report to file"),
1268
+ json_output: bool = typer.Option(False, "--json", help="Output report in JSON format"),
1269
+ simple: bool = typer.Option(False, "--simple", help="Use simple diagnostics (no heavy dependencies)"),
1270
+ ) -> None:
1271
+ """Run comprehensive system diagnostics and health check."""
1272
+ if simple:
1273
+ from .simple_health import simple_diagnose
1274
+ report = simple_diagnose()
1275
+ if output_file:
1276
+ import json
1277
+ with open(output_file, 'w') as f:
1278
+ json.dump(report, f, indent=2)
1279
+ console.print(f"\n📄 Report saved to: {output_file}")
1280
+ if json_output:
1281
+ import json
1282
+ console.print("\n" + json.dumps(report, indent=2))
1283
+ if report["issues"]:
1284
+ raise typer.Exit(1)
1285
+ else:
1286
+ try:
1287
+ asyncio.run(run_diagnostics(output_file=output_file, json_output=json_output))
1288
+ except Exception as e:
1289
+ console.print(f"⚠️ Full diagnostics failed: {e}")
1290
+ console.print("🔄 Falling back to simple diagnostics...")
1291
+ from .simple_health import simple_diagnose
1292
+ report = simple_diagnose()
1293
+ if report["issues"]:
1294
+ raise typer.Exit(1)
1295
+
1296
+
1297
+ @app.command()
1298
+ def health() -> None:
1299
+ """Quick health check - shows system status summary."""
1300
+ from .simple_health import simple_health_check
1301
+
1302
+ result = simple_health_check()
1303
+ if result != 0:
1304
+ raise typer.Exit(result)
1305
+
1263
1306
  # Create MCP configuration command group
1264
1307
  mcp_app = typer.Typer(
1265
1308
  name="mcp",
@@ -0,0 +1,219 @@
1
+ """Simple health check that doesn't require full configuration system."""
2
+
3
+ import os
4
+ import sys
5
+ from pathlib import Path
6
+ from typing import Dict, Any
7
+
8
+ from rich.console import Console
9
+
10
+ console = Console()
11
+
12
+
13
+ def simple_health_check() -> int:
14
+ """Perform a simple health check without heavy dependencies."""
15
+ console.print("\n🏥 [bold blue]MCP Ticketer Quick Health Check[/bold blue]")
16
+ console.print("=" * 50)
17
+
18
+ issues = 0
19
+
20
+ # Check Python version
21
+ python_version = sys.version_info
22
+ if python_version >= (3, 9):
23
+ console.print(f"✅ Python: {python_version.major}.{python_version.minor}.{python_version.micro}")
24
+ else:
25
+ console.print(f"❌ Python: {python_version.major}.{python_version.minor}.{python_version.micro} (requires 3.9+)")
26
+ issues += 1
27
+
28
+ # Check for basic configuration files
29
+ config_files = [
30
+ ".mcp-ticketer.yaml",
31
+ ".mcp-ticketer.yml",
32
+ "mcp-ticketer.yaml",
33
+ "mcp-ticketer.yml",
34
+ ".aitrackdown",
35
+ ]
36
+
37
+ config_found = False
38
+ for config_file in config_files:
39
+ if Path(config_file).exists():
40
+ console.print(f"✅ Configuration: Found {config_file}")
41
+ config_found = True
42
+ break
43
+
44
+ if not config_found:
45
+ console.print("⚠️ Configuration: No config files found (will use defaults)")
46
+
47
+ # Check for aitrackdown directory (default adapter)
48
+ aitrackdown_path = Path(".aitrackdown")
49
+ if aitrackdown_path.exists():
50
+ console.print(f"✅ Aitrackdown: Directory exists at {aitrackdown_path}")
51
+
52
+ # Check for tickets
53
+ tickets_dir = aitrackdown_path / "tickets"
54
+ if tickets_dir.exists():
55
+ ticket_count = len(list(tickets_dir.glob("*.json")))
56
+ console.print(f"ℹ️ Aitrackdown: {ticket_count} tickets found")
57
+ else:
58
+ console.print("ℹ️ Aitrackdown: No tickets directory (will be created)")
59
+ else:
60
+ console.print("ℹ️ Aitrackdown: Directory will be created on first use")
61
+
62
+ # Check environment variables
63
+ env_vars = [
64
+ "LINEAR_API_KEY",
65
+ "LINEAR_TEAM_ID",
66
+ "GITHUB_TOKEN",
67
+ "GITHUB_REPO",
68
+ "JIRA_SERVER",
69
+ "JIRA_EMAIL",
70
+ "JIRA_API_TOKEN",
71
+ ]
72
+
73
+ env_found = []
74
+ for var in env_vars:
75
+ if os.getenv(var):
76
+ env_found.append(var)
77
+
78
+ if env_found:
79
+ console.print(f"✅ Environment: {len(env_found)} adapter variables configured")
80
+ for var in env_found:
81
+ console.print(f" • {var}")
82
+ else:
83
+ console.print("ℹ️ Environment: No adapter variables found (using defaults)")
84
+
85
+ # Check if we can import core modules
86
+ try:
87
+ import mcp_ticketer
88
+ console.print(f"✅ Installation: mcp-ticketer {mcp_ticketer.__version__} installed")
89
+ except Exception as e:
90
+ console.print(f"❌ Installation: Import failed - {e}")
91
+ issues += 1
92
+
93
+ # Try to check queue system (simplified)
94
+ try:
95
+ from ..queue.manager import QueueManager
96
+ queue_manager = QueueManager()
97
+ worker_status = queue_manager.get_worker_status()
98
+
99
+ if worker_status.get("running", False):
100
+ console.print(f"✅ Queue Worker: Running (PID: {worker_status.get('pid')})")
101
+ else:
102
+ console.print("⚠️ Queue Worker: Not running (start with: mcp-ticketer queue worker start)")
103
+
104
+ # Get basic stats
105
+ stats = queue_manager.get_queue_stats()
106
+ total = stats.get("total", 0)
107
+ failed = stats.get("failed", 0)
108
+
109
+ if total > 0:
110
+ failure_rate = (failed / total) * 100
111
+ if failure_rate > 50:
112
+ console.print(f"❌ Queue Health: High failure rate {failure_rate:.1f}% ({failed}/{total})")
113
+ issues += 1
114
+ elif failure_rate > 20:
115
+ console.print(f"⚠️ Queue Health: Elevated failure rate {failure_rate:.1f}% ({failed}/{total})")
116
+ else:
117
+ console.print(f"✅ Queue Health: {failure_rate:.1f}% failure rate ({failed}/{total})")
118
+ else:
119
+ console.print("ℹ️ Queue Health: No items processed yet")
120
+
121
+ except Exception as e:
122
+ console.print(f"⚠️ Queue System: Could not check status - {e}")
123
+
124
+ # Summary
125
+ console.print()
126
+ if issues == 0:
127
+ console.print("🎉 [bold green]System appears healthy![/bold green]")
128
+ console.print("💡 For detailed diagnosis, run: mcp-ticketer diagnose")
129
+ return 0
130
+ else:
131
+ console.print(f"⚠️ [bold yellow]{issues} issue(s) detected[/bold yellow]")
132
+ console.print("💡 For detailed diagnosis, run: mcp-ticketer diagnose")
133
+ return 1
134
+
135
+
136
+ def simple_diagnose() -> Dict[str, Any]:
137
+ """Simple diagnosis that works without full config system."""
138
+ console.print("\n🔍 [bold blue]MCP Ticketer Simple Diagnosis[/bold blue]")
139
+ console.print("=" * 60)
140
+
141
+ report = {
142
+ "timestamp": "2025-10-24", # Static for now
143
+ "version": "0.1.28",
144
+ "python_version": f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
145
+ "working_directory": str(Path.cwd()),
146
+ "issues": [],
147
+ "warnings": [],
148
+ "recommendations": [],
149
+ }
150
+
151
+ # Basic checks
152
+ console.print("\n📋 [yellow]Basic System Check[/yellow]")
153
+
154
+ # Python version
155
+ if sys.version_info < (3, 9):
156
+ issue = f"Python {sys.version_info.major}.{sys.version_info.minor} is too old (requires 3.9+)"
157
+ report["issues"].append(issue)
158
+ console.print(f"❌ {issue}")
159
+ else:
160
+ console.print(f"✅ Python {sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}")
161
+
162
+ # Installation check
163
+ try:
164
+ import mcp_ticketer
165
+ console.print(f"✅ mcp-ticketer {mcp_ticketer.__version__} installed")
166
+ except Exception as e:
167
+ issue = f"Installation check failed: {e}"
168
+ report["issues"].append(issue)
169
+ console.print(f"❌ {issue}")
170
+
171
+ # Configuration check
172
+ console.print("\n📋 [yellow]Configuration Check[/yellow]")
173
+ config_files = [".mcp-ticketer.yaml", ".mcp-ticketer.yml", "mcp-ticketer.yaml", "mcp-ticketer.yml"]
174
+ config_found = any(Path(f).exists() for f in config_files)
175
+
176
+ if config_found:
177
+ console.print("✅ Configuration files found")
178
+ else:
179
+ console.print("ℹ️ No configuration files (using defaults)")
180
+
181
+ # Environment variables
182
+ env_vars = ["LINEAR_API_KEY", "GITHUB_TOKEN", "JIRA_SERVER"]
183
+ env_count = sum(1 for var in env_vars if os.getenv(var))
184
+
185
+ if env_count > 0:
186
+ console.print(f"✅ {env_count} adapter environment variables configured")
187
+ else:
188
+ console.print("ℹ️ No adapter environment variables (using aitrackdown)")
189
+
190
+ # Recommendations
191
+ if not report["issues"]:
192
+ report["recommendations"].append("✅ System appears healthy")
193
+ else:
194
+ report["recommendations"].append("🚨 Critical issues detected - see above")
195
+
196
+ if not config_found and env_count == 0:
197
+ report["recommendations"].append("💡 Consider running: mcp-ticketer init-aitrackdown")
198
+
199
+ # Display summary
200
+ console.print("\n" + "=" * 60)
201
+ console.print("📋 [bold green]DIAGNOSIS SUMMARY[/bold green]")
202
+ console.print("=" * 60)
203
+
204
+ if report["issues"]:
205
+ console.print(f"\n🚨 [bold red]Issues ({len(report['issues'])}):[/bold red]")
206
+ for issue in report["issues"]:
207
+ console.print(f" • {issue}")
208
+
209
+ if report["warnings"]:
210
+ console.print(f"\n⚠️ [bold yellow]Warnings ({len(report['warnings'])}):[/bold yellow]")
211
+ for warning in report["warnings"]:
212
+ console.print(f" • {warning}")
213
+
214
+ if report["recommendations"]:
215
+ console.print(f"\n💡 [bold blue]Recommendations:[/bold blue]")
216
+ for rec in report["recommendations"]:
217
+ console.print(f" {rec}")
218
+
219
+ return report
@@ -1460,6 +1460,29 @@ class MCPTicketServer:
1460
1460
  "required": ["queue_id"],
1461
1461
  },
1462
1462
  },
1463
+ # System diagnostics tools
1464
+ {
1465
+ "name": "system_health",
1466
+ "description": "Quick system health check - shows configuration, queue worker, and failure rates",
1467
+ "inputSchema": {
1468
+ "type": "object",
1469
+ "properties": {},
1470
+ },
1471
+ },
1472
+ {
1473
+ "name": "system_diagnose",
1474
+ "description": "Comprehensive system diagnostics - detailed analysis of all components",
1475
+ "inputSchema": {
1476
+ "type": "object",
1477
+ "properties": {
1478
+ "include_logs": {
1479
+ "type": "boolean",
1480
+ "default": False,
1481
+ "description": "Include recent log analysis in diagnosis",
1482
+ },
1483
+ },
1484
+ },
1485
+ },
1463
1486
  ]
1464
1487
  }
1465
1488
 
@@ -1514,6 +1537,11 @@ class MCPTicketServer:
1514
1537
  result = await self._handle_search(arguments)
1515
1538
  elif tool_name == "ticket_status":
1516
1539
  result = await self._handle_queue_status(arguments)
1540
+ # System diagnostics
1541
+ elif tool_name == "system_health":
1542
+ result = await self._handle_system_health(arguments)
1543
+ elif tool_name == "system_diagnose":
1544
+ result = await self._handle_system_diagnose(arguments)
1517
1545
  # PR integration
1518
1546
  elif tool_name == "ticket_create_pr":
1519
1547
  result = await self._handle_create_pr(arguments)
@@ -1674,5 +1702,194 @@ async def main():
1674
1702
  await server.run()
1675
1703
 
1676
1704
 
1705
+ # Add diagnostic handler methods to MCPTicketServer class
1706
+ async def _handle_system_health(self, arguments: dict[str, Any]) -> dict[str, Any]:
1707
+ """Handle system health check."""
1708
+ from ..cli.diagnostics import SystemDiagnostics
1709
+
1710
+ try:
1711
+ diagnostics = SystemDiagnostics()
1712
+
1713
+ # Quick health checks
1714
+ health_status = {
1715
+ "overall_status": "healthy",
1716
+ "components": {},
1717
+ "issues": [],
1718
+ "warnings": [],
1719
+ }
1720
+
1721
+ # Check configuration
1722
+ try:
1723
+ from ..core.config import get_config
1724
+ config = get_config()
1725
+ adapters = config.get_enabled_adapters()
1726
+ if adapters:
1727
+ health_status["components"]["configuration"] = {
1728
+ "status": "healthy",
1729
+ "adapters_count": len(adapters),
1730
+ }
1731
+ else:
1732
+ health_status["components"]["configuration"] = {
1733
+ "status": "failed",
1734
+ "error": "No adapters configured",
1735
+ }
1736
+ health_status["issues"].append("No adapters configured")
1737
+ health_status["overall_status"] = "critical"
1738
+ except Exception as e:
1739
+ health_status["components"]["configuration"] = {
1740
+ "status": "failed",
1741
+ "error": str(e),
1742
+ }
1743
+ health_status["issues"].append(f"Configuration error: {str(e)}")
1744
+ health_status["overall_status"] = "critical"
1745
+
1746
+ # Check queue system
1747
+ try:
1748
+ from ..queue.manager import QueueManager
1749
+ queue_manager = QueueManager()
1750
+ worker_status = queue_manager.get_worker_status()
1751
+ stats = queue_manager.get_queue_stats()
1752
+
1753
+ total = stats.get("total", 0)
1754
+ failed = stats.get("failed", 0)
1755
+ failure_rate = (failed / total * 100) if total > 0 else 0
1756
+
1757
+ queue_health = {
1758
+ "status": "healthy",
1759
+ "worker_running": worker_status.get("running", False),
1760
+ "worker_pid": worker_status.get("pid"),
1761
+ "failure_rate": failure_rate,
1762
+ "total_processed": total,
1763
+ "failed_items": failed,
1764
+ }
1765
+
1766
+ if not worker_status.get("running", False):
1767
+ queue_health["status"] = "failed"
1768
+ health_status["issues"].append("Queue worker not running")
1769
+ health_status["overall_status"] = "critical"
1770
+ elif failure_rate > 50:
1771
+ queue_health["status"] = "degraded"
1772
+ health_status["issues"].append(f"High queue failure rate: {failure_rate:.1f}%")
1773
+ health_status["overall_status"] = "critical"
1774
+ elif failure_rate > 20:
1775
+ queue_health["status"] = "warning"
1776
+ health_status["warnings"].append(f"Elevated queue failure rate: {failure_rate:.1f}%")
1777
+ if health_status["overall_status"] == "healthy":
1778
+ health_status["overall_status"] = "warning"
1779
+
1780
+ health_status["components"]["queue_system"] = queue_health
1781
+
1782
+ except Exception as e:
1783
+ health_status["components"]["queue_system"] = {
1784
+ "status": "failed",
1785
+ "error": str(e),
1786
+ }
1787
+ health_status["issues"].append(f"Queue system error: {str(e)}")
1788
+ health_status["overall_status"] = "critical"
1789
+
1790
+ return {
1791
+ "content": [
1792
+ {
1793
+ "type": "text",
1794
+ "text": f"System Health Status: {health_status['overall_status'].upper()}\n\n" +
1795
+ f"Configuration: {health_status['components'].get('configuration', {}).get('status', 'unknown')}\n" +
1796
+ f"Queue System: {health_status['components'].get('queue_system', {}).get('status', 'unknown')}\n\n" +
1797
+ f"Issues: {len(health_status['issues'])}\n" +
1798
+ f"Warnings: {len(health_status['warnings'])}\n\n" +
1799
+ (f"Critical Issues:\n" + "\n".join(f"• {issue}" for issue in health_status['issues']) + "\n\n" if health_status['issues'] else "") +
1800
+ (f"Warnings:\n" + "\n".join(f"• {warning}" for warning in health_status['warnings']) + "\n\n" if health_status['warnings'] else "") +
1801
+ "For detailed diagnosis, use system_diagnose tool.",
1802
+ }
1803
+ ],
1804
+ "isError": health_status["overall_status"] == "critical",
1805
+ }
1806
+
1807
+ except Exception as e:
1808
+ return {
1809
+ "content": [
1810
+ {
1811
+ "type": "text",
1812
+ "text": f"Health check failed: {str(e)}",
1813
+ }
1814
+ ],
1815
+ "isError": True,
1816
+ }
1817
+
1818
+
1819
+ async def _handle_system_diagnose(self, arguments: dict[str, Any]) -> dict[str, Any]:
1820
+ """Handle comprehensive system diagnosis."""
1821
+ from ..cli.diagnostics import SystemDiagnostics
1822
+
1823
+ try:
1824
+ diagnostics = SystemDiagnostics()
1825
+ report = await diagnostics.run_full_diagnosis()
1826
+
1827
+ # Format report for MCP response
1828
+ summary = f"""System Diagnosis Report
1829
+ Generated: {report['timestamp']}
1830
+ Version: {report['version']}
1831
+
1832
+ OVERALL STATUS: {
1833
+ 'CRITICAL' if diagnostics.issues else
1834
+ 'WARNING' if diagnostics.warnings else
1835
+ 'HEALTHY'
1836
+ }
1837
+
1838
+ COMPONENT STATUS:
1839
+ • Configuration: {len(report['configuration']['issues'])} issues
1840
+ • Adapters: {report['adapters']['failed_adapters']}/{report['adapters']['total_adapters']} failed
1841
+ • Queue System: {report['queue_system']['health_score']}/100 health score
1842
+
1843
+ STATISTICS:
1844
+ • Successes: {len(diagnostics.successes)}
1845
+ • Warnings: {len(diagnostics.warnings)}
1846
+ • Critical Issues: {len(diagnostics.issues)}
1847
+
1848
+ """
1849
+
1850
+ if diagnostics.issues:
1851
+ summary += "CRITICAL ISSUES:\n"
1852
+ for issue in diagnostics.issues:
1853
+ summary += f"• {issue}\n"
1854
+ summary += "\n"
1855
+
1856
+ if diagnostics.warnings:
1857
+ summary += "WARNINGS:\n"
1858
+ for warning in diagnostics.warnings:
1859
+ summary += f"• {warning}\n"
1860
+ summary += "\n"
1861
+
1862
+ if report['recommendations']:
1863
+ summary += "RECOMMENDATIONS:\n"
1864
+ for rec in report['recommendations']:
1865
+ summary += f"{rec}\n"
1866
+
1867
+ return {
1868
+ "content": [
1869
+ {
1870
+ "type": "text",
1871
+ "text": summary,
1872
+ }
1873
+ ],
1874
+ "isError": bool(diagnostics.issues),
1875
+ }
1876
+
1877
+ except Exception as e:
1878
+ return {
1879
+ "content": [
1880
+ {
1881
+ "type": "text",
1882
+ "text": f"System diagnosis failed: {str(e)}",
1883
+ }
1884
+ ],
1885
+ "isError": True,
1886
+ }
1887
+
1888
+
1889
+ # Monkey patch the methods onto the class
1890
+ MCPTicketServer._handle_system_health = _handle_system_health
1891
+ MCPTicketServer._handle_system_diagnose = _handle_system_diagnose
1892
+
1893
+
1677
1894
  if __name__ == "__main__":
1678
1895
  asyncio.run(main())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcp-ticketer
3
- Version: 0.1.28
3
+ Version: 0.1.29
4
4
  Summary: Universal ticket management interface for AI agents with MCP support
5
5
  Author-email: MCP Ticketer Team <support@mcp-ticketer.io>
6
6
  Maintainer-email: MCP Ticketer Team <support@mcp-ticketer.io>
@@ -1,5 +1,5 @@
1
1
  mcp_ticketer/__init__.py,sha256=Xx4WaprO5PXhVPbYi1L6tBmwmJMkYS-lMyG4ieN6QP0,717
2
- mcp_ticketer/__version__.py,sha256=w-yViL73x_vy42OtylKLgKgQWyIAAbOKsrNKa-cIlJg,1118
2
+ mcp_ticketer/__version__.py,sha256=I8xIN53wlOPHpYlXb_v9Bev1TsC83fWE_W-7TXrQpUQ,1118
3
3
  mcp_ticketer/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  mcp_ticketer/adapters/__init__.py,sha256=B5DFllWn23hkhmrLykNO5uMMSdcFuuPHXyLw_jyFzuE,358
5
5
  mcp_ticketer/adapters/aitrackdown.py,sha256=stlbge8K6w-EyQkw_vEQNSXQgCOWN5tOlQUgGWZQNMQ,17936
@@ -13,12 +13,14 @@ mcp_ticketer/cli/__init__.py,sha256=l9Q8iKmfGkTu0cssHBVqNZTsL4eAtFzOB25AED_0G6g,
13
13
  mcp_ticketer/cli/auggie_configure.py,sha256=MXKzLtqe3K_UTQ2GacHAWbvf_B0779KL325smiAKE0Q,8212
14
14
  mcp_ticketer/cli/codex_configure.py,sha256=xDppHouT6_-cYXswyAggoPX5bSlRXMvCoM_x9PQ-42A,9086
15
15
  mcp_ticketer/cli/configure.py,sha256=BsA_pSHQMQS0t1bJO_wMM8LWsd5sWJDASjEPRHvwC18,16198
16
+ mcp_ticketer/cli/diagnostics.py,sha256=mr11oJRx82PIXO7vNv6Jl93E4mH77daz41Ra7NuesW8,19363
16
17
  mcp_ticketer/cli/discover.py,sha256=AF_qlQc1Oo0UkWayoF5pmRChS5J3fJjH6f2YZzd_k8w,13188
17
18
  mcp_ticketer/cli/gemini_configure.py,sha256=ZNSA1lBW-itVToza-JxW95Po7daVXKiZAh7lp6pmXMU,9343
18
- mcp_ticketer/cli/main.py,sha256=hXPQyeQ9dv5Ry1XxSJZqmanw2KTgN912eXd1dkwd_os,53326
19
+ mcp_ticketer/cli/main.py,sha256=TxhCdmEi97rrDkCd0nT4KYRtZSboTfPSrw7SL607goA,54970
19
20
  mcp_ticketer/cli/mcp_configure.py,sha256=RzV50UjXgOmvMp-9S0zS39psuvjffVByaMrqrUaAGAM,9594
20
21
  mcp_ticketer/cli/migrate_config.py,sha256=MYsr_C5ZxsGg0P13etWTWNrJ_lc6ElRCkzfQADYr3DM,5956
21
22
  mcp_ticketer/cli/queue_commands.py,sha256=mm-3H6jmkUGJDyU_E46o9iRpek8tvFCm77F19OtHiZI,7884
23
+ mcp_ticketer/cli/simple_health.py,sha256=FIMHbrSNHpNJHXx7wtH8HzQXmPlcF9HQE9ngxTbxhMM,8035
22
24
  mcp_ticketer/cli/utils.py,sha256=2ptUrp2ELZsox0kSxAI5DFrHonOU999qh4MxbLv6VBQ,21155
23
25
  mcp_ticketer/core/__init__.py,sha256=eXovsaJymQRP2AwOBuOy6mFtI3I68D7gGenZ5V-IMqo,349
24
26
  mcp_ticketer/core/adapter.py,sha256=q64LxOInIno7EIbmuxItf8KEsd-g9grCs__Z4uwZHto,10273
@@ -30,7 +32,7 @@ mcp_ticketer/core/models.py,sha256=DRuJoYbjp9fcPV9GwQfhVcNUB0XmwQB3vuqW8hQWZ_k,6
30
32
  mcp_ticketer/core/project_config.py,sha256=yYxlgxjcEPeOwx-b-SXFpe0k9pW9xzBRAK72PsItG-o,23346
31
33
  mcp_ticketer/core/registry.py,sha256=ShYLDPE62KFJpB0kj_zFyQzRxSH3LkQEEuo1jaakb1k,3483
32
34
  mcp_ticketer/mcp/__init__.py,sha256=Y05eTzsPk0wH8yKNIM-ekpGjgSDO0bQr0EME-vOP4GE,123
33
- mcp_ticketer/mcp/server.py,sha256=PpENqLi9qdhxT1KTYrjkekT1LWP2mfTZY-PF6la1hs4,68078
35
+ mcp_ticketer/mcp/server.py,sha256=HApPT2dICSNXNzedtJa0EM9-f74OPBgZYhcKFVdvNSU,76379
34
36
  mcp_ticketer/queue/__init__.py,sha256=1YIaCpZpFqPcqvDEQXiEvDLiw94DXRdCJkBaVIFQrms,231
35
37
  mcp_ticketer/queue/__main__.py,sha256=gc_tE9NUdK07OJfTZuD4t6KeBD_vxFQIhknGTQUG_jk,109
36
38
  mcp_ticketer/queue/health_monitor.py,sha256=aQrlBzfbLWu8-fV2b5CuHs4oqyTqGGcntKIHM3r-dDI,11844
@@ -39,9 +41,9 @@ mcp_ticketer/queue/queue.py,sha256=jSAkYNEIbNH1cbYuF8s6eFuZmXqn8WHXx3mbfMU2Ud8,1
39
41
  mcp_ticketer/queue/run_worker.py,sha256=_IBezjvhbJJ7gn0evTBIMbSPjvfFZwxEdT-1DLo_bRk,799
40
42
  mcp_ticketer/queue/ticket_registry.py,sha256=k8FYg2cFYsI4POb94-o-fTrIVr-ttfi60r0O5YhJYck,15321
41
43
  mcp_ticketer/queue/worker.py,sha256=TLXXXTAQT1k9Oiw2WjSd8bzT3rr8TQ8NLt9JBovGQEA,18679
42
- mcp_ticketer-0.1.28.dist-info/licenses/LICENSE,sha256=KOVrunjtILSzY-2N8Lqa3-Q8dMaZIG4LrlLTr9UqL08,1073
43
- mcp_ticketer-0.1.28.dist-info/METADATA,sha256=fJ7LnYE7qITkq4JVxoYUmNZavf9K48EhBPVshcAUOQs,13191
44
- mcp_ticketer-0.1.28.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
45
- mcp_ticketer-0.1.28.dist-info/entry_points.txt,sha256=o1IxVhnHnBNG7FZzbFq-Whcs1Djbofs0qMjiUYBLx2s,60
46
- mcp_ticketer-0.1.28.dist-info/top_level.txt,sha256=WnAG4SOT1Vm9tIwl70AbGG_nA217YyV3aWFhxLH2rxw,13
47
- mcp_ticketer-0.1.28.dist-info/RECORD,,
44
+ mcp_ticketer-0.1.29.dist-info/licenses/LICENSE,sha256=KOVrunjtILSzY-2N8Lqa3-Q8dMaZIG4LrlLTr9UqL08,1073
45
+ mcp_ticketer-0.1.29.dist-info/METADATA,sha256=VRepQQpB6aMoS37BF5gdoNlnZBl2uQzks3qC-RyvxKk,13191
46
+ mcp_ticketer-0.1.29.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
47
+ mcp_ticketer-0.1.29.dist-info/entry_points.txt,sha256=o1IxVhnHnBNG7FZzbFq-Whcs1Djbofs0qMjiUYBLx2s,60
48
+ mcp_ticketer-0.1.29.dist-info/top_level.txt,sha256=WnAG4SOT1Vm9tIwl70AbGG_nA217YyV3aWFhxLH2rxw,13
49
+ mcp_ticketer-0.1.29.dist-info/RECORD,,