mcp-ticketer 2.0.1__py3-none-any.whl → 2.2.13__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of mcp-ticketer might be problematic. Click here for more details.
- mcp_ticketer/__version__.py +1 -1
- mcp_ticketer/_version_scm.py +1 -0
- mcp_ticketer/adapters/aitrackdown.py +122 -0
- mcp_ticketer/adapters/asana/adapter.py +121 -0
- mcp_ticketer/adapters/github/__init__.py +26 -0
- mcp_ticketer/adapters/{github.py → github/adapter.py} +1506 -365
- 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/jira/__init__.py +35 -0
- mcp_ticketer/adapters/{jira.py → jira/adapter.py} +250 -678
- 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/adapter.py +1000 -92
- mcp_ticketer/adapters/linear/client.py +91 -1
- mcp_ticketer/adapters/linear/mappers.py +107 -0
- mcp_ticketer/adapters/linear/queries.py +112 -2
- mcp_ticketer/adapters/linear/types.py +50 -10
- mcp_ticketer/cli/configure.py +524 -89
- mcp_ticketer/cli/install_mcp_server.py +418 -0
- mcp_ticketer/cli/main.py +10 -0
- mcp_ticketer/cli/mcp_configure.py +177 -49
- mcp_ticketer/cli/platform_installer.py +9 -0
- mcp_ticketer/cli/setup_command.py +157 -1
- mcp_ticketer/cli/ticket_commands.py +443 -81
- mcp_ticketer/cli/utils.py +113 -0
- mcp_ticketer/core/__init__.py +28 -0
- mcp_ticketer/core/adapter.py +367 -1
- mcp_ticketer/core/milestone_manager.py +252 -0
- mcp_ticketer/core/models.py +345 -0
- mcp_ticketer/core/project_utils.py +281 -0
- mcp_ticketer/core/project_validator.py +376 -0
- mcp_ticketer/core/session_state.py +6 -1
- mcp_ticketer/core/state_matcher.py +36 -3
- mcp_ticketer/mcp/server/__main__.py +2 -1
- mcp_ticketer/mcp/server/routing.py +68 -0
- mcp_ticketer/mcp/server/tools/__init__.py +7 -4
- mcp_ticketer/mcp/server/tools/attachment_tools.py +3 -1
- mcp_ticketer/mcp/server/tools/config_tools.py +233 -35
- mcp_ticketer/mcp/server/tools/milestone_tools.py +338 -0
- mcp_ticketer/mcp/server/tools/search_tools.py +30 -1
- mcp_ticketer/mcp/server/tools/ticket_tools.py +37 -1
- mcp_ticketer/queue/queue.py +68 -0
- {mcp_ticketer-2.0.1.dist-info → mcp_ticketer-2.2.13.dist-info}/METADATA +33 -3
- {mcp_ticketer-2.0.1.dist-info → mcp_ticketer-2.2.13.dist-info}/RECORD +72 -36
- mcp_ticketer-2.2.13.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-2.0.1.dist-info/top_level.txt +0 -1
- {mcp_ticketer-2.0.1.dist-info → mcp_ticketer-2.2.13.dist-info}/WHEEL +0 -0
- {mcp_ticketer-2.0.1.dist-info → mcp_ticketer-2.2.13.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-2.0.1.dist-info → mcp_ticketer-2.2.13.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""Local milestone storage manager.
|
|
2
|
+
|
|
3
|
+
This module provides local persistent storage for milestones in the
|
|
4
|
+
.mcp-ticketer/milestones.json file. It handles CRUD operations for
|
|
5
|
+
milestones with automatic timestamp management and filtering support.
|
|
6
|
+
|
|
7
|
+
The storage format is JSON with the following structure:
|
|
8
|
+
{
|
|
9
|
+
"version": "1.0",
|
|
10
|
+
"milestones": {
|
|
11
|
+
"milestone-id-1": {...},
|
|
12
|
+
"milestone-id-2": {...}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
Example:
|
|
17
|
+
>>> from pathlib import Path
|
|
18
|
+
>>> from mcp_ticketer.core.milestone_manager import MilestoneManager
|
|
19
|
+
>>> from mcp_ticketer.core.models import Milestone
|
|
20
|
+
>>>
|
|
21
|
+
>>> config_dir = Path.home() / ".mcp-ticketer"
|
|
22
|
+
>>> manager = MilestoneManager(config_dir)
|
|
23
|
+
>>>
|
|
24
|
+
>>> milestone = Milestone(
|
|
25
|
+
... id="mile-001",
|
|
26
|
+
... name="v2.1.0 Release",
|
|
27
|
+
... labels=["v2.1", "release"]
|
|
28
|
+
... )
|
|
29
|
+
>>> saved = manager.save_milestone(milestone)
|
|
30
|
+
>>> retrieved = manager.get_milestone("mile-001")
|
|
31
|
+
|
|
32
|
+
Note:
|
|
33
|
+
Related to ticket 1M-607: Add milestone support (Phase 1 - Core Infrastructure)
|
|
34
|
+
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
import json
|
|
38
|
+
import logging
|
|
39
|
+
from datetime import datetime
|
|
40
|
+
from pathlib import Path
|
|
41
|
+
|
|
42
|
+
from .models import Milestone
|
|
43
|
+
|
|
44
|
+
logger = logging.getLogger(__name__)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class MilestoneManager:
|
|
48
|
+
"""Manages local milestone storage in .mcp-ticketer/milestones.json.
|
|
49
|
+
|
|
50
|
+
This class provides a simple file-based storage mechanism for milestones,
|
|
51
|
+
with automatic timestamp management and filtering capabilities. It is
|
|
52
|
+
designed to work alongside adapter-specific milestone implementations.
|
|
53
|
+
|
|
54
|
+
Attributes:
|
|
55
|
+
config_dir: Path to .mcp-ticketer configuration directory
|
|
56
|
+
milestones_file: Path to milestones.json storage file
|
|
57
|
+
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def __init__(self, config_dir: Path):
|
|
61
|
+
"""Initialize milestone manager.
|
|
62
|
+
|
|
63
|
+
Creates the storage file if it doesn't exist. Ensures the config
|
|
64
|
+
directory exists before attempting to create the storage file.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
config_dir: Path to .mcp-ticketer directory
|
|
68
|
+
|
|
69
|
+
"""
|
|
70
|
+
self.config_dir = config_dir
|
|
71
|
+
self.milestones_file = config_dir / "milestones.json"
|
|
72
|
+
self._ensure_storage()
|
|
73
|
+
|
|
74
|
+
def _ensure_storage(self) -> None:
|
|
75
|
+
"""Ensure milestone storage file exists.
|
|
76
|
+
|
|
77
|
+
Creates the config directory and initializes an empty storage file
|
|
78
|
+
if it doesn't exist. Uses atomic write to prevent corruption.
|
|
79
|
+
|
|
80
|
+
"""
|
|
81
|
+
if not self.milestones_file.exists():
|
|
82
|
+
self.config_dir.mkdir(parents=True, exist_ok=True)
|
|
83
|
+
self._save_data({"milestones": {}, "version": "1.0"})
|
|
84
|
+
logger.info(f"Initialized milestone storage at {self.milestones_file}")
|
|
85
|
+
|
|
86
|
+
def _load_data(self) -> dict:
|
|
87
|
+
"""Load milestone data from file.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Dictionary containing milestones and version info
|
|
91
|
+
|
|
92
|
+
Note:
|
|
93
|
+
Returns empty structure if file doesn't exist or is corrupted
|
|
94
|
+
|
|
95
|
+
"""
|
|
96
|
+
try:
|
|
97
|
+
with open(self.milestones_file, encoding="utf-8") as f:
|
|
98
|
+
return json.load(f)
|
|
99
|
+
except (json.JSONDecodeError, FileNotFoundError) as e:
|
|
100
|
+
logger.warning(
|
|
101
|
+
f"Failed to load milestones from {self.milestones_file}: {e}"
|
|
102
|
+
)
|
|
103
|
+
return {"milestones": {}, "version": "1.0"}
|
|
104
|
+
|
|
105
|
+
def _save_data(self, data: dict) -> None:
|
|
106
|
+
"""Save milestone data to file.
|
|
107
|
+
|
|
108
|
+
Uses atomic write pattern to prevent corruption. Serializes datetime
|
|
109
|
+
objects to ISO format strings automatically.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
data: Dictionary containing milestones and version info
|
|
113
|
+
|
|
114
|
+
"""
|
|
115
|
+
with open(self.milestones_file, "w", encoding="utf-8") as f:
|
|
116
|
+
json.dump(data, f, indent=2, default=self._json_serializer)
|
|
117
|
+
|
|
118
|
+
@staticmethod
|
|
119
|
+
def _json_serializer(obj):
|
|
120
|
+
"""Custom JSON serializer for datetime objects.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
obj: Object to serialize
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
ISO format string for datetime, original object otherwise
|
|
127
|
+
|
|
128
|
+
"""
|
|
129
|
+
if isinstance(obj, datetime):
|
|
130
|
+
return obj.isoformat()
|
|
131
|
+
raise TypeError(f"Object of type {type(obj)} is not JSON serializable")
|
|
132
|
+
|
|
133
|
+
def save_milestone(self, milestone: Milestone) -> Milestone:
|
|
134
|
+
"""Save or update milestone in local storage.
|
|
135
|
+
|
|
136
|
+
Automatically updates the updated_at timestamp before saving.
|
|
137
|
+
If the milestone doesn't have an ID, generates one based on name.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
milestone: Milestone to save
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
Saved milestone with updated timestamp
|
|
144
|
+
|
|
145
|
+
"""
|
|
146
|
+
data = self._load_data()
|
|
147
|
+
|
|
148
|
+
# Generate ID if not present
|
|
149
|
+
if not milestone.id:
|
|
150
|
+
# Simple ID generation based on name
|
|
151
|
+
import uuid
|
|
152
|
+
|
|
153
|
+
milestone.id = str(uuid.uuid4())[:8]
|
|
154
|
+
|
|
155
|
+
# Update timestamp
|
|
156
|
+
milestone.updated_at = datetime.utcnow()
|
|
157
|
+
if not milestone.created_at:
|
|
158
|
+
milestone.created_at = datetime.utcnow()
|
|
159
|
+
|
|
160
|
+
# Convert to dict for storage
|
|
161
|
+
milestone_dict = milestone.model_dump(mode="json")
|
|
162
|
+
data["milestones"][milestone.id] = milestone_dict
|
|
163
|
+
|
|
164
|
+
self._save_data(data)
|
|
165
|
+
logger.debug(f"Saved milestone {milestone.id} ({milestone.name})")
|
|
166
|
+
return milestone
|
|
167
|
+
|
|
168
|
+
def get_milestone(self, milestone_id: str) -> Milestone | None:
|
|
169
|
+
"""Get milestone by ID.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
milestone_id: Milestone identifier
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
Milestone object or None if not found
|
|
176
|
+
|
|
177
|
+
"""
|
|
178
|
+
data = self._load_data()
|
|
179
|
+
milestone_data = data["milestones"].get(milestone_id)
|
|
180
|
+
|
|
181
|
+
if not milestone_data:
|
|
182
|
+
logger.debug(f"Milestone {milestone_id} not found")
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
return Milestone(**milestone_data)
|
|
186
|
+
|
|
187
|
+
def list_milestones(
|
|
188
|
+
self,
|
|
189
|
+
project_id: str | None = None,
|
|
190
|
+
state: str | None = None,
|
|
191
|
+
) -> list[Milestone]:
|
|
192
|
+
"""List milestones with optional filters.
|
|
193
|
+
|
|
194
|
+
Filters are applied in sequence: project_id, then state.
|
|
195
|
+
Results are sorted by target_date (None values appear last).
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
project_id: Filter by project ID
|
|
199
|
+
state: Filter by state (open, active, completed, closed)
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
List of milestones matching filters, sorted by target_date
|
|
203
|
+
|
|
204
|
+
"""
|
|
205
|
+
data = self._load_data()
|
|
206
|
+
milestones = []
|
|
207
|
+
|
|
208
|
+
for milestone_data in data["milestones"].values():
|
|
209
|
+
milestone = Milestone(**milestone_data)
|
|
210
|
+
|
|
211
|
+
# Apply filters
|
|
212
|
+
if project_id and milestone.project_id != project_id:
|
|
213
|
+
continue
|
|
214
|
+
if state and milestone.state != state:
|
|
215
|
+
continue
|
|
216
|
+
|
|
217
|
+
milestones.append(milestone)
|
|
218
|
+
|
|
219
|
+
# Sort by target_date (None values last)
|
|
220
|
+
milestones.sort(
|
|
221
|
+
key=lambda m: (
|
|
222
|
+
m.target_date is None,
|
|
223
|
+
m.target_date if m.target_date else datetime.max,
|
|
224
|
+
)
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
logger.debug(
|
|
228
|
+
f"Listed {len(milestones)} milestones "
|
|
229
|
+
f"(project_id={project_id}, state={state})"
|
|
230
|
+
)
|
|
231
|
+
return milestones
|
|
232
|
+
|
|
233
|
+
def delete_milestone(self, milestone_id: str) -> bool:
|
|
234
|
+
"""Delete milestone from storage.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
milestone_id: Milestone identifier
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
True if deleted, False if not found
|
|
241
|
+
|
|
242
|
+
"""
|
|
243
|
+
data = self._load_data()
|
|
244
|
+
|
|
245
|
+
if milestone_id not in data["milestones"]:
|
|
246
|
+
logger.warning(f"Cannot delete milestone {milestone_id}: not found")
|
|
247
|
+
return False
|
|
248
|
+
|
|
249
|
+
del data["milestones"][milestone_id]
|
|
250
|
+
self._save_data(data)
|
|
251
|
+
logger.info(f"Deleted milestone {milestone_id}")
|
|
252
|
+
return True
|
mcp_ticketer/core/models.py
CHANGED
|
@@ -519,6 +519,351 @@ class ProjectUpdate(BaseModel):
|
|
|
519
519
|
)
|
|
520
520
|
|
|
521
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
|
+
|
|
522
867
|
class SearchQuery(BaseModel):
|
|
523
868
|
"""Search query parameters."""
|
|
524
869
|
|