mcp-ticketer 0.1.28__py3-none-any.whl → 0.1.30__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.
- mcp_ticketer/__version__.py +1 -1
- mcp_ticketer/cli/diagnostics.py +579 -0
- mcp_ticketer/cli/main.py +43 -0
- mcp_ticketer/cli/simple_health.py +219 -0
- mcp_ticketer/mcp/server.py +217 -0
- {mcp_ticketer-0.1.28.dist-info → mcp_ticketer-0.1.30.dist-info}/METADATA +1 -1
- {mcp_ticketer-0.1.28.dist-info → mcp_ticketer-0.1.30.dist-info}/RECORD +11 -9
- {mcp_ticketer-0.1.28.dist-info → mcp_ticketer-0.1.30.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.1.28.dist-info → mcp_ticketer-0.1.30.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.1.28.dist-info → mcp_ticketer-0.1.30.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.1.28.dist-info → mcp_ticketer-0.1.30.dist-info}/top_level.txt +0 -0
mcp_ticketer/__version__.py
CHANGED
|
@@ -0,0 +1,579 @@
|
|
|
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
|
+
def safe_import_config():
|
|
19
|
+
"""Safely import configuration with fallback."""
|
|
20
|
+
try:
|
|
21
|
+
from ..core.config import get_config
|
|
22
|
+
return get_config
|
|
23
|
+
except ImportError:
|
|
24
|
+
# Create a minimal config fallback
|
|
25
|
+
class MockConfig:
|
|
26
|
+
def get_enabled_adapters(self):
|
|
27
|
+
return {}
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def default_adapter(self):
|
|
31
|
+
return "aitrackdown"
|
|
32
|
+
|
|
33
|
+
def get_config():
|
|
34
|
+
return MockConfig()
|
|
35
|
+
|
|
36
|
+
return get_config
|
|
37
|
+
|
|
38
|
+
def safe_import_registry():
|
|
39
|
+
"""Safely import adapter registry with fallback."""
|
|
40
|
+
try:
|
|
41
|
+
from ..core.registry import AdapterRegistry
|
|
42
|
+
return AdapterRegistry
|
|
43
|
+
except ImportError:
|
|
44
|
+
class MockRegistry:
|
|
45
|
+
@staticmethod
|
|
46
|
+
def get_adapter(adapter_type):
|
|
47
|
+
raise ImportError(f"Adapter {adapter_type} not available")
|
|
48
|
+
|
|
49
|
+
return MockRegistry
|
|
50
|
+
|
|
51
|
+
def safe_import_queue_manager():
|
|
52
|
+
"""Safely import queue manager with fallback."""
|
|
53
|
+
try:
|
|
54
|
+
from ..queue.manager import QueueManager
|
|
55
|
+
return QueueManager
|
|
56
|
+
except ImportError:
|
|
57
|
+
class MockQueueManager:
|
|
58
|
+
def get_worker_status(self):
|
|
59
|
+
return {"running": False, "pid": None}
|
|
60
|
+
|
|
61
|
+
def get_queue_stats(self):
|
|
62
|
+
return {"total": 0, "failed": 0}
|
|
63
|
+
|
|
64
|
+
return MockQueueManager
|
|
65
|
+
|
|
66
|
+
# Initialize with safe imports
|
|
67
|
+
get_config = safe_import_config()
|
|
68
|
+
AdapterRegistry = safe_import_registry()
|
|
69
|
+
QueueManager = safe_import_queue_manager()
|
|
70
|
+
|
|
71
|
+
console = Console()
|
|
72
|
+
logger = logging.getLogger(__name__)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class SystemDiagnostics:
|
|
76
|
+
"""Comprehensive system diagnostics and health reporting."""
|
|
77
|
+
|
|
78
|
+
def __init__(self):
|
|
79
|
+
# Initialize lists first
|
|
80
|
+
self.issues = []
|
|
81
|
+
self.warnings = []
|
|
82
|
+
self.successes = []
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
self.config = get_config()
|
|
86
|
+
# Check if this is a mock config
|
|
87
|
+
if hasattr(self.config, '__class__') and 'Mock' in self.config.__class__.__name__:
|
|
88
|
+
self.config_available = False
|
|
89
|
+
self.warnings.append("Configuration system using fallback mode")
|
|
90
|
+
else:
|
|
91
|
+
self.config_available = True
|
|
92
|
+
except Exception as e:
|
|
93
|
+
self.config = None
|
|
94
|
+
self.config_available = False
|
|
95
|
+
console.print(f"⚠️ Could not load configuration: {e}")
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
self.queue_manager = QueueManager()
|
|
99
|
+
# Check if this is a mock queue manager
|
|
100
|
+
if hasattr(self.queue_manager, '__class__') and 'Mock' in self.queue_manager.__class__.__name__:
|
|
101
|
+
self.queue_available = False
|
|
102
|
+
self.warnings.append("Queue system using fallback mode")
|
|
103
|
+
else:
|
|
104
|
+
self.queue_available = True
|
|
105
|
+
except Exception as e:
|
|
106
|
+
self.queue_manager = None
|
|
107
|
+
self.queue_available = False
|
|
108
|
+
console.print(f"⚠️ Could not initialize queue manager: {e}")
|
|
109
|
+
|
|
110
|
+
async def run_full_diagnosis(self) -> Dict[str, Any]:
|
|
111
|
+
"""Run complete system diagnosis and return detailed report."""
|
|
112
|
+
console.print("\n🔍 [bold blue]MCP Ticketer System Diagnosis[/bold blue]")
|
|
113
|
+
console.print("=" * 60)
|
|
114
|
+
|
|
115
|
+
report = {
|
|
116
|
+
"timestamp": datetime.now().isoformat(),
|
|
117
|
+
"version": self._get_version(),
|
|
118
|
+
"system_info": self._get_system_info(),
|
|
119
|
+
"configuration": await self._diagnose_configuration(),
|
|
120
|
+
"adapters": await self._diagnose_adapters(),
|
|
121
|
+
"queue_system": await self._diagnose_queue_system(),
|
|
122
|
+
"recent_logs": await self._analyze_recent_logs(),
|
|
123
|
+
"performance": await self._analyze_performance(),
|
|
124
|
+
"recommendations": self._generate_recommendations(),
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
self._display_diagnosis_summary(report)
|
|
128
|
+
return report
|
|
129
|
+
|
|
130
|
+
def _get_version(self) -> str:
|
|
131
|
+
"""Get current version information."""
|
|
132
|
+
try:
|
|
133
|
+
from ..__version__ import __version__
|
|
134
|
+
return __version__
|
|
135
|
+
except ImportError:
|
|
136
|
+
return "unknown"
|
|
137
|
+
|
|
138
|
+
def _get_system_info(self) -> Dict[str, Any]:
|
|
139
|
+
"""Gather system information."""
|
|
140
|
+
return {
|
|
141
|
+
"python_version": sys.version,
|
|
142
|
+
"platform": sys.platform,
|
|
143
|
+
"working_directory": str(Path.cwd()),
|
|
144
|
+
"config_path": str(self.config.config_file) if hasattr(self.config, 'config_file') else "unknown",
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async def _diagnose_configuration(self) -> Dict[str, Any]:
|
|
148
|
+
"""Diagnose configuration issues."""
|
|
149
|
+
console.print("\n📋 [yellow]Configuration Analysis[/yellow]")
|
|
150
|
+
|
|
151
|
+
config_status = {
|
|
152
|
+
"status": "healthy",
|
|
153
|
+
"adapters_configured": 0,
|
|
154
|
+
"default_adapter": None,
|
|
155
|
+
"issues": [],
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if not self.config:
|
|
159
|
+
issue = "Configuration system not available"
|
|
160
|
+
config_status["issues"].append(issue)
|
|
161
|
+
config_status["status"] = "critical"
|
|
162
|
+
self.issues.append(issue)
|
|
163
|
+
console.print(f"❌ {issue}")
|
|
164
|
+
return config_status
|
|
165
|
+
|
|
166
|
+
if not self.config_available:
|
|
167
|
+
warning = "Configuration system in fallback mode - limited functionality"
|
|
168
|
+
config_status["issues"].append(warning)
|
|
169
|
+
config_status["status"] = "degraded"
|
|
170
|
+
self.warnings.append(warning)
|
|
171
|
+
console.print(f"⚠️ {warning}")
|
|
172
|
+
|
|
173
|
+
# Try to detect adapters from environment variables
|
|
174
|
+
import os
|
|
175
|
+
env_adapters = []
|
|
176
|
+
if os.getenv("LINEAR_API_KEY"):
|
|
177
|
+
env_adapters.append("linear")
|
|
178
|
+
if os.getenv("GITHUB_TOKEN"):
|
|
179
|
+
env_adapters.append("github")
|
|
180
|
+
if os.getenv("JIRA_SERVER"):
|
|
181
|
+
env_adapters.append("jira")
|
|
182
|
+
|
|
183
|
+
config_status["adapters_configured"] = len(env_adapters)
|
|
184
|
+
config_status["default_adapter"] = "aitrackdown"
|
|
185
|
+
|
|
186
|
+
if env_adapters:
|
|
187
|
+
console.print(f"ℹ️ Detected {len(env_adapters)} adapter(s) from environment: {', '.join(env_adapters)}")
|
|
188
|
+
else:
|
|
189
|
+
console.print("ℹ️ No adapter environment variables detected, using aitrackdown")
|
|
190
|
+
|
|
191
|
+
return config_status
|
|
192
|
+
|
|
193
|
+
try:
|
|
194
|
+
# Check adapter configurations
|
|
195
|
+
adapters = self.config.get_enabled_adapters()
|
|
196
|
+
config_status["adapters_configured"] = len(adapters)
|
|
197
|
+
config_status["default_adapter"] = self.config.default_adapter
|
|
198
|
+
|
|
199
|
+
if not adapters:
|
|
200
|
+
issue = "No adapters configured"
|
|
201
|
+
config_status["issues"].append(issue)
|
|
202
|
+
config_status["status"] = "critical"
|
|
203
|
+
self.issues.append(issue)
|
|
204
|
+
console.print(f"❌ {issue}")
|
|
205
|
+
else:
|
|
206
|
+
console.print(f"✅ {len(adapters)} adapter(s) configured")
|
|
207
|
+
|
|
208
|
+
# Check each adapter configuration
|
|
209
|
+
for name, adapter_config in adapters.items():
|
|
210
|
+
try:
|
|
211
|
+
adapter_class = AdapterRegistry.get_adapter(adapter_config.type.value)
|
|
212
|
+
adapter = adapter_class(adapter_config.dict())
|
|
213
|
+
|
|
214
|
+
# Test adapter validation if available
|
|
215
|
+
if hasattr(adapter, 'validate_credentials'):
|
|
216
|
+
is_valid, error = adapter.validate_credentials()
|
|
217
|
+
if is_valid:
|
|
218
|
+
console.print(f"✅ {name}: credentials valid")
|
|
219
|
+
self.successes.append(f"{name} adapter configured correctly")
|
|
220
|
+
else:
|
|
221
|
+
issue = f"{name}: credential validation failed - {error}"
|
|
222
|
+
config_status["issues"].append(issue)
|
|
223
|
+
self.warnings.append(issue)
|
|
224
|
+
console.print(f"⚠️ {issue}")
|
|
225
|
+
else:
|
|
226
|
+
console.print(f"ℹ️ {name}: no credential validation available")
|
|
227
|
+
|
|
228
|
+
except Exception as e:
|
|
229
|
+
issue = f"{name}: configuration error - {str(e)}"
|
|
230
|
+
config_status["issues"].append(issue)
|
|
231
|
+
self.issues.append(issue)
|
|
232
|
+
console.print(f"❌ {issue}")
|
|
233
|
+
|
|
234
|
+
except Exception as e:
|
|
235
|
+
issue = f"Configuration loading failed: {str(e)}"
|
|
236
|
+
config_status["issues"].append(issue)
|
|
237
|
+
config_status["status"] = "critical"
|
|
238
|
+
self.issues.append(issue)
|
|
239
|
+
console.print(f"❌ {issue}")
|
|
240
|
+
|
|
241
|
+
return config_status
|
|
242
|
+
|
|
243
|
+
async def _diagnose_adapters(self) -> Dict[str, Any]:
|
|
244
|
+
"""Diagnose adapter functionality."""
|
|
245
|
+
console.print("\n🔌 [yellow]Adapter Diagnosis[/yellow]")
|
|
246
|
+
|
|
247
|
+
adapter_status = {
|
|
248
|
+
"total_adapters": 0,
|
|
249
|
+
"healthy_adapters": 0,
|
|
250
|
+
"failed_adapters": 0,
|
|
251
|
+
"adapter_details": {},
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
try:
|
|
255
|
+
adapters = self.config.get_enabled_adapters()
|
|
256
|
+
adapter_status["total_adapters"] = len(adapters)
|
|
257
|
+
|
|
258
|
+
for name, adapter_config in adapters.items():
|
|
259
|
+
details = {
|
|
260
|
+
"type": adapter_config.type.value,
|
|
261
|
+
"status": "unknown",
|
|
262
|
+
"last_test": None,
|
|
263
|
+
"error": None,
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
try:
|
|
267
|
+
adapter_class = AdapterRegistry.get_adapter(adapter_config.type.value)
|
|
268
|
+
adapter = adapter_class(adapter_config.dict())
|
|
269
|
+
|
|
270
|
+
# Test basic adapter functionality
|
|
271
|
+
test_start = datetime.now()
|
|
272
|
+
|
|
273
|
+
# Try to list tickets (non-destructive test)
|
|
274
|
+
try:
|
|
275
|
+
await adapter.list(limit=1)
|
|
276
|
+
details["status"] = "healthy"
|
|
277
|
+
details["last_test"] = test_start.isoformat()
|
|
278
|
+
adapter_status["healthy_adapters"] += 1
|
|
279
|
+
console.print(f"✅ {name}: operational")
|
|
280
|
+
except Exception as e:
|
|
281
|
+
details["status"] = "failed"
|
|
282
|
+
details["error"] = str(e)
|
|
283
|
+
adapter_status["failed_adapters"] += 1
|
|
284
|
+
issue = f"{name}: functionality test failed - {str(e)}"
|
|
285
|
+
self.issues.append(issue)
|
|
286
|
+
console.print(f"❌ {issue}")
|
|
287
|
+
|
|
288
|
+
except Exception as e:
|
|
289
|
+
details["status"] = "failed"
|
|
290
|
+
details["error"] = str(e)
|
|
291
|
+
adapter_status["failed_adapters"] += 1
|
|
292
|
+
issue = f"{name}: initialization failed - {str(e)}"
|
|
293
|
+
self.issues.append(issue)
|
|
294
|
+
console.print(f"❌ {issue}")
|
|
295
|
+
|
|
296
|
+
adapter_status["adapter_details"][name] = details
|
|
297
|
+
|
|
298
|
+
except Exception as e:
|
|
299
|
+
issue = f"Adapter diagnosis failed: {str(e)}"
|
|
300
|
+
self.issues.append(issue)
|
|
301
|
+
console.print(f"❌ {issue}")
|
|
302
|
+
|
|
303
|
+
return adapter_status
|
|
304
|
+
|
|
305
|
+
async def _diagnose_queue_system(self) -> Dict[str, Any]:
|
|
306
|
+
"""Diagnose queue system health."""
|
|
307
|
+
console.print("\n⚡ [yellow]Queue System Diagnosis[/yellow]")
|
|
308
|
+
|
|
309
|
+
queue_status = {
|
|
310
|
+
"worker_running": False,
|
|
311
|
+
"worker_pid": None,
|
|
312
|
+
"queue_stats": {},
|
|
313
|
+
"recent_failures": [],
|
|
314
|
+
"failure_rate": 0.0,
|
|
315
|
+
"health_score": 0,
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
try:
|
|
319
|
+
if not self.queue_available:
|
|
320
|
+
warning = "Queue system in fallback mode - limited functionality"
|
|
321
|
+
self.warnings.append(warning)
|
|
322
|
+
console.print(f"⚠️ {warning}")
|
|
323
|
+
queue_status["worker_running"] = False
|
|
324
|
+
queue_status["worker_pid"] = None
|
|
325
|
+
queue_status["health_score"] = 50 # Degraded but not critical
|
|
326
|
+
return queue_status
|
|
327
|
+
|
|
328
|
+
# Check worker status
|
|
329
|
+
worker_status = self.queue_manager.get_worker_status()
|
|
330
|
+
queue_status["worker_running"] = worker_status.get("running", False)
|
|
331
|
+
queue_status["worker_pid"] = worker_status.get("pid")
|
|
332
|
+
|
|
333
|
+
if queue_status["worker_running"]:
|
|
334
|
+
console.print(f"✅ Queue worker running (PID: {queue_status['worker_pid']})")
|
|
335
|
+
self.successes.append("Queue worker is running")
|
|
336
|
+
else:
|
|
337
|
+
issue = "Queue worker not running"
|
|
338
|
+
self.issues.append(issue)
|
|
339
|
+
console.print(f"❌ {issue}")
|
|
340
|
+
|
|
341
|
+
# Get queue statistics
|
|
342
|
+
stats = self.queue_manager.get_queue_stats()
|
|
343
|
+
queue_status["queue_stats"] = stats
|
|
344
|
+
|
|
345
|
+
total_items = stats.get("total", 0)
|
|
346
|
+
failed_items = stats.get("failed", 0)
|
|
347
|
+
|
|
348
|
+
if total_items > 0:
|
|
349
|
+
failure_rate = (failed_items / total_items) * 100
|
|
350
|
+
queue_status["failure_rate"] = failure_rate
|
|
351
|
+
|
|
352
|
+
if failure_rate > 50:
|
|
353
|
+
issue = f"High failure rate: {failure_rate:.1f}% ({failed_items}/{total_items})"
|
|
354
|
+
self.issues.append(issue)
|
|
355
|
+
console.print(f"❌ {issue}")
|
|
356
|
+
elif failure_rate > 20:
|
|
357
|
+
warning = f"Elevated failure rate: {failure_rate:.1f}% ({failed_items}/{total_items})"
|
|
358
|
+
self.warnings.append(warning)
|
|
359
|
+
console.print(f"⚠️ {warning}")
|
|
360
|
+
else:
|
|
361
|
+
console.print(f"✅ Queue failure rate: {failure_rate:.1f}% ({failed_items}/{total_items})")
|
|
362
|
+
|
|
363
|
+
# Calculate health score
|
|
364
|
+
health_score = 100
|
|
365
|
+
if not queue_status["worker_running"]:
|
|
366
|
+
health_score -= 50
|
|
367
|
+
health_score -= min(queue_status["failure_rate"], 50)
|
|
368
|
+
queue_status["health_score"] = max(0, health_score)
|
|
369
|
+
|
|
370
|
+
console.print(f"📊 Queue health score: {queue_status['health_score']}/100")
|
|
371
|
+
|
|
372
|
+
except Exception as e:
|
|
373
|
+
issue = f"Queue system diagnosis failed: {str(e)}"
|
|
374
|
+
self.issues.append(issue)
|
|
375
|
+
console.print(f"❌ {issue}")
|
|
376
|
+
|
|
377
|
+
return queue_status
|
|
378
|
+
|
|
379
|
+
async def _analyze_recent_logs(self) -> Dict[str, Any]:
|
|
380
|
+
"""Analyze recent log entries for issues."""
|
|
381
|
+
console.print("\n📝 [yellow]Recent Log Analysis[/yellow]")
|
|
382
|
+
|
|
383
|
+
log_analysis = {
|
|
384
|
+
"log_files_found": [],
|
|
385
|
+
"recent_errors": [],
|
|
386
|
+
"recent_warnings": [],
|
|
387
|
+
"patterns": {},
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
try:
|
|
391
|
+
# Look for common log locations
|
|
392
|
+
log_paths = [
|
|
393
|
+
Path.home() / ".mcp-ticketer" / "logs",
|
|
394
|
+
Path.cwd() / ".mcp-ticketer" / "logs",
|
|
395
|
+
Path("/var/log/mcp-ticketer"),
|
|
396
|
+
]
|
|
397
|
+
|
|
398
|
+
for log_path in log_paths:
|
|
399
|
+
if log_path.exists():
|
|
400
|
+
log_analysis["log_files_found"].append(str(log_path))
|
|
401
|
+
await self._analyze_log_directory(log_path, log_analysis)
|
|
402
|
+
|
|
403
|
+
if not log_analysis["log_files_found"]:
|
|
404
|
+
console.print("ℹ️ No log files found in standard locations")
|
|
405
|
+
else:
|
|
406
|
+
console.print(f"✅ Found logs in {len(log_analysis['log_files_found'])} location(s)")
|
|
407
|
+
|
|
408
|
+
except Exception as e:
|
|
409
|
+
issue = f"Log analysis failed: {str(e)}"
|
|
410
|
+
self.issues.append(issue)
|
|
411
|
+
console.print(f"❌ {issue}")
|
|
412
|
+
|
|
413
|
+
return log_analysis
|
|
414
|
+
|
|
415
|
+
async def _analyze_log_directory(self, log_path: Path, log_analysis: Dict[str, Any]):
|
|
416
|
+
"""Analyze logs in a specific directory."""
|
|
417
|
+
try:
|
|
418
|
+
for log_file in log_path.glob("*.log"):
|
|
419
|
+
if log_file.stat().st_mtime > (datetime.now() - timedelta(hours=24)).timestamp():
|
|
420
|
+
await self._parse_log_file(log_file, log_analysis)
|
|
421
|
+
except Exception as e:
|
|
422
|
+
self.warnings.append(f"Could not analyze logs in {log_path}: {str(e)}")
|
|
423
|
+
|
|
424
|
+
async def _parse_log_file(self, log_file: Path, log_analysis: Dict[str, Any]):
|
|
425
|
+
"""Parse individual log file for issues."""
|
|
426
|
+
try:
|
|
427
|
+
with open(log_file, 'r') as f:
|
|
428
|
+
lines = f.readlines()[-100:] # Last 100 lines
|
|
429
|
+
|
|
430
|
+
for line in lines:
|
|
431
|
+
if "ERROR" in line:
|
|
432
|
+
log_analysis["recent_errors"].append(line.strip())
|
|
433
|
+
elif "WARNING" in line:
|
|
434
|
+
log_analysis["recent_warnings"].append(line.strip())
|
|
435
|
+
|
|
436
|
+
except Exception as e:
|
|
437
|
+
self.warnings.append(f"Could not parse {log_file}: {str(e)}")
|
|
438
|
+
|
|
439
|
+
async def _analyze_performance(self) -> Dict[str, Any]:
|
|
440
|
+
"""Analyze system performance metrics."""
|
|
441
|
+
console.print("\n⚡ [yellow]Performance Analysis[/yellow]")
|
|
442
|
+
|
|
443
|
+
performance = {
|
|
444
|
+
"response_times": {},
|
|
445
|
+
"throughput": {},
|
|
446
|
+
"resource_usage": {},
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
try:
|
|
450
|
+
# Test basic operations performance
|
|
451
|
+
start_time = datetime.now()
|
|
452
|
+
|
|
453
|
+
# Test configuration loading
|
|
454
|
+
config_start = datetime.now()
|
|
455
|
+
_ = get_config()
|
|
456
|
+
config_time = (datetime.now() - config_start).total_seconds()
|
|
457
|
+
performance["response_times"]["config_load"] = config_time
|
|
458
|
+
|
|
459
|
+
if config_time > 1.0:
|
|
460
|
+
self.warnings.append(f"Slow configuration loading: {config_time:.2f}s")
|
|
461
|
+
|
|
462
|
+
console.print(f"📊 Configuration load time: {config_time:.3f}s")
|
|
463
|
+
|
|
464
|
+
except Exception as e:
|
|
465
|
+
issue = f"Performance analysis failed: {str(e)}"
|
|
466
|
+
self.issues.append(issue)
|
|
467
|
+
console.print(f"❌ {issue}")
|
|
468
|
+
|
|
469
|
+
return performance
|
|
470
|
+
|
|
471
|
+
def _generate_recommendations(self) -> List[str]:
|
|
472
|
+
"""Generate actionable recommendations based on diagnosis."""
|
|
473
|
+
recommendations = []
|
|
474
|
+
|
|
475
|
+
if self.issues:
|
|
476
|
+
recommendations.append("🚨 Critical issues detected - immediate attention required")
|
|
477
|
+
|
|
478
|
+
if any("Queue worker not running" in issue for issue in self.issues):
|
|
479
|
+
recommendations.append("• Restart queue worker: mcp-ticketer queue worker restart")
|
|
480
|
+
|
|
481
|
+
if any("failure rate" in issue.lower() for issue in self.issues):
|
|
482
|
+
recommendations.append("• Check queue system logs for error patterns")
|
|
483
|
+
recommendations.append("• Consider clearing failed queue items: mcp-ticketer queue clear --failed")
|
|
484
|
+
|
|
485
|
+
if any("No adapters configured" in issue for issue in self.issues):
|
|
486
|
+
recommendations.append("• Configure at least one adapter: mcp-ticketer init-aitrackdown")
|
|
487
|
+
|
|
488
|
+
if self.warnings:
|
|
489
|
+
recommendations.append("⚠️ Warnings detected - monitoring recommended")
|
|
490
|
+
|
|
491
|
+
if not self.issues and not self.warnings:
|
|
492
|
+
recommendations.append("✅ System appears healthy - no immediate action required")
|
|
493
|
+
|
|
494
|
+
return recommendations
|
|
495
|
+
|
|
496
|
+
def _display_diagnosis_summary(self, report: Dict[str, Any]):
|
|
497
|
+
"""Display a comprehensive diagnosis summary."""
|
|
498
|
+
console.print("\n" + "=" * 60)
|
|
499
|
+
console.print("📋 [bold green]DIAGNOSIS SUMMARY[/bold green]")
|
|
500
|
+
console.print("=" * 60)
|
|
501
|
+
|
|
502
|
+
# Overall health status
|
|
503
|
+
if self.issues:
|
|
504
|
+
status_color = "red"
|
|
505
|
+
status_text = "CRITICAL"
|
|
506
|
+
status_icon = "🚨"
|
|
507
|
+
elif self.warnings:
|
|
508
|
+
status_color = "yellow"
|
|
509
|
+
status_text = "WARNING"
|
|
510
|
+
status_icon = "⚠️"
|
|
511
|
+
else:
|
|
512
|
+
status_color = "green"
|
|
513
|
+
status_text = "HEALTHY"
|
|
514
|
+
status_icon = "✅"
|
|
515
|
+
|
|
516
|
+
console.print(f"\n{status_icon} [bold {status_color}]System Status: {status_text}[/bold {status_color}]")
|
|
517
|
+
|
|
518
|
+
# Statistics
|
|
519
|
+
stats_table = Table(show_header=True, header_style="bold blue")
|
|
520
|
+
stats_table.add_column("Component")
|
|
521
|
+
stats_table.add_column("Status")
|
|
522
|
+
stats_table.add_column("Details")
|
|
523
|
+
|
|
524
|
+
# Add component statuses
|
|
525
|
+
config_status = "✅ OK" if not any("configuration" in issue.lower() for issue in self.issues) else "❌ FAILED"
|
|
526
|
+
stats_table.add_row("Configuration", config_status, f"{report['configuration']['adapters_configured']} adapters")
|
|
527
|
+
|
|
528
|
+
queue_health = report['queue_system']['health_score']
|
|
529
|
+
queue_status = "✅ OK" if queue_health > 80 else "⚠️ DEGRADED" if queue_health > 50 else "❌ FAILED"
|
|
530
|
+
stats_table.add_row("Queue System", queue_status, f"{queue_health}/100 health score")
|
|
531
|
+
|
|
532
|
+
adapter_stats = report['adapters']
|
|
533
|
+
adapter_status = "✅ OK" if adapter_stats['failed_adapters'] == 0 else "❌ FAILED"
|
|
534
|
+
stats_table.add_row("Adapters", adapter_status, f"{adapter_stats['healthy_adapters']}/{adapter_stats['total_adapters']} healthy")
|
|
535
|
+
|
|
536
|
+
console.print(stats_table)
|
|
537
|
+
|
|
538
|
+
# Issues and recommendations
|
|
539
|
+
if self.issues:
|
|
540
|
+
console.print(f"\n🚨 [bold red]Critical Issues ({len(self.issues)}):[/bold red]")
|
|
541
|
+
for issue in self.issues:
|
|
542
|
+
console.print(f" • {issue}")
|
|
543
|
+
|
|
544
|
+
if self.warnings:
|
|
545
|
+
console.print(f"\n⚠️ [bold yellow]Warnings ({len(self.warnings)}):[/bold yellow]")
|
|
546
|
+
for warning in self.warnings:
|
|
547
|
+
console.print(f" • {warning}")
|
|
548
|
+
|
|
549
|
+
if report['recommendations']:
|
|
550
|
+
console.print(f"\n💡 [bold blue]Recommendations:[/bold blue]")
|
|
551
|
+
for rec in report['recommendations']:
|
|
552
|
+
console.print(f" {rec}")
|
|
553
|
+
|
|
554
|
+
console.print(f"\n📊 [bold]Summary:[/bold] {len(self.successes)} successes, {len(self.warnings)} warnings, {len(self.issues)} critical issues")
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
async def run_diagnostics(
|
|
558
|
+
output_file: Optional[str] = None,
|
|
559
|
+
json_output: bool = False,
|
|
560
|
+
) -> None:
|
|
561
|
+
"""Run comprehensive system diagnostics."""
|
|
562
|
+
diagnostics = SystemDiagnostics()
|
|
563
|
+
report = await diagnostics.run_full_diagnosis()
|
|
564
|
+
|
|
565
|
+
if output_file:
|
|
566
|
+
with open(output_file, 'w') as f:
|
|
567
|
+
json.dump(report, f, indent=2)
|
|
568
|
+
console.print(f"\n📄 Full report saved to: {output_file}")
|
|
569
|
+
|
|
570
|
+
if json_output:
|
|
571
|
+
console.print("\n" + json.dumps(report, indent=2))
|
|
572
|
+
|
|
573
|
+
# Return exit code based on issues
|
|
574
|
+
if diagnostics.issues:
|
|
575
|
+
raise typer.Exit(1) # Critical issues found
|
|
576
|
+
elif diagnostics.warnings:
|
|
577
|
+
raise typer.Exit(2) # Warnings found
|
|
578
|
+
else:
|
|
579
|
+
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
|
mcp_ticketer/mcp/server.py
CHANGED
|
@@ -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.
|
|
3
|
+
Version: 0.1.30
|
|
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=
|
|
2
|
+
mcp_ticketer/__version__.py,sha256=y7yURZVuwtSStGlJ4RI4JrAK3qRSBtOqe64wfkDPASY,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=edTmM0OEDNiMYTvQ56HK304EUNUBtYejMaKmpJTpomY,22821
|
|
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=
|
|
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=
|
|
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.
|
|
43
|
-
mcp_ticketer-0.1.
|
|
44
|
-
mcp_ticketer-0.1.
|
|
45
|
-
mcp_ticketer-0.1.
|
|
46
|
-
mcp_ticketer-0.1.
|
|
47
|
-
mcp_ticketer-0.1.
|
|
44
|
+
mcp_ticketer-0.1.30.dist-info/licenses/LICENSE,sha256=KOVrunjtILSzY-2N8Lqa3-Q8dMaZIG4LrlLTr9UqL08,1073
|
|
45
|
+
mcp_ticketer-0.1.30.dist-info/METADATA,sha256=11bfR6cQYqZexr0tCmf4Tvw-vE0A5A1oQmsrK4kqFMM,13191
|
|
46
|
+
mcp_ticketer-0.1.30.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
47
|
+
mcp_ticketer-0.1.30.dist-info/entry_points.txt,sha256=o1IxVhnHnBNG7FZzbFq-Whcs1Djbofs0qMjiUYBLx2s,60
|
|
48
|
+
mcp_ticketer-0.1.30.dist-info/top_level.txt,sha256=WnAG4SOT1Vm9tIwl70AbGG_nA217YyV3aWFhxLH2rxw,13
|
|
49
|
+
mcp_ticketer-0.1.30.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|