mcp-ticketer 0.1.27__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.
- mcp_ticketer/__version__.py +1 -1
- mcp_ticketer/adapters/aitrackdown.py +58 -0
- mcp_ticketer/adapters/linear.py +60 -0
- mcp_ticketer/cli/diagnostics.py +492 -0
- mcp_ticketer/cli/main.py +43 -0
- mcp_ticketer/cli/simple_health.py +219 -0
- mcp_ticketer/core/config.py +28 -21
- mcp_ticketer/mcp/server.py +217 -0
- {mcp_ticketer-0.1.27.dist-info → mcp_ticketer-0.1.29.dist-info}/METADATA +1 -1
- {mcp_ticketer-0.1.27.dist-info → mcp_ticketer-0.1.29.dist-info}/RECORD +14 -12
- {mcp_ticketer-0.1.27.dist-info → mcp_ticketer-0.1.29.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.1.27.dist-info → mcp_ticketer-0.1.29.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.1.27.dist-info → mcp_ticketer-0.1.29.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.1.27.dist-info → mcp_ticketer-0.1.29.dist-info}/top_level.txt +0 -0
mcp_ticketer/__version__.py
CHANGED
|
@@ -235,6 +235,64 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
235
235
|
|
|
236
236
|
return ticket
|
|
237
237
|
|
|
238
|
+
async def create_epic(self, title: str, description: str = None, **kwargs) -> Epic:
|
|
239
|
+
"""Create a new epic.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
title: Epic title
|
|
243
|
+
description: Epic description
|
|
244
|
+
**kwargs: Additional epic properties
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
Created Epic instance
|
|
248
|
+
"""
|
|
249
|
+
epic = Epic(
|
|
250
|
+
title=title,
|
|
251
|
+
description=description,
|
|
252
|
+
**kwargs
|
|
253
|
+
)
|
|
254
|
+
return await self.create(epic)
|
|
255
|
+
|
|
256
|
+
async def create_issue(self, title: str, parent_epic: str = None, description: str = None, **kwargs) -> Task:
|
|
257
|
+
"""Create a new issue.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
title: Issue title
|
|
261
|
+
parent_epic: Parent epic ID
|
|
262
|
+
description: Issue description
|
|
263
|
+
**kwargs: Additional issue properties
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
Created Task instance (representing an issue)
|
|
267
|
+
"""
|
|
268
|
+
task = Task(
|
|
269
|
+
title=title,
|
|
270
|
+
description=description,
|
|
271
|
+
parent_epic=parent_epic,
|
|
272
|
+
**kwargs
|
|
273
|
+
)
|
|
274
|
+
return await self.create(task)
|
|
275
|
+
|
|
276
|
+
async def create_task(self, title: str, parent_id: str, description: str = None, **kwargs) -> Task:
|
|
277
|
+
"""Create a new task under an issue.
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
title: Task title
|
|
281
|
+
parent_id: Parent issue ID
|
|
282
|
+
description: Task description
|
|
283
|
+
**kwargs: Additional task properties
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
Created Task instance
|
|
287
|
+
"""
|
|
288
|
+
task = Task(
|
|
289
|
+
title=title,
|
|
290
|
+
description=description,
|
|
291
|
+
parent_issue=parent_id,
|
|
292
|
+
**kwargs
|
|
293
|
+
)
|
|
294
|
+
return await self.create(task)
|
|
295
|
+
|
|
238
296
|
async def read(self, ticket_id: str) -> Optional[Union[Task, Epic]]:
|
|
239
297
|
"""Read a task by ID."""
|
|
240
298
|
if self.tracker:
|
mcp_ticketer/adapters/linear.py
CHANGED
|
@@ -973,6 +973,66 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
973
973
|
created_issue = result["issueCreate"]["issue"]
|
|
974
974
|
return self._task_from_linear_issue(created_issue)
|
|
975
975
|
|
|
976
|
+
async def create_epic(self, title: str, description: str = None, **kwargs) -> Task:
|
|
977
|
+
"""Create a new epic (Linear project).
|
|
978
|
+
|
|
979
|
+
Args:
|
|
980
|
+
title: Epic title
|
|
981
|
+
description: Epic description
|
|
982
|
+
**kwargs: Additional epic properties
|
|
983
|
+
|
|
984
|
+
Returns:
|
|
985
|
+
Created Task instance representing the epic
|
|
986
|
+
"""
|
|
987
|
+
# In Linear, epics are represented as issues with special labels/properties
|
|
988
|
+
task = Task(
|
|
989
|
+
title=title,
|
|
990
|
+
description=description,
|
|
991
|
+
tags=kwargs.get('tags', []) + ['epic'], # Add epic tag
|
|
992
|
+
**{k: v for k, v in kwargs.items() if k != 'tags'}
|
|
993
|
+
)
|
|
994
|
+
return await self.create(task)
|
|
995
|
+
|
|
996
|
+
async def create_issue(self, title: str, parent_epic: str = None, description: str = None, **kwargs) -> Task:
|
|
997
|
+
"""Create a new issue.
|
|
998
|
+
|
|
999
|
+
Args:
|
|
1000
|
+
title: Issue title
|
|
1001
|
+
parent_epic: Parent epic ID
|
|
1002
|
+
description: Issue description
|
|
1003
|
+
**kwargs: Additional issue properties
|
|
1004
|
+
|
|
1005
|
+
Returns:
|
|
1006
|
+
Created Task instance representing the issue
|
|
1007
|
+
"""
|
|
1008
|
+
task = Task(
|
|
1009
|
+
title=title,
|
|
1010
|
+
description=description,
|
|
1011
|
+
parent_epic=parent_epic,
|
|
1012
|
+
**kwargs
|
|
1013
|
+
)
|
|
1014
|
+
return await self.create(task)
|
|
1015
|
+
|
|
1016
|
+
async def create_task(self, title: str, parent_id: str, description: str = None, **kwargs) -> Task:
|
|
1017
|
+
"""Create a new task under an issue.
|
|
1018
|
+
|
|
1019
|
+
Args:
|
|
1020
|
+
title: Task title
|
|
1021
|
+
parent_id: Parent issue ID
|
|
1022
|
+
description: Task description
|
|
1023
|
+
**kwargs: Additional task properties
|
|
1024
|
+
|
|
1025
|
+
Returns:
|
|
1026
|
+
Created Task instance
|
|
1027
|
+
"""
|
|
1028
|
+
task = Task(
|
|
1029
|
+
title=title,
|
|
1030
|
+
description=description,
|
|
1031
|
+
parent_issue=parent_id,
|
|
1032
|
+
**kwargs
|
|
1033
|
+
)
|
|
1034
|
+
return await self.create(task)
|
|
1035
|
+
|
|
976
1036
|
async def read(self, ticket_id: str) -> Optional[Task]:
|
|
977
1037
|
"""Read a Linear issue by identifier with full details."""
|
|
978
1038
|
# Validate credentials before attempting operation
|
|
@@ -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
|
mcp_ticketer/core/config.py
CHANGED
|
@@ -9,7 +9,7 @@ from pathlib import Path
|
|
|
9
9
|
from typing import Any, Optional, Union
|
|
10
10
|
|
|
11
11
|
import yaml
|
|
12
|
-
from pydantic import BaseModel, Field,
|
|
12
|
+
from pydantic import BaseModel, Field, field_validator, model_validator
|
|
13
13
|
|
|
14
14
|
logger = logging.getLogger(__name__)
|
|
15
15
|
|
|
@@ -45,24 +45,27 @@ class GitHubConfig(BaseAdapterConfig):
|
|
|
45
45
|
use_projects_v2: bool = False
|
|
46
46
|
custom_priority_scheme: Optional[dict[str, list[str]]] = None
|
|
47
47
|
|
|
48
|
-
@
|
|
49
|
-
|
|
48
|
+
@field_validator("token", mode="before")
|
|
49
|
+
@classmethod
|
|
50
|
+
def validate_token(cls, v):
|
|
50
51
|
if not v:
|
|
51
52
|
v = os.getenv("GITHUB_TOKEN")
|
|
52
53
|
if not v:
|
|
53
54
|
raise ValueError("GitHub token is required")
|
|
54
55
|
return v
|
|
55
56
|
|
|
56
|
-
@
|
|
57
|
-
|
|
57
|
+
@field_validator("owner", mode="before")
|
|
58
|
+
@classmethod
|
|
59
|
+
def validate_owner(cls, v):
|
|
58
60
|
if not v:
|
|
59
61
|
v = os.getenv("GITHUB_OWNER")
|
|
60
62
|
if not v:
|
|
61
63
|
raise ValueError("GitHub owner is required")
|
|
62
64
|
return v
|
|
63
65
|
|
|
64
|
-
@
|
|
65
|
-
|
|
66
|
+
@field_validator("repo", mode="before")
|
|
67
|
+
@classmethod
|
|
68
|
+
def validate_repo(cls, v):
|
|
66
69
|
if not v:
|
|
67
70
|
v = os.getenv("GITHUB_REPO")
|
|
68
71
|
if not v:
|
|
@@ -81,24 +84,27 @@ class JiraConfig(BaseAdapterConfig):
|
|
|
81
84
|
cloud: bool = True
|
|
82
85
|
verify_ssl: bool = True
|
|
83
86
|
|
|
84
|
-
@
|
|
85
|
-
|
|
87
|
+
@field_validator("server", mode="before")
|
|
88
|
+
@classmethod
|
|
89
|
+
def validate_server(cls, v):
|
|
86
90
|
if not v:
|
|
87
91
|
v = os.getenv("JIRA_SERVER")
|
|
88
92
|
if not v:
|
|
89
93
|
raise ValueError("JIRA server URL is required")
|
|
90
94
|
return v.rstrip("/")
|
|
91
95
|
|
|
92
|
-
@
|
|
93
|
-
|
|
96
|
+
@field_validator("email", mode="before")
|
|
97
|
+
@classmethod
|
|
98
|
+
def validate_email(cls, v):
|
|
94
99
|
if not v:
|
|
95
100
|
v = os.getenv("JIRA_EMAIL")
|
|
96
101
|
if not v:
|
|
97
102
|
raise ValueError("JIRA email is required")
|
|
98
103
|
return v
|
|
99
104
|
|
|
100
|
-
@
|
|
101
|
-
|
|
105
|
+
@field_validator("api_token", mode="before")
|
|
106
|
+
@classmethod
|
|
107
|
+
def validate_api_token(cls, v):
|
|
102
108
|
if not v:
|
|
103
109
|
v = os.getenv("JIRA_API_TOKEN")
|
|
104
110
|
if not v:
|
|
@@ -115,8 +121,9 @@ class LinearConfig(BaseAdapterConfig):
|
|
|
115
121
|
team_key: str
|
|
116
122
|
api_url: str = "https://api.linear.app/graphql"
|
|
117
123
|
|
|
118
|
-
@
|
|
119
|
-
|
|
124
|
+
@field_validator("api_key", mode="before")
|
|
125
|
+
@classmethod
|
|
126
|
+
def validate_api_key(cls, v):
|
|
120
127
|
if not v:
|
|
121
128
|
v = os.getenv("LINEAR_API_KEY")
|
|
122
129
|
if not v:
|
|
@@ -163,23 +170,23 @@ class AppConfig(BaseModel):
|
|
|
163
170
|
cache_ttl: int = 300 # Cache TTL in seconds
|
|
164
171
|
default_adapter: Optional[str] = None
|
|
165
172
|
|
|
166
|
-
@
|
|
167
|
-
def validate_adapters(self
|
|
173
|
+
@model_validator(mode="after")
|
|
174
|
+
def validate_adapters(self):
|
|
168
175
|
"""Validate adapter configurations."""
|
|
169
|
-
adapters =
|
|
176
|
+
adapters = self.adapters
|
|
170
177
|
|
|
171
178
|
if not adapters:
|
|
172
179
|
logger.warning("No adapters configured")
|
|
173
|
-
return
|
|
180
|
+
return self
|
|
174
181
|
|
|
175
182
|
# Validate default adapter
|
|
176
|
-
default_adapter =
|
|
183
|
+
default_adapter = self.default_adapter
|
|
177
184
|
if default_adapter and default_adapter not in adapters:
|
|
178
185
|
raise ValueError(
|
|
179
186
|
f"Default adapter '{default_adapter}' not found in adapters"
|
|
180
187
|
)
|
|
181
188
|
|
|
182
|
-
return
|
|
189
|
+
return self
|
|
183
190
|
|
|
184
191
|
def get_adapter_config(self, adapter_name: str) -> Optional[BaseAdapterConfig]:
|
|
185
192
|
"""Get configuration for a specific adapter."""
|
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.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,28 +1,30 @@
|
|
|
1
1
|
mcp_ticketer/__init__.py,sha256=Xx4WaprO5PXhVPbYi1L6tBmwmJMkYS-lMyG4ieN6QP0,717
|
|
2
|
-
mcp_ticketer/__version__.py,sha256=
|
|
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
|
-
mcp_ticketer/adapters/aitrackdown.py,sha256=
|
|
5
|
+
mcp_ticketer/adapters/aitrackdown.py,sha256=stlbge8K6w-EyQkw_vEQNSXQgCOWN5tOlQUgGWZQNMQ,17936
|
|
6
6
|
mcp_ticketer/adapters/github.py,sha256=X0lEWBCfy-vztX2vauuVSYsOCa9_ezt9hGa5BsCQTu8,46663
|
|
7
7
|
mcp_ticketer/adapters/hybrid.py,sha256=UADYZLc_UNw0xHPSbgguBNzvUCnuYn12Qi9ea-zdlMk,19086
|
|
8
8
|
mcp_ticketer/adapters/jira.py,sha256=W2pU-YxrSqgjm1gVt2eGc8We-G0MbRMSggQ2gWkThME,30602
|
|
9
|
-
mcp_ticketer/adapters/linear.py,sha256=
|
|
9
|
+
mcp_ticketer/adapters/linear.py,sha256=0eI8x8pv0isb7RTNmYczefUtaBFKPbOhGnnA4lrUjs8,73369
|
|
10
10
|
mcp_ticketer/cache/__init__.py,sha256=Xcd-cKnt-Cx7jBzvfzUUUPaGkmyXFi5XUFWw3Z4b7d4,138
|
|
11
11
|
mcp_ticketer/cache/memory.py,sha256=2yBqGi9i0SanlUhJoOC7nijWjoMa3_ntPe-V-AV-LfU,5042
|
|
12
12
|
mcp_ticketer/cli/__init__.py,sha256=l9Q8iKmfGkTu0cssHBVqNZTsL4eAtFzOB25AED_0G6g,89
|
|
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=
|
|
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
|
|
25
|
-
mcp_ticketer/core/config.py,sha256=
|
|
27
|
+
mcp_ticketer/core/config.py,sha256=aC1MAV0nghmkB6BnAEP3aa4DwLob9q6U04eP5xmQx0Y,15181
|
|
26
28
|
mcp_ticketer/core/env_discovery.py,sha256=wKp2Pi5vQMGOTrM1690IBv_eoABly-pD8ah7n1zSWDc,17710
|
|
27
29
|
mcp_ticketer/core/http_client.py,sha256=s5ikMiwEJ8TJjNn73wu3gv3OdAtyBEpAqPnSroRMW2k,13971
|
|
28
30
|
mcp_ticketer/core/mappers.py,sha256=1aG1jFsHTCwmGRVgOlXW-VOSTGzc86gv7qjDfiR1ups,17462
|
|
@@ -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.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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|