mcp-ticketer 0.1.30__py3-none-any.whl → 1.2.11__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 (109) hide show
  1. mcp_ticketer/__init__.py +10 -10
  2. mcp_ticketer/__version__.py +3 -3
  3. mcp_ticketer/adapters/__init__.py +2 -0
  4. mcp_ticketer/adapters/aitrackdown.py +796 -46
  5. mcp_ticketer/adapters/asana/__init__.py +15 -0
  6. mcp_ticketer/adapters/asana/adapter.py +1416 -0
  7. mcp_ticketer/adapters/asana/client.py +292 -0
  8. mcp_ticketer/adapters/asana/mappers.py +348 -0
  9. mcp_ticketer/adapters/asana/types.py +146 -0
  10. mcp_ticketer/adapters/github.py +879 -129
  11. mcp_ticketer/adapters/hybrid.py +11 -11
  12. mcp_ticketer/adapters/jira.py +973 -73
  13. mcp_ticketer/adapters/linear/__init__.py +24 -0
  14. mcp_ticketer/adapters/linear/adapter.py +2732 -0
  15. mcp_ticketer/adapters/linear/client.py +344 -0
  16. mcp_ticketer/adapters/linear/mappers.py +420 -0
  17. mcp_ticketer/adapters/linear/queries.py +479 -0
  18. mcp_ticketer/adapters/linear/types.py +360 -0
  19. mcp_ticketer/adapters/linear.py +10 -2315
  20. mcp_ticketer/analysis/__init__.py +23 -0
  21. mcp_ticketer/analysis/orphaned.py +218 -0
  22. mcp_ticketer/analysis/similarity.py +224 -0
  23. mcp_ticketer/analysis/staleness.py +266 -0
  24. mcp_ticketer/cache/memory.py +9 -8
  25. mcp_ticketer/cli/adapter_diagnostics.py +421 -0
  26. mcp_ticketer/cli/auggie_configure.py +116 -15
  27. mcp_ticketer/cli/codex_configure.py +274 -82
  28. mcp_ticketer/cli/configure.py +888 -151
  29. mcp_ticketer/cli/diagnostics.py +400 -157
  30. mcp_ticketer/cli/discover.py +297 -26
  31. mcp_ticketer/cli/gemini_configure.py +119 -26
  32. mcp_ticketer/cli/init_command.py +880 -0
  33. mcp_ticketer/cli/instruction_commands.py +435 -0
  34. mcp_ticketer/cli/linear_commands.py +616 -0
  35. mcp_ticketer/cli/main.py +203 -1165
  36. mcp_ticketer/cli/mcp_configure.py +474 -90
  37. mcp_ticketer/cli/mcp_server_commands.py +415 -0
  38. mcp_ticketer/cli/migrate_config.py +12 -8
  39. mcp_ticketer/cli/platform_commands.py +123 -0
  40. mcp_ticketer/cli/platform_detection.py +418 -0
  41. mcp_ticketer/cli/platform_installer.py +513 -0
  42. mcp_ticketer/cli/python_detection.py +126 -0
  43. mcp_ticketer/cli/queue_commands.py +15 -15
  44. mcp_ticketer/cli/setup_command.py +639 -0
  45. mcp_ticketer/cli/simple_health.py +90 -65
  46. mcp_ticketer/cli/ticket_commands.py +1013 -0
  47. mcp_ticketer/cli/update_checker.py +313 -0
  48. mcp_ticketer/cli/utils.py +114 -66
  49. mcp_ticketer/core/__init__.py +24 -1
  50. mcp_ticketer/core/adapter.py +250 -16
  51. mcp_ticketer/core/config.py +145 -37
  52. mcp_ticketer/core/env_discovery.py +101 -22
  53. mcp_ticketer/core/env_loader.py +349 -0
  54. mcp_ticketer/core/exceptions.py +160 -0
  55. mcp_ticketer/core/http_client.py +26 -26
  56. mcp_ticketer/core/instructions.py +405 -0
  57. mcp_ticketer/core/label_manager.py +732 -0
  58. mcp_ticketer/core/mappers.py +42 -30
  59. mcp_ticketer/core/models.py +280 -28
  60. mcp_ticketer/core/onepassword_secrets.py +379 -0
  61. mcp_ticketer/core/project_config.py +183 -49
  62. mcp_ticketer/core/registry.py +3 -3
  63. mcp_ticketer/core/session_state.py +171 -0
  64. mcp_ticketer/core/state_matcher.py +592 -0
  65. mcp_ticketer/core/url_parser.py +425 -0
  66. mcp_ticketer/core/validators.py +69 -0
  67. mcp_ticketer/defaults/ticket_instructions.md +644 -0
  68. mcp_ticketer/mcp/__init__.py +29 -1
  69. mcp_ticketer/mcp/__main__.py +60 -0
  70. mcp_ticketer/mcp/server/__init__.py +25 -0
  71. mcp_ticketer/mcp/server/__main__.py +60 -0
  72. mcp_ticketer/mcp/server/constants.py +58 -0
  73. mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
  74. mcp_ticketer/mcp/server/dto.py +195 -0
  75. mcp_ticketer/mcp/server/main.py +1343 -0
  76. mcp_ticketer/mcp/server/response_builder.py +206 -0
  77. mcp_ticketer/mcp/server/routing.py +655 -0
  78. mcp_ticketer/mcp/server/server_sdk.py +151 -0
  79. mcp_ticketer/mcp/server/tools/__init__.py +56 -0
  80. mcp_ticketer/mcp/server/tools/analysis_tools.py +495 -0
  81. mcp_ticketer/mcp/server/tools/attachment_tools.py +226 -0
  82. mcp_ticketer/mcp/server/tools/bulk_tools.py +273 -0
  83. mcp_ticketer/mcp/server/tools/comment_tools.py +152 -0
  84. mcp_ticketer/mcp/server/tools/config_tools.py +1439 -0
  85. mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
  86. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +921 -0
  87. mcp_ticketer/mcp/server/tools/instruction_tools.py +300 -0
  88. mcp_ticketer/mcp/server/tools/label_tools.py +948 -0
  89. mcp_ticketer/mcp/server/tools/pr_tools.py +152 -0
  90. mcp_ticketer/mcp/server/tools/search_tools.py +215 -0
  91. mcp_ticketer/mcp/server/tools/session_tools.py +170 -0
  92. mcp_ticketer/mcp/server/tools/ticket_tools.py +1268 -0
  93. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +547 -0
  94. mcp_ticketer/queue/__init__.py +1 -0
  95. mcp_ticketer/queue/health_monitor.py +168 -136
  96. mcp_ticketer/queue/manager.py +95 -25
  97. mcp_ticketer/queue/queue.py +40 -21
  98. mcp_ticketer/queue/run_worker.py +6 -1
  99. mcp_ticketer/queue/ticket_registry.py +213 -155
  100. mcp_ticketer/queue/worker.py +109 -49
  101. mcp_ticketer-1.2.11.dist-info/METADATA +792 -0
  102. mcp_ticketer-1.2.11.dist-info/RECORD +110 -0
  103. mcp_ticketer/mcp/server.py +0 -1895
  104. mcp_ticketer-0.1.30.dist-info/METADATA +0 -413
  105. mcp_ticketer-0.1.30.dist-info/RECORD +0 -49
  106. {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/WHEEL +0 -0
  107. {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/entry_points.txt +0 -0
  108. {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/licenses/LICENSE +0 -0
  109. {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,226 @@
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
+ import mimetypes
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ from ....core.models import Comment, TicketType
13
+ from ..server_sdk import get_adapter, mcp
14
+
15
+
16
+ @mcp.tool()
17
+ async def ticket_attach(
18
+ ticket_id: str,
19
+ file_path: str,
20
+ description: str = "",
21
+ ) -> dict[str, Any]: # Keep as dict for MCP compatibility
22
+ """Attach a file to a ticket.
23
+
24
+ Uploads a file and associates it with the specified ticket. This
25
+ functionality may not be available in all adapters.
26
+
27
+ Args:
28
+ ticket_id: Unique identifier of the ticket
29
+ file_path: Path to the file to attach
30
+ description: Optional description of the attachment
31
+
32
+ Returns:
33
+ Attachment details including URL or ID, or error information
34
+
35
+ """
36
+ try:
37
+ adapter = get_adapter()
38
+
39
+ # Read ticket to validate it exists and determine type
40
+ ticket = await adapter.read(ticket_id)
41
+ if ticket is None:
42
+ return {
43
+ "status": "error",
44
+ "error": f"Ticket {ticket_id} not found",
45
+ }
46
+
47
+ # Check if file exists
48
+ file_path_obj = Path(file_path)
49
+ if not file_path_obj.exists():
50
+ return {
51
+ "status": "error",
52
+ "error": f"File not found: {file_path}",
53
+ "ticket_id": ticket_id,
54
+ }
55
+
56
+ # Try Linear-specific upload methods first (most advanced)
57
+ if hasattr(adapter, "upload_file") and hasattr(adapter, "attach_file_to_issue"):
58
+ try:
59
+ # Determine MIME type
60
+ mime_type = mimetypes.guess_type(file_path)[0]
61
+
62
+ # Upload file to Linear's storage
63
+ file_url = await adapter.upload_file(file_path, mime_type)
64
+
65
+ # Determine ticket type and attach accordingly
66
+ ticket_type = getattr(ticket, "ticket_type", None)
67
+ filename = file_path_obj.name
68
+
69
+ if ticket_type == TicketType.EPIC and hasattr(
70
+ adapter, "attach_file_to_epic"
71
+ ):
72
+ # Attach to epic (project)
73
+ result = await adapter.attach_file_to_epic(
74
+ epic_id=ticket_id,
75
+ file_url=file_url,
76
+ title=description or filename,
77
+ subtitle=f"Uploaded file: {filename}",
78
+ )
79
+ else:
80
+ # Attach to issue/task
81
+ result = await adapter.attach_file_to_issue(
82
+ issue_id=ticket_id,
83
+ file_url=file_url,
84
+ title=description or filename,
85
+ subtitle=f"Uploaded file: {filename}",
86
+ comment_body=description if description else None,
87
+ )
88
+
89
+ return {
90
+ "status": "completed",
91
+ "ticket_id": ticket_id,
92
+ "method": "linear_native_upload",
93
+ "file_url": file_url,
94
+ "attachment": result,
95
+ }
96
+ except Exception:
97
+ # Fall through to legacy method if Linear-specific upload fails
98
+ pass
99
+
100
+ # Try legacy add_attachment method
101
+ if hasattr(adapter, "add_attachment"):
102
+ attachment = await adapter.add_attachment(
103
+ ticket_id=ticket_id, file_path=file_path, description=description
104
+ )
105
+
106
+ return {
107
+ "status": "completed",
108
+ "ticket_id": ticket_id,
109
+ "method": "adapter_native",
110
+ "attachment": attachment,
111
+ }
112
+
113
+ # Fallback: Add file reference as comment
114
+ comment_text = f"Attachment: {file_path}"
115
+ if description:
116
+ comment_text += f"\nDescription: {description}"
117
+
118
+ comment = Comment(
119
+ ticket_id=ticket_id,
120
+ content=comment_text,
121
+ )
122
+
123
+ created_comment = await adapter.add_comment(comment)
124
+
125
+ return {
126
+ "status": "completed",
127
+ "ticket_id": ticket_id,
128
+ "method": "comment_reference",
129
+ "file_path": file_path,
130
+ "comment": created_comment.model_dump(),
131
+ "note": "Adapter does not support direct file uploads. File reference added as comment.",
132
+ }
133
+
134
+ except FileNotFoundError:
135
+ return {
136
+ "status": "error",
137
+ "error": f"File not found: {file_path}",
138
+ "ticket_id": ticket_id,
139
+ }
140
+ except Exception as e:
141
+ return {
142
+ "status": "error",
143
+ "error": f"Failed to attach file: {str(e)}",
144
+ "ticket_id": ticket_id,
145
+ }
146
+
147
+
148
+ @mcp.tool()
149
+ async def ticket_attachments(
150
+ ticket_id: str,
151
+ ) -> dict[str, Any]: # Keep as dict for MCP compatibility
152
+ """Get all attachments for a ticket.
153
+
154
+ Retrieves a list of all files attached to the specified ticket.
155
+ This functionality may not be available in all adapters.
156
+
157
+ Args:
158
+ ticket_id: Unique identifier of the ticket
159
+
160
+ Returns:
161
+ List of attachments with metadata, or error information
162
+
163
+ """
164
+ try:
165
+ adapter = get_adapter()
166
+
167
+ # Read ticket to validate it exists
168
+ ticket = await adapter.read(ticket_id)
169
+ if ticket is None:
170
+ return {
171
+ "status": "error",
172
+ "error": f"Ticket {ticket_id} not found",
173
+ }
174
+
175
+ # Check if adapter supports attachments
176
+ if not hasattr(adapter, "get_attachments"):
177
+ return {
178
+ "status": "error",
179
+ "error": f"Attachment retrieval not supported by {type(adapter).__name__} adapter",
180
+ "ticket_id": ticket_id,
181
+ "note": "Check ticket comments for file references",
182
+ }
183
+
184
+ # Get attachments via adapter
185
+ attachments = await adapter.get_attachments(ticket_id)
186
+
187
+ return {
188
+ "status": "completed",
189
+ "ticket_id": ticket_id,
190
+ "attachments": attachments,
191
+ "count": len(attachments) if isinstance(attachments, list) else 0,
192
+ }
193
+
194
+ except AttributeError:
195
+ # Fallback: Check comments for attachment references
196
+ comments = await adapter.get_comments(ticket_id=ticket_id, limit=100)
197
+
198
+ # Look for comments that reference files
199
+ attachment_refs = []
200
+ for comment in comments:
201
+ content = comment.content or ""
202
+ if content.startswith("Attachment:") or "file://" in content:
203
+ attachment_refs.append(
204
+ {
205
+ "type": "comment_reference",
206
+ "comment_id": comment.id,
207
+ "content": content,
208
+ "created_at": comment.created_at,
209
+ }
210
+ )
211
+
212
+ return {
213
+ "status": "completed",
214
+ "ticket_id": ticket_id,
215
+ "method": "comment_references",
216
+ "attachments": attachment_refs,
217
+ "count": len(attachment_refs),
218
+ "note": "Adapter does not support direct attachments. Showing file references from comments.",
219
+ }
220
+
221
+ except Exception as e:
222
+ return {
223
+ "status": "error",
224
+ "error": f"Failed to get attachments: {str(e)}",
225
+ "ticket_id": ticket_id,
226
+ }
@@ -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: dict[str, list[Any]] = {
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: dict[str, list[Any]] = {
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,152 @@
1
+ """Comment management tools for tickets.
2
+
3
+ This module implements tools for adding and retrieving comments on tickets.
4
+ """
5
+
6
+ import logging
7
+ from typing import Any
8
+
9
+ from ....core.adapter import BaseAdapter
10
+ from ....core.models import Comment
11
+ from ....core.url_parser import is_url
12
+ from ..server_sdk import get_adapter, get_router, has_router, mcp
13
+
14
+
15
+ def _build_adapter_metadata(
16
+ adapter: BaseAdapter,
17
+ ticket_id: str | None = None,
18
+ is_routed: bool = False,
19
+ ) -> dict[str, Any]:
20
+ """Build adapter metadata for MCP responses.
21
+
22
+ Args:
23
+ adapter: The adapter that handled the operation
24
+ ticket_id: Optional ticket ID to include in metadata
25
+ is_routed: Whether this was routed via URL detection
26
+
27
+ Returns:
28
+ Dictionary with adapter metadata fields
29
+
30
+ """
31
+ metadata = {
32
+ "adapter": adapter.adapter_type,
33
+ "adapter_name": adapter.adapter_display_name,
34
+ }
35
+
36
+ if ticket_id:
37
+ metadata["ticket_id"] = ticket_id
38
+
39
+ if is_routed:
40
+ metadata["routed_from_url"] = True
41
+
42
+ return metadata
43
+
44
+
45
+ @mcp.tool()
46
+ async def ticket_comment(
47
+ ticket_id: str,
48
+ operation: str,
49
+ text: str | None = None,
50
+ limit: int = 10,
51
+ offset: int = 0,
52
+ ) -> dict[str, Any]:
53
+ """Add or list comments on a ticket using ID or URL.
54
+
55
+ This tool supports two operations:
56
+ - 'add': Add a new comment to a ticket (requires 'text' parameter)
57
+ - 'list': Retrieve comments from a ticket (supports pagination)
58
+
59
+ Supports both plain ticket IDs and full URLs from multiple platforms.
60
+ See ticket_read for supported URL formats.
61
+
62
+ Args:
63
+ ticket_id: Ticket ID or URL
64
+ operation: Operation to perform - must be 'add' or 'list'
65
+ text: Comment text (required when operation='add')
66
+ limit: Maximum number of comments to return (used when operation='list', default: 10)
67
+ offset: Number of comments to skip for pagination (used when operation='list', default: 0)
68
+
69
+ Returns:
70
+ Comment data or list of comments, or error information
71
+
72
+ """
73
+ try:
74
+ # Validate operation
75
+ if operation not in ["add", "list"]:
76
+ return {
77
+ "status": "error",
78
+ "error": f"Invalid operation '{operation}'. Must be 'add' or 'list'",
79
+ }
80
+
81
+ if operation == "add":
82
+ # Add comment operation
83
+ if not text:
84
+ return {
85
+ "status": "error",
86
+ "error": "Parameter 'text' is required when operation='add'",
87
+ }
88
+
89
+ # Create comment object
90
+ comment = Comment(
91
+ ticket_id=ticket_id, # Will be normalized by router if URL
92
+ content=text,
93
+ )
94
+
95
+ # Route to appropriate adapter
96
+ is_routed = False
97
+ if is_url(ticket_id) and has_router():
98
+ router = get_router()
99
+ logging.info(f"Routing add_comment for URL: {ticket_id}")
100
+ created = await router.route_add_comment(ticket_id, comment)
101
+ is_routed = True
102
+ normalized_id, _, _ = router._normalize_ticket_id(ticket_id)
103
+ adapter = router._get_adapter(
104
+ router._detect_adapter_from_url(ticket_id)
105
+ )
106
+ else:
107
+ adapter = get_adapter()
108
+ created = await adapter.add_comment(comment)
109
+
110
+ return {
111
+ "status": "completed",
112
+ **_build_adapter_metadata(adapter, created.ticket_id, is_routed),
113
+ "operation": "add",
114
+ "comment": created.model_dump(),
115
+ }
116
+
117
+ else: # operation == "list"
118
+ # List comments operation
119
+ # Route to appropriate adapter
120
+ is_routed = False
121
+ if is_url(ticket_id) and has_router():
122
+ router = get_router()
123
+ logging.info(f"Routing get_comments for URL: {ticket_id}")
124
+ comments = await router.route_get_comments(
125
+ ticket_id, limit=limit, offset=offset
126
+ )
127
+ is_routed = True
128
+ normalized_id, _, _ = router._normalize_ticket_id(ticket_id)
129
+ adapter = router._get_adapter(
130
+ router._detect_adapter_from_url(ticket_id)
131
+ )
132
+ else:
133
+ adapter = get_adapter()
134
+ comments = await adapter.get_comments(
135
+ ticket_id=ticket_id, limit=limit, offset=offset
136
+ )
137
+
138
+ return {
139
+ "status": "completed",
140
+ **_build_adapter_metadata(adapter, ticket_id, is_routed),
141
+ "operation": "list",
142
+ "comments": [comment.model_dump() for comment in comments],
143
+ "count": len(comments),
144
+ "limit": limit,
145
+ "offset": offset,
146
+ }
147
+
148
+ except Exception as e:
149
+ return {
150
+ "status": "error",
151
+ "error": f"Comment operation failed: {str(e)}",
152
+ }