mcp-ticketer 0.3.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.
- mcp_ticketer/__init__.py +10 -10
- mcp_ticketer/__version__.py +3 -3
- mcp_ticketer/_version_scm.py +1 -0
- mcp_ticketer/adapters/__init__.py +2 -0
- mcp_ticketer/adapters/aitrackdown.py +930 -52
- mcp_ticketer/adapters/asana/__init__.py +15 -0
- mcp_ticketer/adapters/asana/adapter.py +1537 -0
- mcp_ticketer/adapters/asana/client.py +292 -0
- mcp_ticketer/adapters/asana/mappers.py +348 -0
- mcp_ticketer/adapters/asana/types.py +146 -0
- mcp_ticketer/adapters/github/__init__.py +26 -0
- mcp_ticketer/adapters/github/adapter.py +3229 -0
- mcp_ticketer/adapters/github/client.py +335 -0
- mcp_ticketer/adapters/github/mappers.py +797 -0
- mcp_ticketer/adapters/github/queries.py +692 -0
- mcp_ticketer/adapters/github/types.py +460 -0
- mcp_ticketer/adapters/hybrid.py +58 -16
- mcp_ticketer/adapters/jira/__init__.py +35 -0
- mcp_ticketer/adapters/jira/adapter.py +1351 -0
- mcp_ticketer/adapters/jira/client.py +271 -0
- mcp_ticketer/adapters/jira/mappers.py +246 -0
- mcp_ticketer/adapters/jira/queries.py +216 -0
- mcp_ticketer/adapters/jira/types.py +304 -0
- mcp_ticketer/adapters/linear/__init__.py +1 -1
- mcp_ticketer/adapters/linear/adapter.py +3810 -462
- mcp_ticketer/adapters/linear/client.py +312 -69
- mcp_ticketer/adapters/linear/mappers.py +305 -85
- mcp_ticketer/adapters/linear/queries.py +317 -17
- mcp_ticketer/adapters/linear/types.py +187 -64
- mcp_ticketer/adapters/linear.py +2 -2
- mcp_ticketer/analysis/__init__.py +56 -0
- mcp_ticketer/analysis/dependency_graph.py +255 -0
- mcp_ticketer/analysis/health_assessment.py +304 -0
- mcp_ticketer/analysis/orphaned.py +218 -0
- mcp_ticketer/analysis/project_status.py +594 -0
- mcp_ticketer/analysis/similarity.py +224 -0
- mcp_ticketer/analysis/staleness.py +266 -0
- mcp_ticketer/automation/__init__.py +11 -0
- mcp_ticketer/automation/project_updates.py +378 -0
- mcp_ticketer/cache/memory.py +9 -8
- mcp_ticketer/cli/adapter_diagnostics.py +91 -54
- mcp_ticketer/cli/auggie_configure.py +116 -15
- mcp_ticketer/cli/codex_configure.py +274 -82
- mcp_ticketer/cli/configure.py +1323 -151
- mcp_ticketer/cli/cursor_configure.py +314 -0
- mcp_ticketer/cli/diagnostics.py +209 -114
- mcp_ticketer/cli/discover.py +297 -26
- mcp_ticketer/cli/gemini_configure.py +119 -26
- mcp_ticketer/cli/init_command.py +880 -0
- mcp_ticketer/cli/install_mcp_server.py +418 -0
- mcp_ticketer/cli/instruction_commands.py +435 -0
- mcp_ticketer/cli/linear_commands.py +256 -130
- mcp_ticketer/cli/main.py +140 -1544
- mcp_ticketer/cli/mcp_configure.py +1013 -100
- mcp_ticketer/cli/mcp_server_commands.py +415 -0
- mcp_ticketer/cli/migrate_config.py +12 -8
- mcp_ticketer/cli/platform_commands.py +123 -0
- mcp_ticketer/cli/platform_detection.py +477 -0
- mcp_ticketer/cli/platform_installer.py +545 -0
- mcp_ticketer/cli/project_update_commands.py +350 -0
- mcp_ticketer/cli/python_detection.py +126 -0
- mcp_ticketer/cli/queue_commands.py +15 -15
- mcp_ticketer/cli/setup_command.py +794 -0
- mcp_ticketer/cli/simple_health.py +84 -59
- mcp_ticketer/cli/ticket_commands.py +1375 -0
- mcp_ticketer/cli/update_checker.py +313 -0
- mcp_ticketer/cli/utils.py +195 -72
- mcp_ticketer/core/__init__.py +64 -1
- mcp_ticketer/core/adapter.py +618 -18
- mcp_ticketer/core/config.py +77 -68
- mcp_ticketer/core/env_discovery.py +75 -16
- mcp_ticketer/core/env_loader.py +121 -97
- mcp_ticketer/core/exceptions.py +32 -24
- mcp_ticketer/core/http_client.py +26 -26
- mcp_ticketer/core/instructions.py +405 -0
- mcp_ticketer/core/label_manager.py +732 -0
- mcp_ticketer/core/mappers.py +42 -30
- mcp_ticketer/core/milestone_manager.py +252 -0
- mcp_ticketer/core/models.py +566 -19
- mcp_ticketer/core/onepassword_secrets.py +379 -0
- mcp_ticketer/core/priority_matcher.py +463 -0
- mcp_ticketer/core/project_config.py +189 -49
- mcp_ticketer/core/project_utils.py +281 -0
- mcp_ticketer/core/project_validator.py +376 -0
- mcp_ticketer/core/registry.py +3 -3
- mcp_ticketer/core/session_state.py +176 -0
- mcp_ticketer/core/state_matcher.py +592 -0
- mcp_ticketer/core/url_parser.py +425 -0
- mcp_ticketer/core/validators.py +69 -0
- mcp_ticketer/defaults/ticket_instructions.md +644 -0
- mcp_ticketer/mcp/__init__.py +29 -1
- mcp_ticketer/mcp/__main__.py +60 -0
- mcp_ticketer/mcp/server/__init__.py +25 -0
- mcp_ticketer/mcp/server/__main__.py +60 -0
- mcp_ticketer/mcp/server/constants.py +58 -0
- mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
- mcp_ticketer/mcp/server/dto.py +195 -0
- mcp_ticketer/mcp/server/main.py +1343 -0
- mcp_ticketer/mcp/server/response_builder.py +206 -0
- mcp_ticketer/mcp/server/routing.py +723 -0
- mcp_ticketer/mcp/server/server_sdk.py +151 -0
- mcp_ticketer/mcp/server/tools/__init__.py +69 -0
- mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
- mcp_ticketer/mcp/server/tools/attachment_tools.py +224 -0
- mcp_ticketer/mcp/server/tools/bulk_tools.py +330 -0
- mcp_ticketer/mcp/server/tools/comment_tools.py +152 -0
- mcp_ticketer/mcp/server/tools/config_tools.py +1564 -0
- mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
- mcp_ticketer/mcp/server/tools/hierarchy_tools.py +942 -0
- mcp_ticketer/mcp/server/tools/instruction_tools.py +295 -0
- mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
- mcp_ticketer/mcp/server/tools/milestone_tools.py +338 -0
- mcp_ticketer/mcp/server/tools/pr_tools.py +150 -0
- mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
- mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
- mcp_ticketer/mcp/server/tools/search_tools.py +318 -0
- mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
- mcp_ticketer/mcp/server/tools/ticket_tools.py +1413 -0
- mcp_ticketer/mcp/server/tools/user_ticket_tools.py +364 -0
- mcp_ticketer/queue/__init__.py +1 -0
- mcp_ticketer/queue/health_monitor.py +168 -136
- mcp_ticketer/queue/manager.py +78 -63
- mcp_ticketer/queue/queue.py +108 -21
- mcp_ticketer/queue/run_worker.py +2 -2
- mcp_ticketer/queue/ticket_registry.py +213 -155
- mcp_ticketer/queue/worker.py +96 -58
- mcp_ticketer/utils/__init__.py +5 -0
- mcp_ticketer/utils/token_utils.py +246 -0
- mcp_ticketer-2.2.9.dist-info/METADATA +1396 -0
- mcp_ticketer-2.2.9.dist-info/RECORD +158 -0
- mcp_ticketer-2.2.9.dist-info/top_level.txt +2 -0
- py_mcp_installer/examples/phase3_demo.py +178 -0
- py_mcp_installer/scripts/manage_version.py +54 -0
- py_mcp_installer/setup.py +6 -0
- py_mcp_installer/src/py_mcp_installer/__init__.py +153 -0
- py_mcp_installer/src/py_mcp_installer/command_builder.py +445 -0
- py_mcp_installer/src/py_mcp_installer/config_manager.py +541 -0
- py_mcp_installer/src/py_mcp_installer/exceptions.py +243 -0
- py_mcp_installer/src/py_mcp_installer/installation_strategy.py +617 -0
- py_mcp_installer/src/py_mcp_installer/installer.py +656 -0
- py_mcp_installer/src/py_mcp_installer/mcp_inspector.py +750 -0
- py_mcp_installer/src/py_mcp_installer/platform_detector.py +451 -0
- py_mcp_installer/src/py_mcp_installer/platforms/__init__.py +26 -0
- py_mcp_installer/src/py_mcp_installer/platforms/claude_code.py +225 -0
- py_mcp_installer/src/py_mcp_installer/platforms/codex.py +181 -0
- py_mcp_installer/src/py_mcp_installer/platforms/cursor.py +191 -0
- py_mcp_installer/src/py_mcp_installer/types.py +222 -0
- py_mcp_installer/src/py_mcp_installer/utils.py +463 -0
- py_mcp_installer/tests/__init__.py +0 -0
- py_mcp_installer/tests/platforms/__init__.py +0 -0
- py_mcp_installer/tests/test_platform_detector.py +17 -0
- mcp_ticketer/adapters/github.py +0 -1354
- mcp_ticketer/adapters/jira.py +0 -1011
- mcp_ticketer/mcp/server.py +0 -2030
- mcp_ticketer-0.3.0.dist-info/METADATA +0 -414
- mcp_ticketer-0.3.0.dist-info/RECORD +0 -59
- mcp_ticketer-0.3.0.dist-info/top_level.txt +0 -1
- {mcp_ticketer-0.3.0.dist-info → mcp_ticketer-2.2.9.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.3.0.dist-info → mcp_ticketer-2.2.9.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.3.0.dist-info → mcp_ticketer-2.2.9.dist-info}/licenses/LICENSE +0 -0
mcp_ticketer/core/models.py
CHANGED
|
@@ -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
|
|
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:
|
|
228
|
+
id: str | None = Field(None, description="Unique identifier")
|
|
191
229
|
title: str = Field(..., min_length=1, description="Ticket title")
|
|
192
|
-
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:
|
|
197
|
-
updated_at:
|
|
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:
|
|
260
|
-
parent_epic:
|
|
261
|
-
|
|
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:
|
|
266
|
-
actual_hours:
|
|
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:
|
|
379
|
+
id: str | None = Field(None, description="Comment ID")
|
|
312
380
|
ticket_id: str = Field(..., description="Parent ticket ID")
|
|
313
|
-
author:
|
|
381
|
+
author: str | None = Field(None, description="Comment author")
|
|
314
382
|
content: str = Field(..., min_length=1, description="Comment text")
|
|
315
|
-
created_at:
|
|
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:
|
|
325
|
-
state:
|
|
326
|
-
priority:
|
|
327
|
-
tags:
|
|
328
|
-
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")
|