mcp-ticketer 0.2.0__py3-none-any.whl → 2.2.9__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.
Files changed (160) hide show
  1. mcp_ticketer/__init__.py +10 -10
  2. mcp_ticketer/__version__.py +3 -3
  3. mcp_ticketer/_version_scm.py +1 -0
  4. mcp_ticketer/adapters/__init__.py +2 -0
  5. mcp_ticketer/adapters/aitrackdown.py +930 -52
  6. mcp_ticketer/adapters/asana/__init__.py +15 -0
  7. mcp_ticketer/adapters/asana/adapter.py +1537 -0
  8. mcp_ticketer/adapters/asana/client.py +292 -0
  9. mcp_ticketer/adapters/asana/mappers.py +348 -0
  10. mcp_ticketer/adapters/asana/types.py +146 -0
  11. mcp_ticketer/adapters/github/__init__.py +26 -0
  12. mcp_ticketer/adapters/github/adapter.py +3229 -0
  13. mcp_ticketer/adapters/github/client.py +335 -0
  14. mcp_ticketer/adapters/github/mappers.py +797 -0
  15. mcp_ticketer/adapters/github/queries.py +692 -0
  16. mcp_ticketer/adapters/github/types.py +460 -0
  17. mcp_ticketer/adapters/hybrid.py +58 -16
  18. mcp_ticketer/adapters/jira/__init__.py +35 -0
  19. mcp_ticketer/adapters/jira/adapter.py +1351 -0
  20. mcp_ticketer/adapters/jira/client.py +271 -0
  21. mcp_ticketer/adapters/jira/mappers.py +246 -0
  22. mcp_ticketer/adapters/jira/queries.py +216 -0
  23. mcp_ticketer/adapters/jira/types.py +304 -0
  24. mcp_ticketer/adapters/linear/__init__.py +1 -1
  25. mcp_ticketer/adapters/linear/adapter.py +3810 -462
  26. mcp_ticketer/adapters/linear/client.py +312 -69
  27. mcp_ticketer/adapters/linear/mappers.py +305 -85
  28. mcp_ticketer/adapters/linear/queries.py +317 -17
  29. mcp_ticketer/adapters/linear/types.py +187 -64
  30. mcp_ticketer/adapters/linear.py +2 -2
  31. mcp_ticketer/analysis/__init__.py +56 -0
  32. mcp_ticketer/analysis/dependency_graph.py +255 -0
  33. mcp_ticketer/analysis/health_assessment.py +304 -0
  34. mcp_ticketer/analysis/orphaned.py +218 -0
  35. mcp_ticketer/analysis/project_status.py +594 -0
  36. mcp_ticketer/analysis/similarity.py +224 -0
  37. mcp_ticketer/analysis/staleness.py +266 -0
  38. mcp_ticketer/automation/__init__.py +11 -0
  39. mcp_ticketer/automation/project_updates.py +378 -0
  40. mcp_ticketer/cache/memory.py +9 -8
  41. mcp_ticketer/cli/adapter_diagnostics.py +421 -0
  42. mcp_ticketer/cli/auggie_configure.py +116 -15
  43. mcp_ticketer/cli/codex_configure.py +274 -82
  44. mcp_ticketer/cli/configure.py +1323 -151
  45. mcp_ticketer/cli/cursor_configure.py +314 -0
  46. mcp_ticketer/cli/diagnostics.py +209 -114
  47. mcp_ticketer/cli/discover.py +297 -26
  48. mcp_ticketer/cli/gemini_configure.py +119 -26
  49. mcp_ticketer/cli/init_command.py +880 -0
  50. mcp_ticketer/cli/install_mcp_server.py +418 -0
  51. mcp_ticketer/cli/instruction_commands.py +435 -0
  52. mcp_ticketer/cli/linear_commands.py +256 -130
  53. mcp_ticketer/cli/main.py +140 -1284
  54. mcp_ticketer/cli/mcp_configure.py +1013 -100
  55. mcp_ticketer/cli/mcp_server_commands.py +415 -0
  56. mcp_ticketer/cli/migrate_config.py +12 -8
  57. mcp_ticketer/cli/platform_commands.py +123 -0
  58. mcp_ticketer/cli/platform_detection.py +477 -0
  59. mcp_ticketer/cli/platform_installer.py +545 -0
  60. mcp_ticketer/cli/project_update_commands.py +350 -0
  61. mcp_ticketer/cli/python_detection.py +126 -0
  62. mcp_ticketer/cli/queue_commands.py +15 -15
  63. mcp_ticketer/cli/setup_command.py +794 -0
  64. mcp_ticketer/cli/simple_health.py +84 -59
  65. mcp_ticketer/cli/ticket_commands.py +1375 -0
  66. mcp_ticketer/cli/update_checker.py +313 -0
  67. mcp_ticketer/cli/utils.py +195 -72
  68. mcp_ticketer/core/__init__.py +64 -1
  69. mcp_ticketer/core/adapter.py +618 -18
  70. mcp_ticketer/core/config.py +77 -68
  71. mcp_ticketer/core/env_discovery.py +75 -16
  72. mcp_ticketer/core/env_loader.py +121 -97
  73. mcp_ticketer/core/exceptions.py +32 -24
  74. mcp_ticketer/core/http_client.py +26 -26
  75. mcp_ticketer/core/instructions.py +405 -0
  76. mcp_ticketer/core/label_manager.py +732 -0
  77. mcp_ticketer/core/mappers.py +42 -30
  78. mcp_ticketer/core/milestone_manager.py +252 -0
  79. mcp_ticketer/core/models.py +566 -19
  80. mcp_ticketer/core/onepassword_secrets.py +379 -0
  81. mcp_ticketer/core/priority_matcher.py +463 -0
  82. mcp_ticketer/core/project_config.py +189 -49
  83. mcp_ticketer/core/project_utils.py +281 -0
  84. mcp_ticketer/core/project_validator.py +376 -0
  85. mcp_ticketer/core/registry.py +3 -3
  86. mcp_ticketer/core/session_state.py +176 -0
  87. mcp_ticketer/core/state_matcher.py +592 -0
  88. mcp_ticketer/core/url_parser.py +425 -0
  89. mcp_ticketer/core/validators.py +69 -0
  90. mcp_ticketer/defaults/ticket_instructions.md +644 -0
  91. mcp_ticketer/mcp/__init__.py +29 -1
  92. mcp_ticketer/mcp/__main__.py +60 -0
  93. mcp_ticketer/mcp/server/__init__.py +25 -0
  94. mcp_ticketer/mcp/server/__main__.py +60 -0
  95. mcp_ticketer/mcp/server/constants.py +58 -0
  96. mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
  97. mcp_ticketer/mcp/server/dto.py +195 -0
  98. mcp_ticketer/mcp/server/main.py +1343 -0
  99. mcp_ticketer/mcp/server/response_builder.py +206 -0
  100. mcp_ticketer/mcp/server/routing.py +723 -0
  101. mcp_ticketer/mcp/server/server_sdk.py +151 -0
  102. mcp_ticketer/mcp/server/tools/__init__.py +69 -0
  103. mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
  104. mcp_ticketer/mcp/server/tools/attachment_tools.py +224 -0
  105. mcp_ticketer/mcp/server/tools/bulk_tools.py +330 -0
  106. mcp_ticketer/mcp/server/tools/comment_tools.py +152 -0
  107. mcp_ticketer/mcp/server/tools/config_tools.py +1564 -0
  108. mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
  109. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +942 -0
  110. mcp_ticketer/mcp/server/tools/instruction_tools.py +295 -0
  111. mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
  112. mcp_ticketer/mcp/server/tools/milestone_tools.py +338 -0
  113. mcp_ticketer/mcp/server/tools/pr_tools.py +150 -0
  114. mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
  115. mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
  116. mcp_ticketer/mcp/server/tools/search_tools.py +318 -0
  117. mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
  118. mcp_ticketer/mcp/server/tools/ticket_tools.py +1413 -0
  119. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +364 -0
  120. mcp_ticketer/queue/__init__.py +1 -0
  121. mcp_ticketer/queue/health_monitor.py +168 -136
  122. mcp_ticketer/queue/manager.py +78 -63
  123. mcp_ticketer/queue/queue.py +108 -21
  124. mcp_ticketer/queue/run_worker.py +2 -2
  125. mcp_ticketer/queue/ticket_registry.py +213 -155
  126. mcp_ticketer/queue/worker.py +96 -58
  127. mcp_ticketer/utils/__init__.py +5 -0
  128. mcp_ticketer/utils/token_utils.py +246 -0
  129. mcp_ticketer-2.2.9.dist-info/METADATA +1396 -0
  130. mcp_ticketer-2.2.9.dist-info/RECORD +158 -0
  131. mcp_ticketer-2.2.9.dist-info/top_level.txt +2 -0
  132. py_mcp_installer/examples/phase3_demo.py +178 -0
  133. py_mcp_installer/scripts/manage_version.py +54 -0
  134. py_mcp_installer/setup.py +6 -0
  135. py_mcp_installer/src/py_mcp_installer/__init__.py +153 -0
  136. py_mcp_installer/src/py_mcp_installer/command_builder.py +445 -0
  137. py_mcp_installer/src/py_mcp_installer/config_manager.py +541 -0
  138. py_mcp_installer/src/py_mcp_installer/exceptions.py +243 -0
  139. py_mcp_installer/src/py_mcp_installer/installation_strategy.py +617 -0
  140. py_mcp_installer/src/py_mcp_installer/installer.py +656 -0
  141. py_mcp_installer/src/py_mcp_installer/mcp_inspector.py +750 -0
  142. py_mcp_installer/src/py_mcp_installer/platform_detector.py +451 -0
  143. py_mcp_installer/src/py_mcp_installer/platforms/__init__.py +26 -0
  144. py_mcp_installer/src/py_mcp_installer/platforms/claude_code.py +225 -0
  145. py_mcp_installer/src/py_mcp_installer/platforms/codex.py +181 -0
  146. py_mcp_installer/src/py_mcp_installer/platforms/cursor.py +191 -0
  147. py_mcp_installer/src/py_mcp_installer/types.py +222 -0
  148. py_mcp_installer/src/py_mcp_installer/utils.py +463 -0
  149. py_mcp_installer/tests/__init__.py +0 -0
  150. py_mcp_installer/tests/platforms/__init__.py +0 -0
  151. py_mcp_installer/tests/test_platform_detector.py +17 -0
  152. mcp_ticketer/adapters/github.py +0 -1354
  153. mcp_ticketer/adapters/jira.py +0 -1011
  154. mcp_ticketer/mcp/server.py +0 -1895
  155. mcp_ticketer-0.2.0.dist-info/METADATA +0 -414
  156. mcp_ticketer-0.2.0.dist-info/RECORD +0 -58
  157. mcp_ticketer-0.2.0.dist-info/top_level.txt +0 -1
  158. {mcp_ticketer-0.2.0.dist-info → mcp_ticketer-2.2.9.dist-info}/WHEEL +0 -0
  159. {mcp_ticketer-0.2.0.dist-info → mcp_ticketer-2.2.9.dist-info}/entry_points.txt +0 -0
  160. {mcp_ticketer-0.2.0.dist-info → mcp_ticketer-2.2.9.dist-info}/licenses/LICENSE +0 -0
@@ -20,11 +20,12 @@ Example:
20
20
  ... state=TicketState.IN_PROGRESS
21
21
  ... )
22
22
  >>> print(task.model_dump_json())
23
+
23
24
  """
24
25
 
25
26
  from datetime import datetime
26
27
  from enum import Enum
27
- from typing import Any, Optional
28
+ from typing import Any
28
29
 
29
30
  from pydantic import BaseModel, ConfigDict, Field
30
31
 
@@ -42,6 +43,7 @@ class Priority(str, Enum):
42
43
  MEDIUM: Standard priority, default for most work
43
44
  HIGH: High priority, should be addressed soon
44
45
  CRITICAL: Critical priority, urgent work requiring immediate attention
46
+
45
47
  """
46
48
 
47
49
  LOW = "low"
@@ -66,6 +68,7 @@ class TicketType(str, Enum):
66
68
  ISSUE: Standard work items, the primary unit of work
67
69
  TASK: Sub-work items, smaller pieces of an issue
68
70
  SUBTASK: Alias for TASK for backward compatibility
71
+
69
72
  """
70
73
 
71
74
  EPIC = "epic" # Strategic level (Projects in Linear, Milestones in GitHub)
@@ -101,6 +104,7 @@ class TicketState(str, Enum):
101
104
  WAITING: Work is paused waiting for external dependency
102
105
  BLOCKED: Work is blocked by an impediment
103
106
  CLOSED: Final state, work is closed/archived
107
+
104
108
  """
105
109
 
106
110
  OPEN = "open"
@@ -121,6 +125,7 @@ class TicketState(str, Enum):
121
125
 
122
126
  Note:
123
127
  CLOSED is a terminal state with no valid transitions
128
+
124
129
  """
125
130
  return {
126
131
  cls.OPEN: [cls.IN_PROGRESS, cls.WAITING, cls.BLOCKED, cls.CLOSED],
@@ -151,9 +156,41 @@ class TicketState(str, Enum):
151
156
  True
152
157
  >>> state.can_transition_to(TicketState.DONE)
153
158
  False
159
+
154
160
  """
155
161
  return target.value in self.valid_transitions().get(self, [])
156
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
+
157
194
 
158
195
  class BaseTicket(BaseModel):
159
196
  """Base model for all ticket types with universal field mapping.
@@ -183,18 +220,19 @@ class BaseTicket(BaseModel):
183
220
  ... tags=["bug", "authentication"]
184
221
  ... )
185
222
  >>> ticket.state = TicketState.IN_PROGRESS
223
+
186
224
  """
187
225
 
188
226
  model_config = ConfigDict(use_enum_values=True)
189
227
 
190
- id: Optional[str] = Field(None, description="Unique identifier")
228
+ id: str | None = Field(None, description="Unique identifier")
191
229
  title: str = Field(..., min_length=1, description="Ticket title")
192
- description: Optional[str] = Field(None, description="Detailed description")
230
+ description: str | None = Field(None, description="Detailed description")
193
231
  state: TicketState = Field(TicketState.OPEN, description="Current state")
194
232
  priority: Priority = Field(Priority.MEDIUM, description="Priority level")
195
233
  tags: list[str] = Field(default_factory=list, description="Tags/labels")
196
- created_at: Optional[datetime] = Field(None, description="Creation timestamp")
197
- updated_at: Optional[datetime] = Field(None, description="Last update timestamp")
234
+ created_at: datetime | None = Field(None, description="Creation timestamp")
235
+ updated_at: datetime | None = Field(None, description="Last update timestamp")
198
236
 
199
237
  # Metadata for field mapping to different systems
200
238
  metadata: dict[str, Any] = Field(
@@ -228,6 +266,7 @@ class Epic(BaseTicket):
228
266
  ... priority=Priority.HIGH
229
267
  ... )
230
268
  >>> epic.child_issues = ["ISSUE-123", "ISSUE-124"]
269
+
231
270
  """
232
271
 
233
272
  ticket_type: TicketType = Field(
@@ -245,25 +284,54 @@ class Epic(BaseTicket):
245
284
 
246
285
  Returns:
247
286
  Empty list (epics have no hierarchy constraints)
287
+
248
288
  """
249
289
  # Epics don't have parents in our hierarchy
250
290
  return []
251
291
 
252
292
 
253
293
  class Task(BaseTicket):
254
- """Task - individual work item (can be ISSUE or TASK type)."""
294
+ """Task - individual work item (can be ISSUE or TASK type).
295
+
296
+ Note: The `project` field is a synonym for `parent_epic` to provide
297
+ flexibility in CLI and API usage. Both fields map to the same underlying
298
+ value (the parent epic/project ID).
299
+ """
255
300
 
256
301
  ticket_type: TicketType = Field(
257
302
  default=TicketType.ISSUE, description="Ticket type in hierarchy"
258
303
  )
259
- parent_issue: Optional[str] = Field(None, description="Parent issue ID (for tasks)")
260
- parent_epic: Optional[str] = Field(None, description="Parent epic ID (for issues)")
261
- assignee: Optional[str] = Field(None, description="Assigned user")
304
+ parent_issue: str | None = Field(None, description="Parent issue ID (for tasks)")
305
+ parent_epic: str | None = Field(
306
+ None,
307
+ description="Parent epic/project ID (for issues). Synonym: 'project'",
308
+ )
309
+ assignee: str | None = Field(None, description="Assigned user")
262
310
  children: list[str] = Field(default_factory=list, description="Child task IDs")
263
311
 
264
312
  # Additional fields common across systems
265
- estimated_hours: Optional[float] = Field(None, description="Time estimate")
266
- actual_hours: Optional[float] = Field(None, description="Actual time spent")
313
+ estimated_hours: float | None = Field(None, description="Time estimate")
314
+ actual_hours: float | None = Field(None, description="Actual time spent")
315
+
316
+ @property
317
+ def project(self) -> str | None:
318
+ """Synonym for parent_epic.
319
+
320
+ Returns:
321
+ Parent epic/project ID
322
+
323
+ """
324
+ return self.parent_epic
325
+
326
+ @project.setter
327
+ def project(self, value: str | None) -> None:
328
+ """Set parent_epic via project synonym.
329
+
330
+ Args:
331
+ value: Parent epic/project ID
332
+
333
+ """
334
+ self.parent_epic = value
267
335
 
268
336
  def is_epic(self) -> bool:
269
337
  """Check if this is an epic (should use Epic class instead)."""
@@ -308,23 +376,502 @@ class Comment(BaseModel):
308
376
 
309
377
  model_config = ConfigDict(use_enum_values=True)
310
378
 
311
- id: Optional[str] = Field(None, description="Comment ID")
379
+ id: str | None = Field(None, description="Comment ID")
312
380
  ticket_id: str = Field(..., description="Parent ticket ID")
313
- author: Optional[str] = Field(None, description="Comment author")
381
+ author: str | None = Field(None, description="Comment author")
314
382
  content: str = Field(..., min_length=1, description="Comment text")
315
- created_at: Optional[datetime] = Field(None, description="Creation timestamp")
383
+ created_at: datetime | None = Field(None, description="Creation timestamp")
316
384
  metadata: dict[str, Any] = Field(
317
385
  default_factory=dict, description="System-specific metadata"
318
386
  )
319
387
 
320
388
 
389
+ class Attachment(BaseModel):
390
+ """File attachment metadata for tickets.
391
+
392
+ Represents a file attached to a ticket across all adapters.
393
+ Each adapter maps its native attachment format to this model.
394
+ """
395
+
396
+ model_config = ConfigDict(use_enum_values=True)
397
+
398
+ id: str | None = Field(None, description="Attachment unique identifier")
399
+ ticket_id: str = Field(..., description="Parent ticket identifier")
400
+ filename: str = Field(..., description="Original filename")
401
+ url: str | None = Field(None, description="Download URL or file path")
402
+ content_type: str | None = Field(
403
+ None, description="MIME type (e.g., 'application/pdf', 'image/png')"
404
+ )
405
+ size_bytes: int | None = Field(None, description="File size in bytes")
406
+ created_at: datetime | None = Field(None, description="Upload timestamp")
407
+ created_by: str | None = Field(None, description="User who uploaded the attachment")
408
+ description: str | None = Field(None, description="Attachment description or notes")
409
+ metadata: dict[str, Any] = Field(
410
+ default_factory=dict, description="Adapter-specific attachment metadata"
411
+ )
412
+
413
+ def __str__(self) -> str:
414
+ """Return string representation showing filename and size."""
415
+ size_str = f" ({self.size_bytes} bytes)" if self.size_bytes else ""
416
+ return f"Attachment({self.filename}{size_str})"
417
+
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
+
522
+ class Milestone(BaseModel):
523
+ """Universal milestone model for cross-platform support.
524
+
525
+ A milestone is a collection of issues grouped by labels with a target date.
526
+ Progress is calculated by counting closed vs total issues matching the labels.
527
+
528
+ Platform Mappings:
529
+ - Linear: Milestones (with labels and target dates)
530
+ - GitHub: Milestones (native support with due dates)
531
+ - JIRA: Versions/Releases (with target dates)
532
+ - Asana: Projects with dates (workaround via filtering)
533
+
534
+ The model follows the user's definition: "A milestone is a list of labels
535
+ with target dates, into which issues can be grouped."
536
+
537
+ Attributes:
538
+ id: Unique milestone identifier
539
+ name: Milestone name
540
+ target_date: Target completion date (ISO format: YYYY-MM-DD)
541
+ state: Milestone state (open, active, completed, closed)
542
+ description: Milestone description
543
+ labels: Labels that define this milestone's scope
544
+ total_issues: Total issues in milestone (calculated)
545
+ closed_issues: Closed issues in milestone (calculated)
546
+ progress_pct: Progress percentage 0-100 (calculated)
547
+ project_id: Associated project/epic ID
548
+ created_at: Creation timestamp
549
+ updated_at: Last update timestamp
550
+ platform_data: Platform-specific metadata
551
+
552
+ Example:
553
+ >>> milestone = Milestone(
554
+ ... name="v2.1.0 Release",
555
+ ... target_date=date(2025, 12, 31),
556
+ ... labels=["v2.1", "release"],
557
+ ... project_id="proj-123"
558
+ ... )
559
+ >>> milestone.total_issues = 15
560
+ >>> milestone.closed_issues = 8
561
+ >>> milestone.progress_pct = 53.3
562
+
563
+ Note:
564
+ Related to ticket 1M-607: Add milestone support (Phase 1 - Core Infrastructure)
565
+
566
+ """
567
+
568
+ model_config = ConfigDict(use_enum_values=True)
569
+
570
+ id: str | None = Field(None, description="Unique milestone identifier")
571
+ name: str = Field(..., min_length=1, description="Milestone name")
572
+ target_date: datetime | None = Field(
573
+ None, description="Target completion date (ISO format: YYYY-MM-DD)"
574
+ )
575
+ state: str = Field(
576
+ "open", description="Milestone state: open, active, completed, closed"
577
+ )
578
+ description: str = Field("", description="Milestone description")
579
+
580
+ # Label-based grouping (user's definition)
581
+ labels: list[str] = Field(
582
+ default_factory=list, description="Labels that define this milestone"
583
+ )
584
+
585
+ # Progress tracking (calculated fields)
586
+ total_issues: int = Field(0, ge=0, description="Total issues in milestone")
587
+ closed_issues: int = Field(0, ge=0, description="Closed issues in milestone")
588
+ progress_pct: float = Field(
589
+ 0.0, ge=0.0, le=100.0, description="Progress percentage (0-100)"
590
+ )
591
+
592
+ # Metadata
593
+ project_id: str | None = Field(None, description="Associated project ID")
594
+ created_at: datetime | None = Field(None, description="Creation timestamp")
595
+ updated_at: datetime | None = Field(None, description="Last update timestamp")
596
+
597
+ # Platform-specific data
598
+ platform_data: dict[str, Any] = Field(
599
+ default_factory=dict, description="Platform-specific metadata"
600
+ )
601
+
602
+
603
+ class ProjectState(str, Enum):
604
+ """Project state across platforms.
605
+
606
+ Maps to different platform concepts:
607
+ - Linear: planned, started, completed, paused, canceled
608
+ - GitHub V2: OPEN, CLOSED (with status field for more granular states)
609
+ - JIRA: Not directly supported (use project status or custom fields)
610
+
611
+ Attributes:
612
+ PLANNED: Project is planned but not yet started
613
+ ACTIVE: Project is actively being worked on
614
+ COMPLETED: Project is finished successfully
615
+ ARCHIVED: Project is archived (no longer active)
616
+ CANCELLED: Project was cancelled before completion
617
+
618
+ """
619
+
620
+ PLANNED = "planned"
621
+ ACTIVE = "active"
622
+ COMPLETED = "completed"
623
+ ARCHIVED = "archived"
624
+ CANCELLED = "cancelled"
625
+
626
+
627
+ class ProjectVisibility(str, Enum):
628
+ """Project visibility setting.
629
+
630
+ Controls who can view the project across platforms.
631
+
632
+ Attributes:
633
+ PUBLIC: Visible to everyone
634
+ PRIVATE: Visible only to members
635
+ TEAM: Visible to team members
636
+
637
+ """
638
+
639
+ PUBLIC = "public"
640
+ PRIVATE = "private"
641
+ TEAM = "team"
642
+
643
+
644
+ class ProjectScope(str, Enum):
645
+ """Project organizational scope.
646
+
647
+ Defines the level at which a project exists in the organization hierarchy.
648
+
649
+ Platform Mappings:
650
+ - Linear: TEAM (projects belong to teams) or ORGANIZATION
651
+ - GitHub: REPOSITORY, USER, or ORGANIZATION
652
+ - JIRA: PROJECT (inherent) or ORGANIZATION (via project hierarchy)
653
+
654
+ Attributes:
655
+ USER: User-level project (GitHub Projects V2)
656
+ TEAM: Team-level project (Linear, GitHub org teams)
657
+ ORGANIZATION: Organization-level project (cross-team)
658
+ REPOSITORY: Repository-scoped project (GitHub)
659
+
660
+ """
661
+
662
+ USER = "user"
663
+ TEAM = "team"
664
+ ORGANIZATION = "organization"
665
+ REPOSITORY = "repository"
666
+
667
+
668
+ class Project(BaseModel):
669
+ """Unified project model across platforms.
670
+
671
+ Projects represent strategic-level containers for issues, superseding the
672
+ Epic model with a more comprehensive structure that maps cleanly to:
673
+ - Linear Projects
674
+ - GitHub Projects V2
675
+ - JIRA Projects/Epics
676
+
677
+ This model provides backward compatibility through conversion utilities
678
+ (see project_utils.py) while enabling richer project management features.
679
+
680
+ Attributes:
681
+ id: Unique identifier in MCP Ticketer namespace
682
+ platform: Platform identifier ("linear", "github", "jira")
683
+ platform_id: Original platform-specific identifier
684
+ scope: Organizational scope of the project
685
+ name: Project name (required)
686
+ description: Detailed project description
687
+ state: Current project state
688
+ visibility: Who can view the project
689
+ url: Direct URL to project in platform
690
+ created_at: When project was created
691
+ updated_at: When project was last modified
692
+ start_date: Planned or actual start date
693
+ target_date: Target completion date
694
+ completed_at: Actual completion date
695
+ owner_id: Project owner/lead user ID
696
+ owner_name: Project owner/lead display name
697
+ team_id: Team this project belongs to
698
+ team_name: Team display name
699
+ child_issues: List of issue IDs in this project
700
+ issue_count: Total number of issues
701
+ completed_count: Number of completed issues
702
+ in_progress_count: Number of in-progress issues
703
+ progress_percentage: Overall completion percentage
704
+ extra_data: Platform-specific additional data
705
+
706
+ Example:
707
+ >>> project = Project(
708
+ ... id="proj-123",
709
+ ... platform="linear",
710
+ ... platform_id="eac28953c267",
711
+ ... scope=ProjectScope.TEAM,
712
+ ... name="MCP Ticketer v2.0",
713
+ ... state=ProjectState.ACTIVE,
714
+ ... visibility=ProjectVisibility.TEAM
715
+ ... )
716
+
717
+ """
718
+
719
+ model_config = ConfigDict(use_enum_values=True)
720
+
721
+ # Core identification
722
+ id: str = Field(..., description="Unique identifier")
723
+ platform: str = Field(..., description="Platform name (linear, github, jira)")
724
+ platform_id: str = Field(..., description="Original platform ID")
725
+ scope: ProjectScope = Field(..., description="Organizational scope")
726
+
727
+ # Basic information
728
+ name: str = Field(..., min_length=1, description="Project name")
729
+ description: str | None = Field(None, description="Project description")
730
+ state: ProjectState = Field(ProjectState.PLANNED, description="Current state")
731
+ visibility: ProjectVisibility = Field(
732
+ ProjectVisibility.TEAM, description="Visibility"
733
+ )
734
+
735
+ # URLs and references
736
+ url: str | None = Field(None, description="Direct URL to project")
737
+
738
+ # Dates
739
+ created_at: datetime | None = Field(None, description="Creation timestamp")
740
+ updated_at: datetime | None = Field(None, description="Last update timestamp")
741
+ start_date: datetime | None = Field(None, description="Start date")
742
+ target_date: datetime | None = Field(None, description="Target completion date")
743
+ completed_at: datetime | None = Field(None, description="Completion timestamp")
744
+
745
+ # Ownership
746
+ owner_id: str | None = Field(None, description="Owner user ID")
747
+ owner_name: str | None = Field(None, description="Owner display name")
748
+ team_id: str | None = Field(None, description="Team ID")
749
+ team_name: str | None = Field(None, description="Team display name")
750
+
751
+ # Issue relationships
752
+ child_issues: list[str] = Field(default_factory=list, description="Child issue IDs")
753
+ issue_count: int | None = Field(None, ge=0, description="Total issue count")
754
+ completed_count: int | None = Field(None, ge=0, description="Completed issues")
755
+ in_progress_count: int | None = Field(None, ge=0, description="In-progress issues")
756
+ progress_percentage: float | None = Field(
757
+ None, ge=0.0, le=100.0, description="Completion percentage"
758
+ )
759
+
760
+ # Platform-specific data
761
+ extra_data: dict[str, Any] = Field(
762
+ default_factory=dict, description="Platform-specific metadata"
763
+ )
764
+
765
+ def calculate_progress(self) -> float:
766
+ """Calculate progress percentage from issue counts.
767
+
768
+ Returns:
769
+ Progress percentage (0-100), or 0 if no issues
770
+
771
+ """
772
+ if not self.issue_count or self.issue_count == 0:
773
+ return 0.0
774
+
775
+ completed = self.completed_count or 0
776
+ return (completed / self.issue_count) * 100.0
777
+
778
+
779
+ class ProjectStatistics(BaseModel):
780
+ """Statistics and metrics for a project.
781
+
782
+ Provides calculated metrics for project health and progress tracking.
783
+ These statistics are typically computed from current project state
784
+ rather than stored directly.
785
+
786
+ Attributes:
787
+ project_id: ID of the project these stats belong to (optional for compatibility)
788
+ total_issues: Total number of issues (legacy field, use total_count)
789
+ completed_issues: Count of completed issues (legacy field, use completed_count)
790
+ in_progress_issues: Count of in-progress issues (legacy field, use in_progress_count)
791
+ open_issues: Count of open/backlog issues (legacy field, use open_count)
792
+ blocked_issues: Count of blocked issues (legacy field, use blocked_count)
793
+ total_count: Total number of issues (preferred)
794
+ open_count: Count of open issues (preferred)
795
+ in_progress_count: Count of in-progress issues (preferred)
796
+ completed_count: Count of completed issues (preferred)
797
+ blocked_count: Count of blocked issues (preferred)
798
+ priority_low_count: Count of low priority issues
799
+ priority_medium_count: Count of medium priority issues
800
+ priority_high_count: Count of high priority issues
801
+ priority_critical_count: Count of critical priority issues
802
+ health: Project health status (on_track, at_risk, off_track)
803
+ progress_percentage: Overall completion percentage
804
+ velocity: Issues completed per week (if available)
805
+ estimated_completion: Projected completion date
806
+
807
+ Example:
808
+ >>> stats = ProjectStatistics(
809
+ ... total_count=50,
810
+ ... completed_count=30,
811
+ ... in_progress_count=15,
812
+ ... open_count=5,
813
+ ... blocked_count=0,
814
+ ... priority_high_count=10,
815
+ ... health="on_track",
816
+ ... progress_percentage=60.0
817
+ ... )
818
+
819
+ """
820
+
821
+ model_config = ConfigDict(use_enum_values=True)
822
+
823
+ # Legacy fields for backward compatibility (optional)
824
+ project_id: str | None = Field(None, description="Project identifier (legacy)")
825
+ total_issues: int | None = Field(
826
+ None, ge=0, description="Total issue count (legacy)"
827
+ )
828
+ completed_issues: int | None = Field(
829
+ None, ge=0, description="Completed issues (legacy)"
830
+ )
831
+ in_progress_issues: int | None = Field(
832
+ None, ge=0, description="In-progress issues (legacy)"
833
+ )
834
+ open_issues: int | None = Field(
835
+ None, ge=0, description="Open/backlog issues (legacy)"
836
+ )
837
+ blocked_issues: int | None = Field(
838
+ None, ge=0, description="Blocked issues (legacy)"
839
+ )
840
+
841
+ # New preferred fields
842
+ total_count: int = Field(0, ge=0, description="Total issue count")
843
+ open_count: int = Field(0, ge=0, description="Open issues")
844
+ in_progress_count: int = Field(0, ge=0, description="In-progress issues")
845
+ completed_count: int = Field(0, ge=0, description="Completed issues")
846
+ blocked_count: int = Field(0, ge=0, description="Blocked issues")
847
+
848
+ # Priority distribution
849
+ priority_low_count: int = Field(0, ge=0, description="Low priority issues")
850
+ priority_medium_count: int = Field(0, ge=0, description="Medium priority issues")
851
+ priority_high_count: int = Field(0, ge=0, description="High priority issues")
852
+ priority_critical_count: int = Field(
853
+ 0, ge=0, description="Critical priority issues"
854
+ )
855
+
856
+ # Health and progress
857
+ health: str = Field(
858
+ "on_track", description="Health status: on_track, at_risk, off_track"
859
+ )
860
+ progress_percentage: float = Field(0.0, ge=0.0, le=100.0, description="Progress %")
861
+ velocity: float | None = Field(None, description="Issues/week completion rate")
862
+ estimated_completion: datetime | None = Field(
863
+ None, description="Projected completion date"
864
+ )
865
+
866
+
321
867
  class SearchQuery(BaseModel):
322
868
  """Search query parameters."""
323
869
 
324
- query: Optional[str] = Field(None, description="Text search query")
325
- state: Optional[TicketState] = Field(None, description="Filter by state")
326
- priority: Optional[Priority] = Field(None, description="Filter by priority")
327
- tags: Optional[list[str]] = Field(None, description="Filter by tags")
328
- assignee: Optional[str] = Field(None, description="Filter by assignee")
870
+ query: str | None = Field(None, description="Text search query")
871
+ state: TicketState | None = Field(None, description="Filter by state")
872
+ priority: Priority | None = Field(None, description="Filter by priority")
873
+ tags: list[str] | None = Field(None, description="Filter by tags")
874
+ assignee: str | None = Field(None, description="Filter by assignee")
875
+ project: str | None = Field(None, description="Filter by project/epic ID or name")
329
876
  limit: int = Field(10, gt=0, le=100, description="Maximum results")
330
877
  offset: int = Field(0, ge=0, description="Result offset for pagination")