mcp-ticketer 0.12.0__py3-none-any.whl → 2.0.1__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 (87) hide show
  1. mcp_ticketer/__init__.py +10 -10
  2. mcp_ticketer/__version__.py +1 -1
  3. mcp_ticketer/adapters/aitrackdown.py +385 -6
  4. mcp_ticketer/adapters/asana/adapter.py +108 -0
  5. mcp_ticketer/adapters/asana/mappers.py +14 -0
  6. mcp_ticketer/adapters/github.py +525 -11
  7. mcp_ticketer/adapters/hybrid.py +47 -5
  8. mcp_ticketer/adapters/jira.py +521 -0
  9. mcp_ticketer/adapters/linear/adapter.py +1784 -101
  10. mcp_ticketer/adapters/linear/client.py +85 -3
  11. mcp_ticketer/adapters/linear/mappers.py +96 -8
  12. mcp_ticketer/adapters/linear/queries.py +168 -1
  13. mcp_ticketer/adapters/linear/types.py +80 -4
  14. mcp_ticketer/analysis/__init__.py +56 -0
  15. mcp_ticketer/analysis/dependency_graph.py +255 -0
  16. mcp_ticketer/analysis/health_assessment.py +304 -0
  17. mcp_ticketer/analysis/orphaned.py +218 -0
  18. mcp_ticketer/analysis/project_status.py +594 -0
  19. mcp_ticketer/analysis/similarity.py +224 -0
  20. mcp_ticketer/analysis/staleness.py +266 -0
  21. mcp_ticketer/automation/__init__.py +11 -0
  22. mcp_ticketer/automation/project_updates.py +378 -0
  23. mcp_ticketer/cli/adapter_diagnostics.py +3 -1
  24. mcp_ticketer/cli/auggie_configure.py +17 -5
  25. mcp_ticketer/cli/codex_configure.py +97 -61
  26. mcp_ticketer/cli/configure.py +851 -103
  27. mcp_ticketer/cli/cursor_configure.py +314 -0
  28. mcp_ticketer/cli/diagnostics.py +13 -12
  29. mcp_ticketer/cli/discover.py +5 -0
  30. mcp_ticketer/cli/gemini_configure.py +17 -5
  31. mcp_ticketer/cli/init_command.py +880 -0
  32. mcp_ticketer/cli/instruction_commands.py +6 -0
  33. mcp_ticketer/cli/main.py +233 -3151
  34. mcp_ticketer/cli/mcp_configure.py +672 -98
  35. mcp_ticketer/cli/mcp_server_commands.py +415 -0
  36. mcp_ticketer/cli/platform_detection.py +77 -12
  37. mcp_ticketer/cli/platform_installer.py +536 -0
  38. mcp_ticketer/cli/project_update_commands.py +350 -0
  39. mcp_ticketer/cli/setup_command.py +639 -0
  40. mcp_ticketer/cli/simple_health.py +12 -10
  41. mcp_ticketer/cli/ticket_commands.py +264 -24
  42. mcp_ticketer/core/__init__.py +28 -6
  43. mcp_ticketer/core/adapter.py +166 -1
  44. mcp_ticketer/core/config.py +21 -21
  45. mcp_ticketer/core/exceptions.py +7 -1
  46. mcp_ticketer/core/label_manager.py +732 -0
  47. mcp_ticketer/core/mappers.py +31 -19
  48. mcp_ticketer/core/models.py +135 -0
  49. mcp_ticketer/core/onepassword_secrets.py +1 -1
  50. mcp_ticketer/core/priority_matcher.py +463 -0
  51. mcp_ticketer/core/project_config.py +132 -14
  52. mcp_ticketer/core/session_state.py +171 -0
  53. mcp_ticketer/core/state_matcher.py +592 -0
  54. mcp_ticketer/core/url_parser.py +425 -0
  55. mcp_ticketer/core/validators.py +69 -0
  56. mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
  57. mcp_ticketer/mcp/server/main.py +106 -25
  58. mcp_ticketer/mcp/server/routing.py +655 -0
  59. mcp_ticketer/mcp/server/server_sdk.py +58 -0
  60. mcp_ticketer/mcp/server/tools/__init__.py +31 -12
  61. mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
  62. mcp_ticketer/mcp/server/tools/attachment_tools.py +6 -8
  63. mcp_ticketer/mcp/server/tools/bulk_tools.py +259 -202
  64. mcp_ticketer/mcp/server/tools/comment_tools.py +74 -12
  65. mcp_ticketer/mcp/server/tools/config_tools.py +1184 -136
  66. mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
  67. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +870 -460
  68. mcp_ticketer/mcp/server/tools/instruction_tools.py +7 -5
  69. mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
  70. mcp_ticketer/mcp/server/tools/pr_tools.py +3 -7
  71. mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
  72. mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
  73. mcp_ticketer/mcp/server/tools/search_tools.py +180 -97
  74. mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
  75. mcp_ticketer/mcp/server/tools/ticket_tools.py +1070 -123
  76. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +218 -236
  77. mcp_ticketer/queue/worker.py +1 -1
  78. mcp_ticketer/utils/__init__.py +5 -0
  79. mcp_ticketer/utils/token_utils.py +246 -0
  80. mcp_ticketer-2.0.1.dist-info/METADATA +1366 -0
  81. mcp_ticketer-2.0.1.dist-info/RECORD +122 -0
  82. mcp_ticketer-0.12.0.dist-info/METADATA +0 -550
  83. mcp_ticketer-0.12.0.dist-info/RECORD +0 -91
  84. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/WHEEL +0 -0
  85. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/entry_points.txt +0 -0
  86. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/licenses/LICENSE +0 -0
  87. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/top_level.txt +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:
@@ -160,6 +160,37 @@ class TicketState(str, Enum):
160
160
  """
161
161
  return target.value in self.valid_transitions().get(self, [])
162
162
 
163
+ def completion_level(self) -> int:
164
+ """Get numeric completion level for state ordering.
165
+
166
+ Higher numbers indicate more complete states. Used for parent/child
167
+ state constraints where parents must be at least as complete as
168
+ their most complete child.
169
+
170
+ Returns:
171
+ Completion level (0-7)
172
+
173
+ Example:
174
+ >>> TicketState.OPEN.completion_level()
175
+ 0
176
+ >>> TicketState.DONE.completion_level()
177
+ 6
178
+ >>> TicketState.DONE.completion_level() > TicketState.IN_PROGRESS.completion_level()
179
+ True
180
+
181
+ """
182
+ levels = {
183
+ TicketState.OPEN: 0, # Not started
184
+ TicketState.BLOCKED: 1, # Blocked
185
+ TicketState.WAITING: 2, # Waiting
186
+ TicketState.IN_PROGRESS: 3, # In progress
187
+ TicketState.READY: 4, # Ready for review
188
+ TicketState.TESTED: 5, # Tested
189
+ TicketState.DONE: 6, # Done
190
+ TicketState.CLOSED: 7, # Closed (terminal)
191
+ }
192
+ return levels.get(self, 0)
193
+
163
194
 
164
195
  class BaseTicket(BaseModel):
165
196
  """Base model for all ticket types with universal field mapping.
@@ -385,6 +416,109 @@ class Attachment(BaseModel):
385
416
  return f"Attachment({self.filename}{size_str})"
386
417
 
387
418
 
419
+ class ProjectUpdateHealth(str, Enum):
420
+ """Project health status indicator for status updates.
421
+
422
+ Represents the health/status of a project at the time of an update.
423
+ These states map to different platform-specific health indicators:
424
+
425
+ Platform Mappings:
426
+ - Linear: on_track, at_risk, off_track (1:1 mapping)
427
+ - GitHub V2: Uses ProjectV2StatusOptionConfiguration
428
+ - complete: Project is finished
429
+ - inactive: Project is not actively being worked on
430
+ - Asana: On Track, At Risk, Off Track (1:1 mapping)
431
+ - JIRA: Not directly supported (workaround via status comments)
432
+
433
+ Attributes:
434
+ ON_TRACK: Project is progressing as planned
435
+ AT_RISK: Project has some issues but recoverable
436
+ OFF_TRACK: Project is significantly behind or blocked
437
+ COMPLETE: Project is finished (GitHub-specific)
438
+ INACTIVE: Project is not actively being worked on (GitHub-specific)
439
+
440
+ Note:
441
+ Related to ticket 1M-238: Add project updates support with flexible
442
+ project identification.
443
+
444
+ """
445
+
446
+ ON_TRACK = "on_track" # Linear, Asana
447
+ AT_RISK = "at_risk" # Linear, Asana
448
+ OFF_TRACK = "off_track" # Linear, Asana
449
+ COMPLETE = "complete" # GitHub only
450
+ INACTIVE = "inactive" # GitHub only
451
+
452
+
453
+ class ProjectUpdate(BaseModel):
454
+ """Represents a project status update across different platforms.
455
+
456
+ ProjectUpdate provides a unified interface for creating and retrieving
457
+ project status updates with health indicators, supporting Linear, GitHub V2,
458
+ Asana, and JIRA (via workaround).
459
+
460
+ Platform Mappings:
461
+ - Linear: ProjectUpdate entity with health, diff_markdown, staleness
462
+ - GitHub V2: ProjectV2StatusUpdate with status options
463
+ - Asana: Project Status Updates with color-coded health
464
+ - JIRA: Comments with custom formatting (workaround)
465
+
466
+ The model includes platform-specific optional fields to support features
467
+ like Linear's auto-generated diffs and staleness indicators.
468
+
469
+ Attributes:
470
+ id: Unique identifier for the update
471
+ project_id: ID of the project this update belongs to
472
+ project_name: Optional human-readable project name
473
+ body: Markdown-formatted update content (required)
474
+ health: Optional health status indicator
475
+ created_at: Timestamp when update was created
476
+ updated_at: Timestamp when update was last modified
477
+ author_id: Optional ID of the user who created the update
478
+ author_name: Optional human-readable author name
479
+ url: Optional direct URL to the update
480
+ diff_markdown: Linear-specific auto-generated diff of project changes
481
+ is_stale: Linear-specific indicator if update is outdated
482
+
483
+ Example:
484
+ >>> update = ProjectUpdate(
485
+ ... project_id="PROJ-123",
486
+ ... body="Sprint completed with 15/20 stories done",
487
+ ... health=ProjectUpdateHealth.AT_RISK,
488
+ ... created_at=datetime.now()
489
+ ... )
490
+ >>> print(update.model_dump_json())
491
+
492
+ Note:
493
+ Related to ticket 1M-238: Add project updates support with flexible
494
+ project identification.
495
+
496
+ """
497
+
498
+ model_config = ConfigDict(use_enum_values=True)
499
+
500
+ id: str = Field(..., description="Unique update identifier")
501
+ project_id: str = Field(..., description="Parent project identifier")
502
+ project_name: str | None = Field(None, description="Human-readable project name")
503
+ body: str = Field(..., min_length=1, description="Markdown update content")
504
+ health: ProjectUpdateHealth | None = Field(
505
+ None, description="Project health status"
506
+ )
507
+ created_at: datetime = Field(..., description="Creation timestamp")
508
+ updated_at: datetime | None = Field(None, description="Last update timestamp")
509
+ author_id: str | None = Field(None, description="Update author identifier")
510
+ author_name: str | None = Field(None, description="Update author name")
511
+ url: str | None = Field(None, description="Direct URL to update")
512
+
513
+ # Platform-specific fields
514
+ diff_markdown: str | None = Field(
515
+ None, description="Linear: Auto-generated diff of project changes"
516
+ )
517
+ is_stale: bool | None = Field(
518
+ None, description="Linear: Indicator if update is outdated"
519
+ )
520
+
521
+
388
522
  class SearchQuery(BaseModel):
389
523
  """Search query parameters."""
390
524
 
@@ -393,5 +527,6 @@ class SearchQuery(BaseModel):
393
527
  priority: Priority | None = Field(None, description="Filter by priority")
394
528
  tags: list[str] | None = Field(None, description="Filter by tags")
395
529
  assignee: str | None = Field(None, description="Filter by assignee")
530
+ project: str | None = Field(None, description="Filter by project/epic ID or name")
396
531
  limit: int = Field(10, gt=0, le=100, description="Maximum results")
397
532
  offset: int = Field(0, ge=0, description="Result offset for pagination")
@@ -334,7 +334,7 @@ def check_op_cli_status() -> dict[str, Any]:
334
334
  """
335
335
  loader = OnePasswordSecretsLoader()
336
336
 
337
- status = {
337
+ status: dict[str, Any] = {
338
338
  "installed": loader.is_op_available(),
339
339
  "authenticated": False,
340
340
  "version": None,