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

Files changed (129) hide show
  1. mcp_ticketer/__init__.py +10 -10
  2. mcp_ticketer/__version__.py +1 -1
  3. mcp_ticketer/_version_scm.py +1 -0
  4. mcp_ticketer/adapters/aitrackdown.py +507 -6
  5. mcp_ticketer/adapters/asana/adapter.py +229 -0
  6. mcp_ticketer/adapters/asana/mappers.py +14 -0
  7. mcp_ticketer/adapters/github/__init__.py +26 -0
  8. mcp_ticketer/adapters/github/adapter.py +3229 -0
  9. mcp_ticketer/adapters/github/client.py +335 -0
  10. mcp_ticketer/adapters/github/mappers.py +797 -0
  11. mcp_ticketer/adapters/github/queries.py +692 -0
  12. mcp_ticketer/adapters/github/types.py +460 -0
  13. mcp_ticketer/adapters/hybrid.py +47 -5
  14. mcp_ticketer/adapters/jira/__init__.py +35 -0
  15. mcp_ticketer/adapters/jira/adapter.py +1351 -0
  16. mcp_ticketer/adapters/jira/client.py +271 -0
  17. mcp_ticketer/adapters/jira/mappers.py +246 -0
  18. mcp_ticketer/adapters/jira/queries.py +216 -0
  19. mcp_ticketer/adapters/jira/types.py +304 -0
  20. mcp_ticketer/adapters/linear/adapter.py +2730 -139
  21. mcp_ticketer/adapters/linear/client.py +175 -3
  22. mcp_ticketer/adapters/linear/mappers.py +203 -8
  23. mcp_ticketer/adapters/linear/queries.py +280 -3
  24. mcp_ticketer/adapters/linear/types.py +120 -4
  25. mcp_ticketer/analysis/__init__.py +56 -0
  26. mcp_ticketer/analysis/dependency_graph.py +255 -0
  27. mcp_ticketer/analysis/health_assessment.py +304 -0
  28. mcp_ticketer/analysis/orphaned.py +218 -0
  29. mcp_ticketer/analysis/project_status.py +594 -0
  30. mcp_ticketer/analysis/similarity.py +224 -0
  31. mcp_ticketer/analysis/staleness.py +266 -0
  32. mcp_ticketer/automation/__init__.py +11 -0
  33. mcp_ticketer/automation/project_updates.py +378 -0
  34. mcp_ticketer/cli/adapter_diagnostics.py +3 -1
  35. mcp_ticketer/cli/auggie_configure.py +17 -5
  36. mcp_ticketer/cli/codex_configure.py +97 -61
  37. mcp_ticketer/cli/configure.py +1288 -105
  38. mcp_ticketer/cli/cursor_configure.py +314 -0
  39. mcp_ticketer/cli/diagnostics.py +13 -12
  40. mcp_ticketer/cli/discover.py +5 -0
  41. mcp_ticketer/cli/gemini_configure.py +17 -5
  42. mcp_ticketer/cli/init_command.py +880 -0
  43. mcp_ticketer/cli/install_mcp_server.py +418 -0
  44. mcp_ticketer/cli/instruction_commands.py +6 -0
  45. mcp_ticketer/cli/main.py +267 -3175
  46. mcp_ticketer/cli/mcp_configure.py +821 -119
  47. mcp_ticketer/cli/mcp_server_commands.py +415 -0
  48. mcp_ticketer/cli/platform_detection.py +77 -12
  49. mcp_ticketer/cli/platform_installer.py +545 -0
  50. mcp_ticketer/cli/project_update_commands.py +350 -0
  51. mcp_ticketer/cli/setup_command.py +795 -0
  52. mcp_ticketer/cli/simple_health.py +12 -10
  53. mcp_ticketer/cli/ticket_commands.py +705 -103
  54. mcp_ticketer/cli/utils.py +113 -0
  55. mcp_ticketer/core/__init__.py +56 -6
  56. mcp_ticketer/core/adapter.py +533 -2
  57. mcp_ticketer/core/config.py +21 -21
  58. mcp_ticketer/core/exceptions.py +7 -1
  59. mcp_ticketer/core/label_manager.py +732 -0
  60. mcp_ticketer/core/mappers.py +31 -19
  61. mcp_ticketer/core/milestone_manager.py +252 -0
  62. mcp_ticketer/core/models.py +480 -0
  63. mcp_ticketer/core/onepassword_secrets.py +1 -1
  64. mcp_ticketer/core/priority_matcher.py +463 -0
  65. mcp_ticketer/core/project_config.py +132 -14
  66. mcp_ticketer/core/project_utils.py +281 -0
  67. mcp_ticketer/core/project_validator.py +376 -0
  68. mcp_ticketer/core/session_state.py +176 -0
  69. mcp_ticketer/core/state_matcher.py +625 -0
  70. mcp_ticketer/core/url_parser.py +425 -0
  71. mcp_ticketer/core/validators.py +69 -0
  72. mcp_ticketer/mcp/server/__main__.py +2 -1
  73. mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
  74. mcp_ticketer/mcp/server/main.py +106 -25
  75. mcp_ticketer/mcp/server/routing.py +723 -0
  76. mcp_ticketer/mcp/server/server_sdk.py +58 -0
  77. mcp_ticketer/mcp/server/tools/__init__.py +33 -11
  78. mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
  79. mcp_ticketer/mcp/server/tools/attachment_tools.py +5 -5
  80. mcp_ticketer/mcp/server/tools/bulk_tools.py +259 -202
  81. mcp_ticketer/mcp/server/tools/comment_tools.py +74 -12
  82. mcp_ticketer/mcp/server/tools/config_tools.py +1391 -145
  83. mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
  84. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +870 -460
  85. mcp_ticketer/mcp/server/tools/instruction_tools.py +7 -5
  86. mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
  87. mcp_ticketer/mcp/server/tools/milestone_tools.py +338 -0
  88. mcp_ticketer/mcp/server/tools/pr_tools.py +3 -7
  89. mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
  90. mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
  91. mcp_ticketer/mcp/server/tools/search_tools.py +209 -97
  92. mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
  93. mcp_ticketer/mcp/server/tools/ticket_tools.py +1107 -124
  94. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +218 -236
  95. mcp_ticketer/queue/queue.py +68 -0
  96. mcp_ticketer/queue/worker.py +1 -1
  97. mcp_ticketer/utils/__init__.py +5 -0
  98. mcp_ticketer/utils/token_utils.py +246 -0
  99. mcp_ticketer-2.2.13.dist-info/METADATA +1396 -0
  100. mcp_ticketer-2.2.13.dist-info/RECORD +158 -0
  101. mcp_ticketer-2.2.13.dist-info/top_level.txt +2 -0
  102. py_mcp_installer/examples/phase3_demo.py +178 -0
  103. py_mcp_installer/scripts/manage_version.py +54 -0
  104. py_mcp_installer/setup.py +6 -0
  105. py_mcp_installer/src/py_mcp_installer/__init__.py +153 -0
  106. py_mcp_installer/src/py_mcp_installer/command_builder.py +445 -0
  107. py_mcp_installer/src/py_mcp_installer/config_manager.py +541 -0
  108. py_mcp_installer/src/py_mcp_installer/exceptions.py +243 -0
  109. py_mcp_installer/src/py_mcp_installer/installation_strategy.py +617 -0
  110. py_mcp_installer/src/py_mcp_installer/installer.py +656 -0
  111. py_mcp_installer/src/py_mcp_installer/mcp_inspector.py +750 -0
  112. py_mcp_installer/src/py_mcp_installer/platform_detector.py +451 -0
  113. py_mcp_installer/src/py_mcp_installer/platforms/__init__.py +26 -0
  114. py_mcp_installer/src/py_mcp_installer/platforms/claude_code.py +225 -0
  115. py_mcp_installer/src/py_mcp_installer/platforms/codex.py +181 -0
  116. py_mcp_installer/src/py_mcp_installer/platforms/cursor.py +191 -0
  117. py_mcp_installer/src/py_mcp_installer/types.py +222 -0
  118. py_mcp_installer/src/py_mcp_installer/utils.py +463 -0
  119. py_mcp_installer/tests/__init__.py +0 -0
  120. py_mcp_installer/tests/platforms/__init__.py +0 -0
  121. py_mcp_installer/tests/test_platform_detector.py +17 -0
  122. mcp_ticketer/adapters/github.py +0 -1574
  123. mcp_ticketer/adapters/jira.py +0 -1258
  124. mcp_ticketer-0.12.0.dist-info/METADATA +0 -550
  125. mcp_ticketer-0.12.0.dist-info/RECORD +0 -91
  126. mcp_ticketer-0.12.0.dist-info/top_level.txt +0 -1
  127. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.2.13.dist-info}/WHEEL +0 -0
  128. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.2.13.dist-info}/entry_points.txt +0 -0
  129. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.2.13.dist-info}/licenses/LICENSE +0 -0
@@ -98,13 +98,13 @@ class StateMapper(BaseMapper):
98
98
  self._mapping: BiDirectionalDict | None = None
99
99
 
100
100
  @lru_cache(maxsize=1)
101
- def get_mapping(self) -> BiDirectionalDict:
101
+ def get_mapping(self) -> BiDirectionalDict[TicketState, str]:
102
102
  """Get cached bidirectional state mapping."""
103
103
  if self._mapping is not None:
104
104
  return self._mapping
105
105
 
106
106
  # Default mappings by adapter type
107
- default_mappings = {
107
+ default_mappings: dict[str, dict[TicketState, str]] = {
108
108
  "github": {
109
109
  TicketState.OPEN: "open",
110
110
  TicketState.IN_PROGRESS: "open", # Uses labels
@@ -147,13 +147,16 @@ class StateMapper(BaseMapper):
147
147
  },
148
148
  }
149
149
 
150
- mapping = default_mappings.get(self.adapter_type, {})
150
+ mapping: dict[TicketState, str] = default_mappings.get(self.adapter_type, {})
151
151
 
152
- # Apply custom mappings
152
+ # Apply custom mappings (cast to proper type)
153
153
  if self.custom_mappings:
154
- mapping.update(self.custom_mappings)
154
+ # custom_mappings might have str keys, need to convert to TicketState
155
+ for key, value in self.custom_mappings.items():
156
+ if isinstance(key, TicketState):
157
+ mapping[key] = value
155
158
 
156
- self._mapping = BiDirectionalDict(mapping)
159
+ self._mapping = BiDirectionalDict[TicketState, str](mapping)
157
160
  return self._mapping
158
161
 
159
162
  def to_system_state(self, adapter_state: str) -> TicketState:
@@ -168,7 +171,9 @@ class StateMapper(BaseMapper):
168
171
  """
169
172
  cache_key = f"to_system_{adapter_state}"
170
173
  if cache_key in self._cache:
171
- return self._cache[cache_key]
174
+ cached = self._cache[cache_key]
175
+ if isinstance(cached, TicketState):
176
+ return cached
172
177
 
173
178
  mapping = self.get_mapping()
174
179
  result = mapping.get_reverse(adapter_state)
@@ -205,7 +210,9 @@ class StateMapper(BaseMapper):
205
210
  """
206
211
  cache_key = f"from_system_{system_state.value}"
207
212
  if cache_key in self._cache:
208
- return self._cache[cache_key]
213
+ cached = self._cache[cache_key]
214
+ if isinstance(cached, str):
215
+ return cached
209
216
 
210
217
  mapping = self.get_mapping()
211
218
  result = mapping.get_forward(system_state)
@@ -273,13 +280,13 @@ class PriorityMapper(BaseMapper):
273
280
  self._mapping: BiDirectionalDict | None = None
274
281
 
275
282
  @lru_cache(maxsize=1)
276
- def get_mapping(self) -> BiDirectionalDict:
283
+ def get_mapping(self) -> BiDirectionalDict[Priority, Any]:
277
284
  """Get cached bidirectional priority mapping."""
278
285
  if self._mapping is not None:
279
286
  return self._mapping
280
287
 
281
288
  # Default mappings by adapter type
282
- default_mappings = {
289
+ default_mappings: dict[str, dict[Priority, Any]] = {
283
290
  "github": {
284
291
  Priority.CRITICAL: "P0",
285
292
  Priority.HIGH: "P1",
@@ -306,13 +313,16 @@ class PriorityMapper(BaseMapper):
306
313
  },
307
314
  }
308
315
 
309
- mapping = default_mappings.get(self.adapter_type, {})
316
+ mapping: dict[Priority, Any] = default_mappings.get(self.adapter_type, {})
310
317
 
311
- # Apply custom mappings
318
+ # Apply custom mappings (cast to proper type)
312
319
  if self.custom_mappings:
313
- mapping.update(self.custom_mappings)
320
+ # custom_mappings might have str keys, need to convert to Priority
321
+ for key, value in self.custom_mappings.items():
322
+ if isinstance(key, Priority):
323
+ mapping[key] = value
314
324
 
315
- self._mapping = BiDirectionalDict(mapping)
325
+ self._mapping = BiDirectionalDict[Priority, Any](mapping)
316
326
  return self._mapping
317
327
 
318
328
  def to_system_priority(self, adapter_priority: Any) -> Priority:
@@ -327,7 +337,9 @@ class PriorityMapper(BaseMapper):
327
337
  """
328
338
  cache_key = f"to_system_{adapter_priority}"
329
339
  if cache_key in self._cache:
330
- return self._cache[cache_key]
340
+ cached = self._cache[cache_key]
341
+ if isinstance(cached, Priority):
342
+ return cached
331
343
 
332
344
  mapping = self.get_mapping()
333
345
  result = mapping.get_reverse(adapter_priority)
@@ -524,10 +536,10 @@ class MapperRegistry:
524
536
  @classmethod
525
537
  def clear_cache(cls) -> None:
526
538
  """Clear all mapper caches."""
527
- for mapper in cls._state_mappers.values():
528
- mapper.clear_cache()
529
- for mapper in cls._priority_mappers.values():
530
- mapper.clear_cache()
539
+ for state_mapper in cls._state_mappers.values():
540
+ state_mapper.clear_cache()
541
+ for priority_mapper in cls._priority_mappers.values():
542
+ priority_mapper.clear_cache()
531
543
 
532
544
  @classmethod
533
545
  def reset(cls) -> None:
@@ -0,0 +1,252 @@
1
+ """Local milestone storage manager.
2
+
3
+ This module provides local persistent storage for milestones in the
4
+ .mcp-ticketer/milestones.json file. It handles CRUD operations for
5
+ milestones with automatic timestamp management and filtering support.
6
+
7
+ The storage format is JSON with the following structure:
8
+ {
9
+ "version": "1.0",
10
+ "milestones": {
11
+ "milestone-id-1": {...},
12
+ "milestone-id-2": {...}
13
+ }
14
+ }
15
+
16
+ Example:
17
+ >>> from pathlib import Path
18
+ >>> from mcp_ticketer.core.milestone_manager import MilestoneManager
19
+ >>> from mcp_ticketer.core.models import Milestone
20
+ >>>
21
+ >>> config_dir = Path.home() / ".mcp-ticketer"
22
+ >>> manager = MilestoneManager(config_dir)
23
+ >>>
24
+ >>> milestone = Milestone(
25
+ ... id="mile-001",
26
+ ... name="v2.1.0 Release",
27
+ ... labels=["v2.1", "release"]
28
+ ... )
29
+ >>> saved = manager.save_milestone(milestone)
30
+ >>> retrieved = manager.get_milestone("mile-001")
31
+
32
+ Note:
33
+ Related to ticket 1M-607: Add milestone support (Phase 1 - Core Infrastructure)
34
+
35
+ """
36
+
37
+ import json
38
+ import logging
39
+ from datetime import datetime
40
+ from pathlib import Path
41
+
42
+ from .models import Milestone
43
+
44
+ logger = logging.getLogger(__name__)
45
+
46
+
47
+ class MilestoneManager:
48
+ """Manages local milestone storage in .mcp-ticketer/milestones.json.
49
+
50
+ This class provides a simple file-based storage mechanism for milestones,
51
+ with automatic timestamp management and filtering capabilities. It is
52
+ designed to work alongside adapter-specific milestone implementations.
53
+
54
+ Attributes:
55
+ config_dir: Path to .mcp-ticketer configuration directory
56
+ milestones_file: Path to milestones.json storage file
57
+
58
+ """
59
+
60
+ def __init__(self, config_dir: Path):
61
+ """Initialize milestone manager.
62
+
63
+ Creates the storage file if it doesn't exist. Ensures the config
64
+ directory exists before attempting to create the storage file.
65
+
66
+ Args:
67
+ config_dir: Path to .mcp-ticketer directory
68
+
69
+ """
70
+ self.config_dir = config_dir
71
+ self.milestones_file = config_dir / "milestones.json"
72
+ self._ensure_storage()
73
+
74
+ def _ensure_storage(self) -> None:
75
+ """Ensure milestone storage file exists.
76
+
77
+ Creates the config directory and initializes an empty storage file
78
+ if it doesn't exist. Uses atomic write to prevent corruption.
79
+
80
+ """
81
+ if not self.milestones_file.exists():
82
+ self.config_dir.mkdir(parents=True, exist_ok=True)
83
+ self._save_data({"milestones": {}, "version": "1.0"})
84
+ logger.info(f"Initialized milestone storage at {self.milestones_file}")
85
+
86
+ def _load_data(self) -> dict:
87
+ """Load milestone data from file.
88
+
89
+ Returns:
90
+ Dictionary containing milestones and version info
91
+
92
+ Note:
93
+ Returns empty structure if file doesn't exist or is corrupted
94
+
95
+ """
96
+ try:
97
+ with open(self.milestones_file, encoding="utf-8") as f:
98
+ return json.load(f)
99
+ except (json.JSONDecodeError, FileNotFoundError) as e:
100
+ logger.warning(
101
+ f"Failed to load milestones from {self.milestones_file}: {e}"
102
+ )
103
+ return {"milestones": {}, "version": "1.0"}
104
+
105
+ def _save_data(self, data: dict) -> None:
106
+ """Save milestone data to file.
107
+
108
+ Uses atomic write pattern to prevent corruption. Serializes datetime
109
+ objects to ISO format strings automatically.
110
+
111
+ Args:
112
+ data: Dictionary containing milestones and version info
113
+
114
+ """
115
+ with open(self.milestones_file, "w", encoding="utf-8") as f:
116
+ json.dump(data, f, indent=2, default=self._json_serializer)
117
+
118
+ @staticmethod
119
+ def _json_serializer(obj):
120
+ """Custom JSON serializer for datetime objects.
121
+
122
+ Args:
123
+ obj: Object to serialize
124
+
125
+ Returns:
126
+ ISO format string for datetime, original object otherwise
127
+
128
+ """
129
+ if isinstance(obj, datetime):
130
+ return obj.isoformat()
131
+ raise TypeError(f"Object of type {type(obj)} is not JSON serializable")
132
+
133
+ def save_milestone(self, milestone: Milestone) -> Milestone:
134
+ """Save or update milestone in local storage.
135
+
136
+ Automatically updates the updated_at timestamp before saving.
137
+ If the milestone doesn't have an ID, generates one based on name.
138
+
139
+ Args:
140
+ milestone: Milestone to save
141
+
142
+ Returns:
143
+ Saved milestone with updated timestamp
144
+
145
+ """
146
+ data = self._load_data()
147
+
148
+ # Generate ID if not present
149
+ if not milestone.id:
150
+ # Simple ID generation based on name
151
+ import uuid
152
+
153
+ milestone.id = str(uuid.uuid4())[:8]
154
+
155
+ # Update timestamp
156
+ milestone.updated_at = datetime.utcnow()
157
+ if not milestone.created_at:
158
+ milestone.created_at = datetime.utcnow()
159
+
160
+ # Convert to dict for storage
161
+ milestone_dict = milestone.model_dump(mode="json")
162
+ data["milestones"][milestone.id] = milestone_dict
163
+
164
+ self._save_data(data)
165
+ logger.debug(f"Saved milestone {milestone.id} ({milestone.name})")
166
+ return milestone
167
+
168
+ def get_milestone(self, milestone_id: str) -> Milestone | None:
169
+ """Get milestone by ID.
170
+
171
+ Args:
172
+ milestone_id: Milestone identifier
173
+
174
+ Returns:
175
+ Milestone object or None if not found
176
+
177
+ """
178
+ data = self._load_data()
179
+ milestone_data = data["milestones"].get(milestone_id)
180
+
181
+ if not milestone_data:
182
+ logger.debug(f"Milestone {milestone_id} not found")
183
+ return None
184
+
185
+ return Milestone(**milestone_data)
186
+
187
+ def list_milestones(
188
+ self,
189
+ project_id: str | None = None,
190
+ state: str | None = None,
191
+ ) -> list[Milestone]:
192
+ """List milestones with optional filters.
193
+
194
+ Filters are applied in sequence: project_id, then state.
195
+ Results are sorted by target_date (None values appear last).
196
+
197
+ Args:
198
+ project_id: Filter by project ID
199
+ state: Filter by state (open, active, completed, closed)
200
+
201
+ Returns:
202
+ List of milestones matching filters, sorted by target_date
203
+
204
+ """
205
+ data = self._load_data()
206
+ milestones = []
207
+
208
+ for milestone_data in data["milestones"].values():
209
+ milestone = Milestone(**milestone_data)
210
+
211
+ # Apply filters
212
+ if project_id and milestone.project_id != project_id:
213
+ continue
214
+ if state and milestone.state != state:
215
+ continue
216
+
217
+ milestones.append(milestone)
218
+
219
+ # Sort by target_date (None values last)
220
+ milestones.sort(
221
+ key=lambda m: (
222
+ m.target_date is None,
223
+ m.target_date if m.target_date else datetime.max,
224
+ )
225
+ )
226
+
227
+ logger.debug(
228
+ f"Listed {len(milestones)} milestones "
229
+ f"(project_id={project_id}, state={state})"
230
+ )
231
+ return milestones
232
+
233
+ def delete_milestone(self, milestone_id: str) -> bool:
234
+ """Delete milestone from storage.
235
+
236
+ Args:
237
+ milestone_id: Milestone identifier
238
+
239
+ Returns:
240
+ True if deleted, False if not found
241
+
242
+ """
243
+ data = self._load_data()
244
+
245
+ if milestone_id not in data["milestones"]:
246
+ logger.warning(f"Cannot delete milestone {milestone_id}: not found")
247
+ return False
248
+
249
+ del data["milestones"][milestone_id]
250
+ self._save_data(data)
251
+ logger.info(f"Deleted milestone {milestone_id}")
252
+ return True