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

@@ -0,0 +1,383 @@
1
+ """Hierarchy management tools for Epic/Issue/Task structure.
2
+
3
+ This module implements tools for managing the three-level ticket hierarchy:
4
+ - Epic: Strategic level containers
5
+ - Issue: Standard work items
6
+ - Task: Sub-work items
7
+ """
8
+
9
+ from datetime import datetime
10
+ from typing import Any, Optional
11
+
12
+ from ...core.models import Epic, Priority, Task, TicketType
13
+ from ..server_sdk import get_adapter, mcp
14
+
15
+
16
+ @mcp.tool()
17
+ async def epic_create(
18
+ title: str,
19
+ description: str = "",
20
+ target_date: Optional[str] = None,
21
+ lead_id: Optional[str] = None,
22
+ child_issues: Optional[list[str]] = None,
23
+ ) -> dict[str, Any]:
24
+ """Create a new epic (strategic level container).
25
+
26
+ Args:
27
+ title: Epic title (required)
28
+ description: Detailed description of the epic
29
+ target_date: Target completion date in ISO format (YYYY-MM-DD)
30
+ lead_id: User ID or email of the epic lead
31
+ child_issues: List of existing issue IDs to link to this epic
32
+
33
+ Returns:
34
+ Created epic details including ID and metadata, or error information
35
+
36
+ """
37
+ try:
38
+ adapter = get_adapter()
39
+
40
+ # Parse target date if provided
41
+ target_datetime = None
42
+ if target_date:
43
+ try:
44
+ target_datetime = datetime.fromisoformat(target_date)
45
+ except ValueError:
46
+ return {
47
+ "status": "error",
48
+ "error": f"Invalid date format '{target_date}'. Use ISO format: YYYY-MM-DD",
49
+ }
50
+
51
+ # Create epic object
52
+ epic = Epic(
53
+ title=title,
54
+ description=description or "",
55
+ due_date=target_datetime,
56
+ assignee=lead_id,
57
+ child_issues=child_issues or [],
58
+ )
59
+
60
+ # Create via adapter
61
+ created = await adapter.create(epic)
62
+
63
+ return {
64
+ "status": "completed",
65
+ "epic": created.model_dump(),
66
+ }
67
+ except Exception as e:
68
+ return {
69
+ "status": "error",
70
+ "error": f"Failed to create epic: {str(e)}",
71
+ }
72
+
73
+
74
+ @mcp.tool()
75
+ async def epic_list(
76
+ limit: int = 10,
77
+ offset: int = 0,
78
+ ) -> dict[str, Any]:
79
+ """List all epics with pagination.
80
+
81
+ Args:
82
+ limit: Maximum number of epics to return (default: 10)
83
+ offset: Number of epics to skip for pagination (default: 0)
84
+
85
+ Returns:
86
+ List of epics, or error information
87
+
88
+ """
89
+ try:
90
+ adapter = get_adapter()
91
+
92
+ # List with epic filter
93
+ filters = {"ticket_type": TicketType.EPIC}
94
+ epics = await adapter.list(limit=limit, offset=offset, filters=filters)
95
+
96
+ return {
97
+ "status": "completed",
98
+ "epics": [epic.model_dump() for epic in epics],
99
+ "count": len(epics),
100
+ "limit": limit,
101
+ "offset": offset,
102
+ }
103
+ except Exception as e:
104
+ return {
105
+ "status": "error",
106
+ "error": f"Failed to list epics: {str(e)}",
107
+ }
108
+
109
+
110
+ @mcp.tool()
111
+ async def epic_issues(epic_id: str) -> dict[str, Any]:
112
+ """Get all issues belonging to an epic.
113
+
114
+ Args:
115
+ epic_id: Unique identifier of the epic
116
+
117
+ Returns:
118
+ List of issues in the epic, or error information
119
+
120
+ """
121
+ try:
122
+ adapter = get_adapter()
123
+
124
+ # Read the epic to get child issue IDs
125
+ epic = await adapter.read(epic_id)
126
+ if epic is None:
127
+ return {
128
+ "status": "error",
129
+ "error": f"Epic {epic_id} not found",
130
+ }
131
+
132
+ # If epic has no child_issues attribute, use empty list
133
+ child_issue_ids = getattr(epic, "child_issues", [])
134
+
135
+ # Fetch each child issue
136
+ issues = []
137
+ for issue_id in child_issue_ids:
138
+ issue = await adapter.read(issue_id)
139
+ if issue:
140
+ issues.append(issue.model_dump())
141
+
142
+ return {
143
+ "status": "completed",
144
+ "epic_id": epic_id,
145
+ "issues": issues,
146
+ "count": len(issues),
147
+ }
148
+ except Exception as e:
149
+ return {
150
+ "status": "error",
151
+ "error": f"Failed to get epic issues: {str(e)}",
152
+ }
153
+
154
+
155
+ @mcp.tool()
156
+ async def issue_create(
157
+ title: str,
158
+ description: str = "",
159
+ epic_id: Optional[str] = None,
160
+ assignee: Optional[str] = None,
161
+ priority: str = "medium",
162
+ ) -> dict[str, Any]:
163
+ """Create a new issue (standard work item).
164
+
165
+ Args:
166
+ title: Issue title (required)
167
+ description: Detailed description of the issue
168
+ epic_id: Parent epic ID to link this issue to
169
+ assignee: User ID or email to assign the issue to
170
+ priority: Priority level - must be one of: low, medium, high, critical
171
+
172
+ Returns:
173
+ Created issue details including ID and metadata, or error information
174
+
175
+ """
176
+ try:
177
+ adapter = get_adapter()
178
+
179
+ # Validate and convert priority
180
+ try:
181
+ priority_enum = Priority(priority.lower())
182
+ except ValueError:
183
+ return {
184
+ "status": "error",
185
+ "error": f"Invalid priority '{priority}'. Must be one of: low, medium, high, critical",
186
+ }
187
+
188
+ # Create issue (Task with ISSUE type)
189
+ issue = Task(
190
+ title=title,
191
+ description=description or "",
192
+ ticket_type=TicketType.ISSUE,
193
+ parent_epic=epic_id,
194
+ assignee=assignee,
195
+ priority=priority_enum,
196
+ )
197
+
198
+ # Create via adapter
199
+ created = await adapter.create(issue)
200
+
201
+ return {
202
+ "status": "completed",
203
+ "issue": created.model_dump(),
204
+ }
205
+ except Exception as e:
206
+ return {
207
+ "status": "error",
208
+ "error": f"Failed to create issue: {str(e)}",
209
+ }
210
+
211
+
212
+ @mcp.tool()
213
+ async def issue_tasks(issue_id: str) -> dict[str, Any]:
214
+ """Get all tasks (sub-items) belonging to an issue.
215
+
216
+ Args:
217
+ issue_id: Unique identifier of the issue
218
+
219
+ Returns:
220
+ List of tasks in the issue, or error information
221
+
222
+ """
223
+ try:
224
+ adapter = get_adapter()
225
+
226
+ # Read the issue to get child task IDs
227
+ issue = await adapter.read(issue_id)
228
+ if issue is None:
229
+ return {
230
+ "status": "error",
231
+ "error": f"Issue {issue_id} not found",
232
+ }
233
+
234
+ # Get child task IDs
235
+ child_task_ids = getattr(issue, "children", [])
236
+
237
+ # Fetch each child task
238
+ tasks = []
239
+ for task_id in child_task_ids:
240
+ task = await adapter.read(task_id)
241
+ if task:
242
+ tasks.append(task.model_dump())
243
+
244
+ return {
245
+ "status": "completed",
246
+ "issue_id": issue_id,
247
+ "tasks": tasks,
248
+ "count": len(tasks),
249
+ }
250
+ except Exception as e:
251
+ return {
252
+ "status": "error",
253
+ "error": f"Failed to get issue tasks: {str(e)}",
254
+ }
255
+
256
+
257
+ @mcp.tool()
258
+ async def task_create(
259
+ title: str,
260
+ description: str = "",
261
+ issue_id: Optional[str] = None,
262
+ assignee: Optional[str] = None,
263
+ priority: str = "medium",
264
+ ) -> dict[str, Any]:
265
+ """Create a new task (sub-work item).
266
+
267
+ Args:
268
+ title: Task title (required)
269
+ description: Detailed description of the task
270
+ issue_id: Parent issue ID to link this task to
271
+ assignee: User ID or email to assign the task to
272
+ priority: Priority level - must be one of: low, medium, high, critical
273
+
274
+ Returns:
275
+ Created task details including ID and metadata, or error information
276
+
277
+ """
278
+ try:
279
+ adapter = get_adapter()
280
+
281
+ # Validate and convert priority
282
+ try:
283
+ priority_enum = Priority(priority.lower())
284
+ except ValueError:
285
+ return {
286
+ "status": "error",
287
+ "error": f"Invalid priority '{priority}'. Must be one of: low, medium, high, critical",
288
+ }
289
+
290
+ # Create task (Task with TASK type)
291
+ task = Task(
292
+ title=title,
293
+ description=description or "",
294
+ ticket_type=TicketType.TASK,
295
+ parent_issue=issue_id,
296
+ assignee=assignee,
297
+ priority=priority_enum,
298
+ )
299
+
300
+ # Create via adapter
301
+ created = await adapter.create(task)
302
+
303
+ return {
304
+ "status": "completed",
305
+ "task": created.model_dump(),
306
+ }
307
+ except Exception as e:
308
+ return {
309
+ "status": "error",
310
+ "error": f"Failed to create task: {str(e)}",
311
+ }
312
+
313
+
314
+ @mcp.tool()
315
+ async def hierarchy_tree(
316
+ epic_id: str,
317
+ max_depth: int = 3,
318
+ ) -> dict[str, Any]:
319
+ """Get complete hierarchy tree for an epic.
320
+
321
+ Retrieves the full hierarchy tree starting from an epic, including all
322
+ child issues and their tasks up to the specified depth.
323
+
324
+ Args:
325
+ epic_id: Unique identifier of the root epic
326
+ max_depth: Maximum depth to traverse (1=epic only, 2=epic+issues, 3=epic+issues+tasks)
327
+
328
+ Returns:
329
+ Complete hierarchy tree structure, or error information
330
+
331
+ """
332
+ try:
333
+ adapter = get_adapter()
334
+
335
+ # Read the epic
336
+ epic = await adapter.read(epic_id)
337
+ if epic is None:
338
+ return {
339
+ "status": "error",
340
+ "error": f"Epic {epic_id} not found",
341
+ }
342
+
343
+ # Build tree structure
344
+ tree = {
345
+ "epic": epic.model_dump(),
346
+ "issues": [],
347
+ }
348
+
349
+ if max_depth < 2:
350
+ return {
351
+ "status": "completed",
352
+ "tree": tree,
353
+ }
354
+
355
+ # Get child issues
356
+ child_issue_ids = getattr(epic, "child_issues", [])
357
+ for issue_id in child_issue_ids:
358
+ issue = await adapter.read(issue_id)
359
+ if issue:
360
+ issue_data = {
361
+ "issue": issue.model_dump(),
362
+ "tasks": [],
363
+ }
364
+
365
+ if max_depth >= 3:
366
+ # Get child tasks
367
+ child_task_ids = getattr(issue, "children", [])
368
+ for task_id in child_task_ids:
369
+ task = await adapter.read(task_id)
370
+ if task:
371
+ issue_data["tasks"].append(task.model_dump())
372
+
373
+ tree["issues"].append(issue_data)
374
+
375
+ return {
376
+ "status": "completed",
377
+ "tree": tree,
378
+ }
379
+ except Exception as e:
380
+ return {
381
+ "status": "error",
382
+ "error": f"Failed to build hierarchy tree: {str(e)}",
383
+ }
@@ -0,0 +1,154 @@
1
+ """Pull request integration tools for tickets.
2
+
3
+ This module implements tools for linking tickets with pull requests and
4
+ creating PRs from tickets. Note that PR functionality may not be available
5
+ in all adapters.
6
+ """
7
+
8
+ from typing import Any, Optional
9
+
10
+ from ..server_sdk import get_adapter, mcp
11
+
12
+
13
+ @mcp.tool()
14
+ async def ticket_create_pr(
15
+ ticket_id: str,
16
+ title: str,
17
+ description: str = "",
18
+ source_branch: Optional[str] = None,
19
+ target_branch: str = "main",
20
+ ) -> dict[str, Any]:
21
+ """Create a pull request linked to a ticket.
22
+
23
+ Creates a new pull request and automatically links it to the specified
24
+ ticket. This functionality may not be available in all adapters.
25
+
26
+ Args:
27
+ ticket_id: Unique identifier of the ticket to link the PR to
28
+ title: Pull request title
29
+ description: Pull request description
30
+ source_branch: Source branch for the PR (if not specified, may use ticket ID)
31
+ target_branch: Target branch for the PR (default: main)
32
+
33
+ Returns:
34
+ Created PR details and link information, or error information
35
+
36
+ """
37
+ try:
38
+ adapter = get_adapter()
39
+
40
+ # Check if adapter supports PR operations
41
+ if not hasattr(adapter, "create_pull_request"):
42
+ return {
43
+ "status": "error",
44
+ "error": f"Pull request creation not supported by {type(adapter).__name__} adapter",
45
+ "ticket_id": ticket_id,
46
+ }
47
+
48
+ # Read ticket to validate it exists
49
+ ticket = await adapter.read(ticket_id)
50
+ if ticket is None:
51
+ return {
52
+ "status": "error",
53
+ "error": f"Ticket {ticket_id} not found",
54
+ }
55
+
56
+ # Use ticket ID as source branch if not specified
57
+ if source_branch is None:
58
+ source_branch = f"feature/{ticket_id}"
59
+
60
+ # Create PR via adapter
61
+ pr_data = await adapter.create_pull_request( # type: ignore
62
+ ticket_id=ticket_id,
63
+ title=title,
64
+ description=description,
65
+ source_branch=source_branch,
66
+ target_branch=target_branch,
67
+ )
68
+
69
+ return {
70
+ "status": "completed",
71
+ "ticket_id": ticket_id,
72
+ "pull_request": pr_data,
73
+ }
74
+
75
+ except AttributeError:
76
+ return {
77
+ "status": "error",
78
+ "error": "Pull request creation not supported by this adapter",
79
+ "ticket_id": ticket_id,
80
+ }
81
+ except Exception as e:
82
+ return {
83
+ "status": "error",
84
+ "error": f"Failed to create pull request: {str(e)}",
85
+ "ticket_id": ticket_id,
86
+ }
87
+
88
+
89
+ @mcp.tool()
90
+ async def ticket_link_pr(
91
+ ticket_id: str,
92
+ pr_url: str,
93
+ ) -> dict[str, Any]:
94
+ """Link an existing pull request to a ticket.
95
+
96
+ Associates an existing pull request (identified by URL) with a ticket.
97
+ This is typically done by adding the PR URL to the ticket's metadata
98
+ or as a comment.
99
+
100
+ Args:
101
+ ticket_id: Unique identifier of the ticket
102
+ pr_url: URL of the pull request to link
103
+
104
+ Returns:
105
+ Link confirmation and updated ticket details, or error information
106
+
107
+ """
108
+ try:
109
+ adapter = get_adapter()
110
+
111
+ # Read ticket to validate it exists
112
+ ticket = await adapter.read(ticket_id)
113
+ if ticket is None:
114
+ return {
115
+ "status": "error",
116
+ "error": f"Ticket {ticket_id} not found",
117
+ }
118
+
119
+ # Check if adapter has specialized PR linking
120
+ if hasattr(adapter, "link_pull_request"):
121
+ result = await adapter.link_pull_request( # type: ignore
122
+ ticket_id=ticket_id, pr_url=pr_url
123
+ )
124
+ return {
125
+ "status": "completed",
126
+ "ticket_id": ticket_id,
127
+ "pr_url": pr_url,
128
+ "result": result,
129
+ }
130
+
131
+ # Fallback: Add PR link as comment
132
+ from ...core.models import Comment
133
+
134
+ comment = Comment(
135
+ ticket_id=ticket_id,
136
+ content=f"Pull Request: {pr_url}",
137
+ )
138
+
139
+ created_comment = await adapter.add_comment(comment)
140
+
141
+ return {
142
+ "status": "completed",
143
+ "ticket_id": ticket_id,
144
+ "pr_url": pr_url,
145
+ "method": "comment",
146
+ "comment": created_comment.model_dump(),
147
+ }
148
+
149
+ except Exception as e:
150
+ return {
151
+ "status": "error",
152
+ "error": f"Failed to link pull request: {str(e)}",
153
+ "ticket_id": ticket_id,
154
+ }