mcp-ticketer 0.2.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.
Files changed (160) hide show
  1. mcp_ticketer/__init__.py +10 -10
  2. mcp_ticketer/__version__.py +3 -3
  3. mcp_ticketer/_version_scm.py +1 -0
  4. mcp_ticketer/adapters/__init__.py +2 -0
  5. mcp_ticketer/adapters/aitrackdown.py +930 -52
  6. mcp_ticketer/adapters/asana/__init__.py +15 -0
  7. mcp_ticketer/adapters/asana/adapter.py +1537 -0
  8. mcp_ticketer/adapters/asana/client.py +292 -0
  9. mcp_ticketer/adapters/asana/mappers.py +348 -0
  10. mcp_ticketer/adapters/asana/types.py +146 -0
  11. mcp_ticketer/adapters/github/__init__.py +26 -0
  12. mcp_ticketer/adapters/github/adapter.py +3229 -0
  13. mcp_ticketer/adapters/github/client.py +335 -0
  14. mcp_ticketer/adapters/github/mappers.py +797 -0
  15. mcp_ticketer/adapters/github/queries.py +692 -0
  16. mcp_ticketer/adapters/github/types.py +460 -0
  17. mcp_ticketer/adapters/hybrid.py +58 -16
  18. mcp_ticketer/adapters/jira/__init__.py +35 -0
  19. mcp_ticketer/adapters/jira/adapter.py +1351 -0
  20. mcp_ticketer/adapters/jira/client.py +271 -0
  21. mcp_ticketer/adapters/jira/mappers.py +246 -0
  22. mcp_ticketer/adapters/jira/queries.py +216 -0
  23. mcp_ticketer/adapters/jira/types.py +304 -0
  24. mcp_ticketer/adapters/linear/__init__.py +1 -1
  25. mcp_ticketer/adapters/linear/adapter.py +3810 -462
  26. mcp_ticketer/adapters/linear/client.py +312 -69
  27. mcp_ticketer/adapters/linear/mappers.py +305 -85
  28. mcp_ticketer/adapters/linear/queries.py +317 -17
  29. mcp_ticketer/adapters/linear/types.py +187 -64
  30. mcp_ticketer/adapters/linear.py +2 -2
  31. mcp_ticketer/analysis/__init__.py +56 -0
  32. mcp_ticketer/analysis/dependency_graph.py +255 -0
  33. mcp_ticketer/analysis/health_assessment.py +304 -0
  34. mcp_ticketer/analysis/orphaned.py +218 -0
  35. mcp_ticketer/analysis/project_status.py +594 -0
  36. mcp_ticketer/analysis/similarity.py +224 -0
  37. mcp_ticketer/analysis/staleness.py +266 -0
  38. mcp_ticketer/automation/__init__.py +11 -0
  39. mcp_ticketer/automation/project_updates.py +378 -0
  40. mcp_ticketer/cache/memory.py +9 -8
  41. mcp_ticketer/cli/adapter_diagnostics.py +421 -0
  42. mcp_ticketer/cli/auggie_configure.py +116 -15
  43. mcp_ticketer/cli/codex_configure.py +274 -82
  44. mcp_ticketer/cli/configure.py +1323 -151
  45. mcp_ticketer/cli/cursor_configure.py +314 -0
  46. mcp_ticketer/cli/diagnostics.py +209 -114
  47. mcp_ticketer/cli/discover.py +297 -26
  48. mcp_ticketer/cli/gemini_configure.py +119 -26
  49. mcp_ticketer/cli/init_command.py +880 -0
  50. mcp_ticketer/cli/install_mcp_server.py +418 -0
  51. mcp_ticketer/cli/instruction_commands.py +435 -0
  52. mcp_ticketer/cli/linear_commands.py +256 -130
  53. mcp_ticketer/cli/main.py +140 -1284
  54. mcp_ticketer/cli/mcp_configure.py +1013 -100
  55. mcp_ticketer/cli/mcp_server_commands.py +415 -0
  56. mcp_ticketer/cli/migrate_config.py +12 -8
  57. mcp_ticketer/cli/platform_commands.py +123 -0
  58. mcp_ticketer/cli/platform_detection.py +477 -0
  59. mcp_ticketer/cli/platform_installer.py +545 -0
  60. mcp_ticketer/cli/project_update_commands.py +350 -0
  61. mcp_ticketer/cli/python_detection.py +126 -0
  62. mcp_ticketer/cli/queue_commands.py +15 -15
  63. mcp_ticketer/cli/setup_command.py +794 -0
  64. mcp_ticketer/cli/simple_health.py +84 -59
  65. mcp_ticketer/cli/ticket_commands.py +1375 -0
  66. mcp_ticketer/cli/update_checker.py +313 -0
  67. mcp_ticketer/cli/utils.py +195 -72
  68. mcp_ticketer/core/__init__.py +64 -1
  69. mcp_ticketer/core/adapter.py +618 -18
  70. mcp_ticketer/core/config.py +77 -68
  71. mcp_ticketer/core/env_discovery.py +75 -16
  72. mcp_ticketer/core/env_loader.py +121 -97
  73. mcp_ticketer/core/exceptions.py +32 -24
  74. mcp_ticketer/core/http_client.py +26 -26
  75. mcp_ticketer/core/instructions.py +405 -0
  76. mcp_ticketer/core/label_manager.py +732 -0
  77. mcp_ticketer/core/mappers.py +42 -30
  78. mcp_ticketer/core/milestone_manager.py +252 -0
  79. mcp_ticketer/core/models.py +566 -19
  80. mcp_ticketer/core/onepassword_secrets.py +379 -0
  81. mcp_ticketer/core/priority_matcher.py +463 -0
  82. mcp_ticketer/core/project_config.py +189 -49
  83. mcp_ticketer/core/project_utils.py +281 -0
  84. mcp_ticketer/core/project_validator.py +376 -0
  85. mcp_ticketer/core/registry.py +3 -3
  86. mcp_ticketer/core/session_state.py +176 -0
  87. mcp_ticketer/core/state_matcher.py +592 -0
  88. mcp_ticketer/core/url_parser.py +425 -0
  89. mcp_ticketer/core/validators.py +69 -0
  90. mcp_ticketer/defaults/ticket_instructions.md +644 -0
  91. mcp_ticketer/mcp/__init__.py +29 -1
  92. mcp_ticketer/mcp/__main__.py +60 -0
  93. mcp_ticketer/mcp/server/__init__.py +25 -0
  94. mcp_ticketer/mcp/server/__main__.py +60 -0
  95. mcp_ticketer/mcp/server/constants.py +58 -0
  96. mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
  97. mcp_ticketer/mcp/server/dto.py +195 -0
  98. mcp_ticketer/mcp/server/main.py +1343 -0
  99. mcp_ticketer/mcp/server/response_builder.py +206 -0
  100. mcp_ticketer/mcp/server/routing.py +723 -0
  101. mcp_ticketer/mcp/server/server_sdk.py +151 -0
  102. mcp_ticketer/mcp/server/tools/__init__.py +69 -0
  103. mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
  104. mcp_ticketer/mcp/server/tools/attachment_tools.py +224 -0
  105. mcp_ticketer/mcp/server/tools/bulk_tools.py +330 -0
  106. mcp_ticketer/mcp/server/tools/comment_tools.py +152 -0
  107. mcp_ticketer/mcp/server/tools/config_tools.py +1564 -0
  108. mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
  109. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +942 -0
  110. mcp_ticketer/mcp/server/tools/instruction_tools.py +295 -0
  111. mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
  112. mcp_ticketer/mcp/server/tools/milestone_tools.py +338 -0
  113. mcp_ticketer/mcp/server/tools/pr_tools.py +150 -0
  114. mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
  115. mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
  116. mcp_ticketer/mcp/server/tools/search_tools.py +318 -0
  117. mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
  118. mcp_ticketer/mcp/server/tools/ticket_tools.py +1413 -0
  119. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +364 -0
  120. mcp_ticketer/queue/__init__.py +1 -0
  121. mcp_ticketer/queue/health_monitor.py +168 -136
  122. mcp_ticketer/queue/manager.py +78 -63
  123. mcp_ticketer/queue/queue.py +108 -21
  124. mcp_ticketer/queue/run_worker.py +2 -2
  125. mcp_ticketer/queue/ticket_registry.py +213 -155
  126. mcp_ticketer/queue/worker.py +96 -58
  127. mcp_ticketer/utils/__init__.py +5 -0
  128. mcp_ticketer/utils/token_utils.py +246 -0
  129. mcp_ticketer-2.2.9.dist-info/METADATA +1396 -0
  130. mcp_ticketer-2.2.9.dist-info/RECORD +158 -0
  131. mcp_ticketer-2.2.9.dist-info/top_level.txt +2 -0
  132. py_mcp_installer/examples/phase3_demo.py +178 -0
  133. py_mcp_installer/scripts/manage_version.py +54 -0
  134. py_mcp_installer/setup.py +6 -0
  135. py_mcp_installer/src/py_mcp_installer/__init__.py +153 -0
  136. py_mcp_installer/src/py_mcp_installer/command_builder.py +445 -0
  137. py_mcp_installer/src/py_mcp_installer/config_manager.py +541 -0
  138. py_mcp_installer/src/py_mcp_installer/exceptions.py +243 -0
  139. py_mcp_installer/src/py_mcp_installer/installation_strategy.py +617 -0
  140. py_mcp_installer/src/py_mcp_installer/installer.py +656 -0
  141. py_mcp_installer/src/py_mcp_installer/mcp_inspector.py +750 -0
  142. py_mcp_installer/src/py_mcp_installer/platform_detector.py +451 -0
  143. py_mcp_installer/src/py_mcp_installer/platforms/__init__.py +26 -0
  144. py_mcp_installer/src/py_mcp_installer/platforms/claude_code.py +225 -0
  145. py_mcp_installer/src/py_mcp_installer/platforms/codex.py +181 -0
  146. py_mcp_installer/src/py_mcp_installer/platforms/cursor.py +191 -0
  147. py_mcp_installer/src/py_mcp_installer/types.py +222 -0
  148. py_mcp_installer/src/py_mcp_installer/utils.py +463 -0
  149. py_mcp_installer/tests/__init__.py +0 -0
  150. py_mcp_installer/tests/platforms/__init__.py +0 -0
  151. py_mcp_installer/tests/test_platform_detector.py +17 -0
  152. mcp_ticketer/adapters/github.py +0 -1354
  153. mcp_ticketer/adapters/jira.py +0 -1011
  154. mcp_ticketer/mcp/server.py +0 -1895
  155. mcp_ticketer-0.2.0.dist-info/METADATA +0 -414
  156. mcp_ticketer-0.2.0.dist-info/RECORD +0 -58
  157. mcp_ticketer-0.2.0.dist-info/top_level.txt +0 -1
  158. {mcp_ticketer-0.2.0.dist-info → mcp_ticketer-2.2.9.dist-info}/WHEEL +0 -0
  159. {mcp_ticketer-0.2.0.dist-info → mcp_ticketer-2.2.9.dist-info}/entry_points.txt +0 -0
  160. {mcp_ticketer-0.2.0.dist-info → mcp_ticketer-2.2.9.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,224 @@
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
14
+
15
+
16
+ async def ticket_attach(
17
+ ticket_id: str,
18
+ file_path: str,
19
+ description: str = "",
20
+ ) -> dict[str, Any]: # Keep as dict for MCP compatibility
21
+ """Attach a file to a ticket.
22
+
23
+ Uploads a file and associates it with the specified ticket. This
24
+ functionality may not be available in all adapters.
25
+
26
+ Args:
27
+ ticket_id: Unique identifier of the ticket
28
+ file_path: Path to the file to attach
29
+ description: Optional description of the attachment
30
+
31
+ Returns:
32
+ Attachment details including URL or ID, or error information
33
+
34
+ """
35
+ try:
36
+ adapter = get_adapter()
37
+
38
+ # Read ticket to validate it exists and determine type
39
+ ticket = await adapter.read(ticket_id)
40
+ if ticket is None:
41
+ return {
42
+ "status": "error",
43
+ "error": f"Ticket {ticket_id} not found",
44
+ }
45
+
46
+ # Check if file exists
47
+ file_path_obj = Path(file_path)
48
+ if not file_path_obj.exists():
49
+ return {
50
+ "status": "error",
51
+ "error": f"File not found: {file_path}",
52
+ "ticket_id": ticket_id,
53
+ }
54
+
55
+ # Try Linear-specific upload methods first (most advanced)
56
+ if hasattr(adapter, "upload_file") and hasattr(adapter, "attach_file_to_issue"):
57
+ try:
58
+ # Determine MIME type
59
+ mime_type = mimetypes.guess_type(file_path)[0]
60
+
61
+ # Upload file to Linear's storage
62
+ file_url = await adapter.upload_file(file_path, mime_type)
63
+
64
+ # Determine ticket type and attach accordingly
65
+ ticket_type = getattr(ticket, "ticket_type", None)
66
+ filename = file_path_obj.name
67
+
68
+ if ticket_type == TicketType.EPIC and hasattr(
69
+ adapter, "attach_file_to_epic"
70
+ ):
71
+ # Attach to epic (project)
72
+ result = await adapter.attach_file_to_epic(
73
+ epic_id=ticket_id,
74
+ file_url=file_url,
75
+ title=description or filename,
76
+ subtitle=f"Uploaded file: {filename}",
77
+ )
78
+ else:
79
+ # Attach to issue/task
80
+ result = await adapter.attach_file_to_issue(
81
+ issue_id=ticket_id,
82
+ file_url=file_url,
83
+ title=description or filename,
84
+ subtitle=f"Uploaded file: {filename}",
85
+ comment_body=description if description else None,
86
+ )
87
+
88
+ return {
89
+ "status": "completed",
90
+ "ticket_id": ticket_id,
91
+ "method": "linear_native_upload",
92
+ "file_url": file_url,
93
+ "attachment": result,
94
+ }
95
+ except Exception:
96
+ # Fall through to legacy method if Linear-specific upload fails
97
+ pass
98
+
99
+ # Try legacy add_attachment method
100
+ if hasattr(adapter, "add_attachment"):
101
+ attachment = await adapter.add_attachment(
102
+ ticket_id=ticket_id, file_path=file_path, description=description
103
+ )
104
+
105
+ return {
106
+ "status": "completed",
107
+ "ticket_id": ticket_id,
108
+ "method": "adapter_native",
109
+ "attachment": attachment,
110
+ }
111
+
112
+ # Fallback: Add file reference as comment
113
+ comment_text = f"Attachment: {file_path}"
114
+ if description:
115
+ comment_text += f"\nDescription: {description}"
116
+
117
+ comment = Comment(
118
+ ticket_id=ticket_id,
119
+ content=comment_text,
120
+ )
121
+
122
+ created_comment = await adapter.add_comment(comment)
123
+
124
+ return {
125
+ "status": "completed",
126
+ "ticket_id": ticket_id,
127
+ "method": "comment_reference",
128
+ "file_path": file_path,
129
+ "comment": created_comment.model_dump(),
130
+ "note": "Adapter does not support direct file uploads. File reference added as comment.",
131
+ }
132
+
133
+ except FileNotFoundError:
134
+ return {
135
+ "status": "error",
136
+ "error": f"File not found: {file_path}",
137
+ "ticket_id": ticket_id,
138
+ }
139
+ except Exception as e:
140
+ return {
141
+ "status": "error",
142
+ "error": f"Failed to attach file: {str(e)}",
143
+ "ticket_id": ticket_id,
144
+ }
145
+
146
+
147
+ async def ticket_attachments(
148
+ ticket_id: str,
149
+ ) -> dict[str, Any]: # Keep as dict for MCP compatibility
150
+ """Get all attachments for a ticket.
151
+
152
+ Retrieves a list of all files attached to the specified ticket.
153
+ This functionality may not be available in all adapters.
154
+
155
+ Args:
156
+ ticket_id: Unique identifier of the ticket
157
+
158
+ Returns:
159
+ List of attachments with metadata, or error information
160
+
161
+ """
162
+ try:
163
+ adapter = get_adapter()
164
+
165
+ # Read ticket to validate it exists
166
+ ticket = await adapter.read(ticket_id)
167
+ if ticket is None:
168
+ return {
169
+ "status": "error",
170
+ "error": f"Ticket {ticket_id} not found",
171
+ }
172
+
173
+ # Check if adapter supports attachments
174
+ if not hasattr(adapter, "get_attachments"):
175
+ return {
176
+ "status": "error",
177
+ "error": f"Attachment retrieval not supported by {type(adapter).__name__} adapter",
178
+ "ticket_id": ticket_id,
179
+ "note": "Check ticket comments for file references",
180
+ }
181
+
182
+ # Get attachments via adapter
183
+ attachments = await adapter.get_attachments(ticket_id)
184
+
185
+ return {
186
+ "status": "completed",
187
+ "ticket_id": ticket_id,
188
+ "attachments": attachments,
189
+ "count": len(attachments) if isinstance(attachments, list) else 0,
190
+ }
191
+
192
+ except AttributeError:
193
+ # Fallback: Check comments for attachment references
194
+ comments = await adapter.get_comments(ticket_id=ticket_id, limit=100)
195
+
196
+ # Look for comments that reference files
197
+ attachment_refs = []
198
+ for comment in comments:
199
+ content = comment.content or ""
200
+ if content.startswith("Attachment:") or "file://" in content:
201
+ attachment_refs.append(
202
+ {
203
+ "type": "comment_reference",
204
+ "comment_id": comment.id,
205
+ "content": content,
206
+ "created_at": comment.created_at,
207
+ }
208
+ )
209
+
210
+ return {
211
+ "status": "completed",
212
+ "ticket_id": ticket_id,
213
+ "method": "comment_references",
214
+ "attachments": attachment_refs,
215
+ "count": len(attachment_refs),
216
+ "note": "Adapter does not support direct attachments. Showing file references from comments.",
217
+ }
218
+
219
+ except Exception as e:
220
+ return {
221
+ "status": "error",
222
+ "error": f"Failed to get attachments: {str(e)}",
223
+ "ticket_id": ticket_id,
224
+ }
@@ -0,0 +1,330 @@
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
+ Features:
7
+ - ticket_bulk: Unified interface for all bulk operations (create, update)
8
+
9
+ All tools follow the MCP response pattern:
10
+ {
11
+ "status": "completed" | "error",
12
+ "summary": {"total": N, "created": N, "updated": N, "failed": N},
13
+ "results": {...}
14
+ }
15
+ """
16
+
17
+ from typing import Any
18
+
19
+ from ....core.models import Priority, Task, TicketState, TicketType
20
+ from ..server_sdk import get_adapter, mcp
21
+
22
+
23
+ @mcp.tool()
24
+ async def ticket_bulk(
25
+ action: str,
26
+ tickets: list[dict[str, Any]] | None = None,
27
+ updates: list[dict[str, Any]] | None = None,
28
+ ) -> dict[str, Any]:
29
+ """Unified bulk ticket operations tool.
30
+
31
+ Performs bulk create or update operations on tickets through a single
32
+ interface.
33
+
34
+ Args:
35
+ action: Operation to perform. Valid values:
36
+ - "create": Create multiple new tickets
37
+ - "update": Update multiple existing tickets
38
+ tickets: List of ticket dicts for bulk create (required if action="create")
39
+ Each dict should contain at minimum 'title', with optional fields:
40
+ description, priority, tags, assignee, ticket_type, parent_epic, parent_issue
41
+ updates: List of update dicts for bulk update (required if action="update")
42
+ Each dict must contain 'ticket_id' and at least one field to update.
43
+ Valid update fields: title, description, priority, state, assignee, tags
44
+
45
+ Returns:
46
+ Results dictionary containing:
47
+ - status: "completed" or "error"
48
+ - summary: Statistics (total, created/updated, failed)
49
+ - results: Detailed results for each operation
50
+
51
+ Raises:
52
+ ValueError: If action is invalid or required parameters missing
53
+
54
+ Examples:
55
+ # Bulk create
56
+ result = await ticket_bulk(
57
+ action="create",
58
+ tickets=[
59
+ {"title": "Bug 1", "priority": "high", "description": "Fix login"},
60
+ {"title": "Bug 2", "priority": "medium", "tags": ["backend"]}
61
+ ]
62
+ )
63
+
64
+ # Bulk update
65
+ result = await ticket_bulk(
66
+ action="update",
67
+ updates=[
68
+ {"ticket_id": "PROJ-123", "state": "done", "priority": "low"},
69
+ {"ticket_id": "PROJ-456", "assignee": "user@example.com"}
70
+ ]
71
+ )
72
+
73
+ See: docs/mcp-api-reference.md for detailed response formats
74
+ """
75
+ action_lower = action.lower()
76
+
77
+ # Route to appropriate handler based on action
78
+ if action_lower == "create":
79
+ if tickets is None:
80
+ return {
81
+ "status": "error",
82
+ "error": "tickets parameter required for action='create'",
83
+ "hint": "Use ticket_bulk(action='create', tickets=[...])",
84
+ }
85
+ # Inline implementation of bulk create
86
+ try:
87
+ adapter = get_adapter()
88
+
89
+ if not tickets:
90
+ return {
91
+ "status": "error",
92
+ "error": "No tickets provided for bulk creation",
93
+ }
94
+
95
+ results: dict[str, list[Any]] = {
96
+ "created": [],
97
+ "failed": [],
98
+ }
99
+
100
+ for i, ticket_data in enumerate(tickets):
101
+ try:
102
+ # Validate required fields
103
+ if "title" not in ticket_data:
104
+ results["failed"].append(
105
+ {
106
+ "index": i,
107
+ "error": "Missing required field: title",
108
+ "data": ticket_data,
109
+ }
110
+ )
111
+ continue
112
+
113
+ # Parse priority if provided
114
+ priority = Priority.MEDIUM # Default
115
+ if "priority" in ticket_data:
116
+ try:
117
+ priority = Priority(ticket_data["priority"].lower())
118
+ except ValueError:
119
+ results["failed"].append(
120
+ {
121
+ "index": i,
122
+ "error": f"Invalid priority: {ticket_data['priority']}",
123
+ "data": ticket_data,
124
+ }
125
+ )
126
+ continue
127
+
128
+ # Parse ticket type if provided
129
+ ticket_type = TicketType.ISSUE # Default
130
+ if "ticket_type" in ticket_data:
131
+ try:
132
+ ticket_type = TicketType(ticket_data["ticket_type"].lower())
133
+ except ValueError:
134
+ results["failed"].append(
135
+ {
136
+ "index": i,
137
+ "error": f"Invalid ticket_type: {ticket_data['ticket_type']}",
138
+ "data": ticket_data,
139
+ }
140
+ )
141
+ continue
142
+
143
+ # Create task object
144
+ task = Task(
145
+ title=ticket_data["title"],
146
+ description=ticket_data.get("description", ""),
147
+ priority=priority,
148
+ ticket_type=ticket_type,
149
+ tags=ticket_data.get("tags", []),
150
+ assignee=ticket_data.get("assignee"),
151
+ parent_epic=ticket_data.get("parent_epic"),
152
+ parent_issue=ticket_data.get("parent_issue"),
153
+ )
154
+
155
+ # Create via adapter
156
+ created = await adapter.create(task)
157
+ results["created"].append(
158
+ {
159
+ "index": i,
160
+ "ticket": created.model_dump(),
161
+ }
162
+ )
163
+
164
+ except Exception as e:
165
+ results["failed"].append(
166
+ {
167
+ "index": i,
168
+ "error": str(e),
169
+ "data": ticket_data,
170
+ }
171
+ )
172
+
173
+ return {
174
+ "status": "completed",
175
+ "summary": {
176
+ "total": len(tickets),
177
+ "created": len(results["created"]),
178
+ "failed": len(results["failed"]),
179
+ },
180
+ "results": results,
181
+ }
182
+
183
+ except Exception as e:
184
+ return {
185
+ "status": "error",
186
+ "error": f"Bulk creation failed: {str(e)}",
187
+ }
188
+
189
+ elif action_lower == "update":
190
+ if updates is None:
191
+ return {
192
+ "status": "error",
193
+ "error": "updates parameter required for action='update'",
194
+ "hint": "Use ticket_bulk(action='update', updates=[...])",
195
+ }
196
+ # Inline implementation of bulk update
197
+ try:
198
+ adapter = get_adapter()
199
+
200
+ if not updates:
201
+ return {
202
+ "status": "error",
203
+ "error": "No updates provided for bulk operation",
204
+ }
205
+
206
+ results: dict[str, list[Any]] = {
207
+ "updated": [],
208
+ "failed": [],
209
+ }
210
+
211
+ for i, update_data in enumerate(updates):
212
+ try:
213
+ # Validate required fields
214
+ if "ticket_id" not in update_data:
215
+ results["failed"].append(
216
+ {
217
+ "index": i,
218
+ "error": "Missing required field: ticket_id",
219
+ "data": update_data,
220
+ }
221
+ )
222
+ continue
223
+
224
+ ticket_id = update_data["ticket_id"]
225
+
226
+ # Build update dict
227
+ update_fields: dict[str, Any] = {}
228
+
229
+ if "title" in update_data:
230
+ update_fields["title"] = update_data["title"]
231
+ if "description" in update_data:
232
+ update_fields["description"] = update_data["description"]
233
+ if "assignee" in update_data:
234
+ update_fields["assignee"] = update_data["assignee"]
235
+ if "tags" in update_data:
236
+ update_fields["tags"] = update_data["tags"]
237
+
238
+ # Parse priority if provided
239
+ if "priority" in update_data:
240
+ try:
241
+ update_fields["priority"] = Priority(
242
+ update_data["priority"].lower()
243
+ )
244
+ except ValueError:
245
+ results["failed"].append(
246
+ {
247
+ "index": i,
248
+ "error": f"Invalid priority: {update_data['priority']}",
249
+ "data": update_data,
250
+ }
251
+ )
252
+ continue
253
+
254
+ # Parse state if provided
255
+ if "state" in update_data:
256
+ try:
257
+ update_fields["state"] = TicketState(
258
+ update_data["state"].lower()
259
+ )
260
+ except ValueError:
261
+ results["failed"].append(
262
+ {
263
+ "index": i,
264
+ "error": f"Invalid state: {update_data['state']}",
265
+ "data": update_data,
266
+ }
267
+ )
268
+ continue
269
+
270
+ if not update_fields:
271
+ results["failed"].append(
272
+ {
273
+ "index": i,
274
+ "error": "No valid update fields provided",
275
+ "data": update_data,
276
+ }
277
+ )
278
+ continue
279
+
280
+ # Update via adapter
281
+ updated = await adapter.update(ticket_id, update_fields)
282
+ if updated is None:
283
+ results["failed"].append(
284
+ {
285
+ "index": i,
286
+ "error": f"Ticket {ticket_id} not found or update failed",
287
+ "data": update_data,
288
+ }
289
+ )
290
+ else:
291
+ results["updated"].append(
292
+ {
293
+ "index": i,
294
+ "ticket": updated.model_dump(),
295
+ }
296
+ )
297
+
298
+ except Exception as e:
299
+ results["failed"].append(
300
+ {
301
+ "index": i,
302
+ "error": str(e),
303
+ "data": update_data,
304
+ }
305
+ )
306
+
307
+ return {
308
+ "status": "completed",
309
+ "summary": {
310
+ "total": len(updates),
311
+ "updated": len(results["updated"]),
312
+ "failed": len(results["failed"]),
313
+ },
314
+ "results": results,
315
+ }
316
+
317
+ except Exception as e:
318
+ return {
319
+ "status": "error",
320
+ "error": f"Bulk update failed: {str(e)}",
321
+ }
322
+
323
+ else:
324
+ valid_actions = ["create", "update"]
325
+ return {
326
+ "status": "error",
327
+ "error": f"Invalid action '{action}'. Must be one of: {', '.join(valid_actions)}",
328
+ "valid_actions": valid_actions,
329
+ "hint": "Use ticket_bulk(action='create'|'update', ...)",
330
+ }