mcp-ticketer 0.4.1__py3-none-any.whl → 0.4.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of mcp-ticketer might be problematic. Click here for more details.

Files changed (56) hide show
  1. mcp_ticketer/__init__.py +3 -12
  2. mcp_ticketer/__version__.py +1 -1
  3. mcp_ticketer/adapters/aitrackdown.py +243 -11
  4. mcp_ticketer/adapters/github.py +15 -14
  5. mcp_ticketer/adapters/hybrid.py +11 -11
  6. mcp_ticketer/adapters/jira.py +22 -25
  7. mcp_ticketer/adapters/linear/adapter.py +9 -21
  8. mcp_ticketer/adapters/linear/client.py +2 -1
  9. mcp_ticketer/adapters/linear/mappers.py +2 -1
  10. mcp_ticketer/cache/memory.py +6 -5
  11. mcp_ticketer/cli/adapter_diagnostics.py +4 -2
  12. mcp_ticketer/cli/auggie_configure.py +66 -0
  13. mcp_ticketer/cli/codex_configure.py +70 -2
  14. mcp_ticketer/cli/configure.py +7 -14
  15. mcp_ticketer/cli/diagnostics.py +2 -2
  16. mcp_ticketer/cli/discover.py +6 -11
  17. mcp_ticketer/cli/gemini_configure.py +68 -2
  18. mcp_ticketer/cli/linear_commands.py +6 -7
  19. mcp_ticketer/cli/main.py +341 -203
  20. mcp_ticketer/cli/mcp_configure.py +61 -2
  21. mcp_ticketer/cli/ticket_commands.py +27 -30
  22. mcp_ticketer/cli/utils.py +23 -22
  23. mcp_ticketer/core/__init__.py +3 -1
  24. mcp_ticketer/core/adapter.py +82 -13
  25. mcp_ticketer/core/config.py +27 -29
  26. mcp_ticketer/core/env_discovery.py +10 -10
  27. mcp_ticketer/core/env_loader.py +8 -8
  28. mcp_ticketer/core/http_client.py +16 -16
  29. mcp_ticketer/core/mappers.py +10 -10
  30. mcp_ticketer/core/models.py +50 -20
  31. mcp_ticketer/core/project_config.py +40 -34
  32. mcp_ticketer/core/registry.py +2 -2
  33. mcp_ticketer/mcp/dto.py +32 -32
  34. mcp_ticketer/mcp/response_builder.py +2 -2
  35. mcp_ticketer/mcp/server.py +17 -37
  36. mcp_ticketer/mcp/server_sdk.py +93 -0
  37. mcp_ticketer/mcp/tools/__init__.py +36 -0
  38. mcp_ticketer/mcp/tools/attachment_tools.py +179 -0
  39. mcp_ticketer/mcp/tools/bulk_tools.py +273 -0
  40. mcp_ticketer/mcp/tools/comment_tools.py +90 -0
  41. mcp_ticketer/mcp/tools/hierarchy_tools.py +383 -0
  42. mcp_ticketer/mcp/tools/pr_tools.py +154 -0
  43. mcp_ticketer/mcp/tools/search_tools.py +206 -0
  44. mcp_ticketer/mcp/tools/ticket_tools.py +277 -0
  45. mcp_ticketer/queue/health_monitor.py +4 -4
  46. mcp_ticketer/queue/manager.py +2 -2
  47. mcp_ticketer/queue/queue.py +16 -16
  48. mcp_ticketer/queue/ticket_registry.py +7 -7
  49. mcp_ticketer/queue/worker.py +2 -2
  50. {mcp_ticketer-0.4.1.dist-info → mcp_ticketer-0.4.3.dist-info}/METADATA +90 -17
  51. mcp_ticketer-0.4.3.dist-info/RECORD +73 -0
  52. mcp_ticketer-0.4.1.dist-info/RECORD +0 -64
  53. {mcp_ticketer-0.4.1.dist-info → mcp_ticketer-0.4.3.dist-info}/WHEEL +0 -0
  54. {mcp_ticketer-0.4.1.dist-info → mcp_ticketer-0.4.3.dist-info}/entry_points.txt +0 -0
  55. {mcp_ticketer-0.4.1.dist-info → mcp_ticketer-0.4.3.dist-info}/licenses/LICENSE +0 -0
  56. {mcp_ticketer-0.4.1.dist-info → mcp_ticketer-0.4.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,179 @@
1
+ """Attachment management tools for tickets.
2
+
3
+ This module implements tools for attaching files to tickets and retrieving
4
+ attachment information. Note that file attachment functionality may not be
5
+ available in all adapters.
6
+ """
7
+
8
+ from typing import Any
9
+
10
+ from ...core.models import Comment
11
+ from ..server_sdk import get_adapter, mcp
12
+
13
+
14
+ @mcp.tool()
15
+ async def ticket_attach(
16
+ ticket_id: str,
17
+ file_path: str,
18
+ description: str = "",
19
+ ) -> dict[str, Any]: # Keep as dict for MCP compatibility
20
+ """Attach a file to a ticket.
21
+
22
+ Uploads a file and associates it with the specified ticket. This
23
+ functionality may not be available in all adapters.
24
+
25
+ Args:
26
+ ticket_id: Unique identifier of the ticket
27
+ file_path: Path to the file to attach
28
+ description: Optional description of the attachment
29
+
30
+ Returns:
31
+ Attachment details including URL or ID, or error information
32
+
33
+ """
34
+ try:
35
+ adapter = get_adapter()
36
+
37
+ # Read ticket to validate it exists
38
+ ticket = await adapter.read(ticket_id)
39
+ if ticket is None:
40
+ return {
41
+ "status": "error",
42
+ "error": f"Ticket {ticket_id} not found",
43
+ }
44
+
45
+ # Check if adapter supports attachments
46
+ if not hasattr(adapter, "add_attachment"):
47
+ return {
48
+ "status": "error",
49
+ "error": f"File attachments not supported by {type(adapter).__name__} adapter",
50
+ "ticket_id": ticket_id,
51
+ "note": "Consider using ticket_comment to add a reference to the file location",
52
+ }
53
+
54
+ # Add attachment via adapter
55
+ attachment = await adapter.add_attachment( # type: ignore
56
+ ticket_id=ticket_id, file_path=file_path, description=description
57
+ )
58
+
59
+ return {
60
+ "status": "completed",
61
+ "ticket_id": ticket_id,
62
+ "attachment": attachment,
63
+ }
64
+
65
+ except AttributeError:
66
+ # Fallback: Add file reference as comment
67
+ comment_text = f"Attachment: {file_path}"
68
+ if description:
69
+ comment_text += f"\nDescription: {description}"
70
+
71
+ comment = Comment(
72
+ ticket_id=ticket_id,
73
+ content=comment_text,
74
+ )
75
+
76
+ created_comment = await adapter.add_comment(comment)
77
+
78
+ return {
79
+ "status": "completed",
80
+ "ticket_id": ticket_id,
81
+ "method": "comment_reference",
82
+ "file_path": file_path,
83
+ "comment": created_comment.model_dump(),
84
+ "note": "Adapter does not support direct file uploads. File reference added as comment.",
85
+ }
86
+
87
+ except FileNotFoundError:
88
+ return {
89
+ "status": "error",
90
+ "error": f"File not found: {file_path}",
91
+ "ticket_id": ticket_id,
92
+ }
93
+ except Exception as e:
94
+ return {
95
+ "status": "error",
96
+ "error": f"Failed to attach file: {str(e)}",
97
+ "ticket_id": ticket_id,
98
+ }
99
+
100
+
101
+ @mcp.tool()
102
+ async def ticket_attachments(
103
+ ticket_id: str,
104
+ ) -> dict[str, Any]: # Keep as dict for MCP compatibility
105
+ """Get all attachments for a ticket.
106
+
107
+ Retrieves a list of all files attached to the specified ticket.
108
+ This functionality may not be available in all adapters.
109
+
110
+ Args:
111
+ ticket_id: Unique identifier of the ticket
112
+
113
+ Returns:
114
+ List of attachments with metadata, or error information
115
+
116
+ """
117
+ try:
118
+ adapter = get_adapter()
119
+
120
+ # Read ticket to validate it exists
121
+ ticket = await adapter.read(ticket_id)
122
+ if ticket is None:
123
+ return {
124
+ "status": "error",
125
+ "error": f"Ticket {ticket_id} not found",
126
+ }
127
+
128
+ # Check if adapter supports attachments
129
+ if not hasattr(adapter, "get_attachments"):
130
+ return {
131
+ "status": "error",
132
+ "error": f"Attachment retrieval not supported by {type(adapter).__name__} adapter",
133
+ "ticket_id": ticket_id,
134
+ "note": "Check ticket comments for file references",
135
+ }
136
+
137
+ # Get attachments via adapter
138
+ attachments = await adapter.get_attachments(ticket_id) # type: ignore
139
+
140
+ return {
141
+ "status": "completed",
142
+ "ticket_id": ticket_id,
143
+ "attachments": attachments,
144
+ "count": len(attachments) if isinstance(attachments, list) else 0,
145
+ }
146
+
147
+ except AttributeError:
148
+ # Fallback: Check comments for attachment references
149
+ comments = await adapter.get_comments(ticket_id=ticket_id, limit=100)
150
+
151
+ # Look for comments that reference files
152
+ attachment_refs = []
153
+ for comment in comments:
154
+ content = comment.content or ""
155
+ if content.startswith("Attachment:") or "file://" in content:
156
+ attachment_refs.append(
157
+ {
158
+ "type": "comment_reference",
159
+ "comment_id": comment.id,
160
+ "content": content,
161
+ "created_at": comment.created_at,
162
+ }
163
+ )
164
+
165
+ return {
166
+ "status": "completed",
167
+ "ticket_id": ticket_id,
168
+ "method": "comment_references",
169
+ "attachments": attachment_refs,
170
+ "count": len(attachment_refs),
171
+ "note": "Adapter does not support direct attachments. Showing file references from comments.",
172
+ }
173
+
174
+ except Exception as e:
175
+ return {
176
+ "status": "error",
177
+ "error": f"Failed to get attachments: {str(e)}",
178
+ "ticket_id": ticket_id,
179
+ }
@@ -0,0 +1,273 @@
1
+ """Bulk operations for creating and updating multiple tickets.
2
+
3
+ This module implements tools for batch operations on tickets to improve
4
+ efficiency when working with multiple items.
5
+ """
6
+
7
+ from typing import Any
8
+
9
+ from ...core.models import Priority, Task, TicketState, TicketType
10
+ from ..server_sdk import get_adapter, mcp
11
+
12
+
13
+ @mcp.tool()
14
+ async def ticket_bulk_create(
15
+ tickets: list[dict[str, Any]],
16
+ ) -> dict[str, Any]:
17
+ """Create multiple tickets in a single operation.
18
+
19
+ Each ticket dict should contain at minimum a 'title' field, with optional
20
+ fields: description, priority, tags, assignee, ticket_type, parent_epic, parent_issue.
21
+
22
+ Args:
23
+ tickets: List of ticket dictionaries to create
24
+
25
+ Returns:
26
+ Results of bulk creation including successes and failures
27
+
28
+ """
29
+ try:
30
+ adapter = get_adapter()
31
+
32
+ if not tickets:
33
+ return {
34
+ "status": "error",
35
+ "error": "No tickets provided for bulk creation",
36
+ }
37
+
38
+ results = {
39
+ "created": [],
40
+ "failed": [],
41
+ }
42
+
43
+ for i, ticket_data in enumerate(tickets):
44
+ try:
45
+ # Validate required fields
46
+ if "title" not in ticket_data:
47
+ results["failed"].append(
48
+ {
49
+ "index": i,
50
+ "error": "Missing required field: title",
51
+ "data": ticket_data,
52
+ }
53
+ )
54
+ continue
55
+
56
+ # Parse priority if provided
57
+ priority = Priority.MEDIUM # Default
58
+ if "priority" in ticket_data:
59
+ try:
60
+ priority = Priority(ticket_data["priority"].lower())
61
+ except ValueError:
62
+ results["failed"].append(
63
+ {
64
+ "index": i,
65
+ "error": f"Invalid priority: {ticket_data['priority']}",
66
+ "data": ticket_data,
67
+ }
68
+ )
69
+ continue
70
+
71
+ # Parse ticket type if provided
72
+ ticket_type = TicketType.ISSUE # Default
73
+ if "ticket_type" in ticket_data:
74
+ try:
75
+ ticket_type = TicketType(ticket_data["ticket_type"].lower())
76
+ except ValueError:
77
+ results["failed"].append(
78
+ {
79
+ "index": i,
80
+ "error": f"Invalid ticket_type: {ticket_data['ticket_type']}",
81
+ "data": ticket_data,
82
+ }
83
+ )
84
+ continue
85
+
86
+ # Create task object
87
+ task = Task(
88
+ title=ticket_data["title"],
89
+ description=ticket_data.get("description", ""),
90
+ priority=priority,
91
+ ticket_type=ticket_type,
92
+ tags=ticket_data.get("tags", []),
93
+ assignee=ticket_data.get("assignee"),
94
+ parent_epic=ticket_data.get("parent_epic"),
95
+ parent_issue=ticket_data.get("parent_issue"),
96
+ )
97
+
98
+ # Create via adapter
99
+ created = await adapter.create(task)
100
+ results["created"].append(
101
+ {
102
+ "index": i,
103
+ "ticket": created.model_dump(),
104
+ }
105
+ )
106
+
107
+ except Exception as e:
108
+ results["failed"].append(
109
+ {
110
+ "index": i,
111
+ "error": str(e),
112
+ "data": ticket_data,
113
+ }
114
+ )
115
+
116
+ return {
117
+ "status": "completed",
118
+ "summary": {
119
+ "total": len(tickets),
120
+ "created": len(results["created"]),
121
+ "failed": len(results["failed"]),
122
+ },
123
+ "results": results,
124
+ }
125
+
126
+ except Exception as e:
127
+ return {
128
+ "status": "error",
129
+ "error": f"Bulk creation failed: {str(e)}",
130
+ }
131
+
132
+
133
+ @mcp.tool()
134
+ async def ticket_bulk_update(
135
+ updates: list[dict[str, Any]],
136
+ ) -> dict[str, Any]:
137
+ """Update multiple tickets in a single operation.
138
+
139
+ Each update dict must contain 'ticket_id' and at least one field to update.
140
+ Valid update fields: title, description, priority, state, assignee, tags.
141
+
142
+ Args:
143
+ updates: List of update operation dictionaries
144
+
145
+ Returns:
146
+ Results of bulk update including successes and failures
147
+
148
+ """
149
+ try:
150
+ adapter = get_adapter()
151
+
152
+ if not updates:
153
+ return {
154
+ "status": "error",
155
+ "error": "No updates provided for bulk operation",
156
+ }
157
+
158
+ results = {
159
+ "updated": [],
160
+ "failed": [],
161
+ }
162
+
163
+ for i, update_data in enumerate(updates):
164
+ try:
165
+ # Validate required fields
166
+ if "ticket_id" not in update_data:
167
+ results["failed"].append(
168
+ {
169
+ "index": i,
170
+ "error": "Missing required field: ticket_id",
171
+ "data": update_data,
172
+ }
173
+ )
174
+ continue
175
+
176
+ ticket_id = update_data["ticket_id"]
177
+
178
+ # Build update dict
179
+ update_fields: dict[str, Any] = {}
180
+
181
+ if "title" in update_data:
182
+ update_fields["title"] = update_data["title"]
183
+ if "description" in update_data:
184
+ update_fields["description"] = update_data["description"]
185
+ if "assignee" in update_data:
186
+ update_fields["assignee"] = update_data["assignee"]
187
+ if "tags" in update_data:
188
+ update_fields["tags"] = update_data["tags"]
189
+
190
+ # Parse priority if provided
191
+ if "priority" in update_data:
192
+ try:
193
+ update_fields["priority"] = Priority(
194
+ update_data["priority"].lower()
195
+ )
196
+ except ValueError:
197
+ results["failed"].append(
198
+ {
199
+ "index": i,
200
+ "error": f"Invalid priority: {update_data['priority']}",
201
+ "data": update_data,
202
+ }
203
+ )
204
+ continue
205
+
206
+ # Parse state if provided
207
+ if "state" in update_data:
208
+ try:
209
+ update_fields["state"] = TicketState(
210
+ update_data["state"].lower()
211
+ )
212
+ except ValueError:
213
+ results["failed"].append(
214
+ {
215
+ "index": i,
216
+ "error": f"Invalid state: {update_data['state']}",
217
+ "data": update_data,
218
+ }
219
+ )
220
+ continue
221
+
222
+ if not update_fields:
223
+ results["failed"].append(
224
+ {
225
+ "index": i,
226
+ "error": "No valid update fields provided",
227
+ "data": update_data,
228
+ }
229
+ )
230
+ continue
231
+
232
+ # Update via adapter
233
+ updated = await adapter.update(ticket_id, update_fields)
234
+ if updated is None:
235
+ results["failed"].append(
236
+ {
237
+ "index": i,
238
+ "error": f"Ticket {ticket_id} not found or update failed",
239
+ "data": update_data,
240
+ }
241
+ )
242
+ else:
243
+ results["updated"].append(
244
+ {
245
+ "index": i,
246
+ "ticket": updated.model_dump(),
247
+ }
248
+ )
249
+
250
+ except Exception as e:
251
+ results["failed"].append(
252
+ {
253
+ "index": i,
254
+ "error": str(e),
255
+ "data": update_data,
256
+ }
257
+ )
258
+
259
+ return {
260
+ "status": "completed",
261
+ "summary": {
262
+ "total": len(updates),
263
+ "updated": len(results["updated"]),
264
+ "failed": len(results["failed"]),
265
+ },
266
+ "results": results,
267
+ }
268
+
269
+ except Exception as e:
270
+ return {
271
+ "status": "error",
272
+ "error": f"Bulk update failed: {str(e)}",
273
+ }
@@ -0,0 +1,90 @@
1
+ """Comment management tools for tickets.
2
+
3
+ This module implements tools for adding and retrieving comments on tickets.
4
+ """
5
+
6
+ from typing import Any
7
+
8
+ from ...core.models import Comment
9
+ from ..server_sdk import get_adapter, mcp
10
+
11
+
12
+ @mcp.tool()
13
+ async def ticket_comment(
14
+ ticket_id: str,
15
+ operation: str,
16
+ text: str | None = None,
17
+ limit: int = 10,
18
+ offset: int = 0,
19
+ ) -> dict[str, Any]:
20
+ """Add or list comments on a ticket.
21
+
22
+ This tool supports two operations:
23
+ - 'add': Add a new comment to a ticket (requires 'text' parameter)
24
+ - 'list': Retrieve comments from a ticket (supports pagination)
25
+
26
+ Args:
27
+ ticket_id: Unique identifier of the ticket
28
+ operation: Operation to perform - must be 'add' or 'list'
29
+ text: Comment text (required when operation='add')
30
+ limit: Maximum number of comments to return (used when operation='list', default: 10)
31
+ offset: Number of comments to skip for pagination (used when operation='list', default: 0)
32
+
33
+ Returns:
34
+ Comment data or list of comments, or error information
35
+
36
+ """
37
+ try:
38
+ adapter = get_adapter()
39
+
40
+ # Validate operation
41
+ if operation not in ["add", "list"]:
42
+ return {
43
+ "status": "error",
44
+ "error": f"Invalid operation '{operation}'. Must be 'add' or 'list'",
45
+ }
46
+
47
+ if operation == "add":
48
+ # Add comment operation
49
+ if not text:
50
+ return {
51
+ "status": "error",
52
+ "error": "Parameter 'text' is required when operation='add'",
53
+ }
54
+
55
+ # Create comment object
56
+ comment = Comment(
57
+ ticket_id=ticket_id,
58
+ content=text,
59
+ )
60
+
61
+ # Add comment via adapter
62
+ created = await adapter.add_comment(comment)
63
+
64
+ return {
65
+ "status": "completed",
66
+ "operation": "add",
67
+ "comment": created.model_dump(),
68
+ }
69
+
70
+ else: # operation == "list"
71
+ # List comments operation
72
+ comments = await adapter.get_comments(
73
+ ticket_id=ticket_id, limit=limit, offset=offset
74
+ )
75
+
76
+ return {
77
+ "status": "completed",
78
+ "operation": "list",
79
+ "ticket_id": ticket_id,
80
+ "comments": [comment.model_dump() for comment in comments],
81
+ "count": len(comments),
82
+ "limit": limit,
83
+ "offset": offset,
84
+ }
85
+
86
+ except Exception as e:
87
+ return {
88
+ "status": "error",
89
+ "error": f"Comment operation failed: {str(e)}",
90
+ }