mcp-ticketer 0.12.0__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.

Files changed (129) hide show
  1. mcp_ticketer/__init__.py +10 -10
  2. mcp_ticketer/__version__.py +1 -1
  3. mcp_ticketer/_version_scm.py +1 -0
  4. mcp_ticketer/adapters/aitrackdown.py +507 -6
  5. mcp_ticketer/adapters/asana/adapter.py +229 -0
  6. mcp_ticketer/adapters/asana/mappers.py +14 -0
  7. mcp_ticketer/adapters/github/__init__.py +26 -0
  8. mcp_ticketer/adapters/github/adapter.py +3229 -0
  9. mcp_ticketer/adapters/github/client.py +335 -0
  10. mcp_ticketer/adapters/github/mappers.py +797 -0
  11. mcp_ticketer/adapters/github/queries.py +692 -0
  12. mcp_ticketer/adapters/github/types.py +460 -0
  13. mcp_ticketer/adapters/hybrid.py +47 -5
  14. mcp_ticketer/adapters/jira/__init__.py +35 -0
  15. mcp_ticketer/adapters/jira/adapter.py +1351 -0
  16. mcp_ticketer/adapters/jira/client.py +271 -0
  17. mcp_ticketer/adapters/jira/mappers.py +246 -0
  18. mcp_ticketer/adapters/jira/queries.py +216 -0
  19. mcp_ticketer/adapters/jira/types.py +304 -0
  20. mcp_ticketer/adapters/linear/adapter.py +2730 -139
  21. mcp_ticketer/adapters/linear/client.py +175 -3
  22. mcp_ticketer/adapters/linear/mappers.py +203 -8
  23. mcp_ticketer/adapters/linear/queries.py +280 -3
  24. mcp_ticketer/adapters/linear/types.py +120 -4
  25. mcp_ticketer/analysis/__init__.py +56 -0
  26. mcp_ticketer/analysis/dependency_graph.py +255 -0
  27. mcp_ticketer/analysis/health_assessment.py +304 -0
  28. mcp_ticketer/analysis/orphaned.py +218 -0
  29. mcp_ticketer/analysis/project_status.py +594 -0
  30. mcp_ticketer/analysis/similarity.py +224 -0
  31. mcp_ticketer/analysis/staleness.py +266 -0
  32. mcp_ticketer/automation/__init__.py +11 -0
  33. mcp_ticketer/automation/project_updates.py +378 -0
  34. mcp_ticketer/cli/adapter_diagnostics.py +3 -1
  35. mcp_ticketer/cli/auggie_configure.py +17 -5
  36. mcp_ticketer/cli/codex_configure.py +97 -61
  37. mcp_ticketer/cli/configure.py +1288 -105
  38. mcp_ticketer/cli/cursor_configure.py +314 -0
  39. mcp_ticketer/cli/diagnostics.py +13 -12
  40. mcp_ticketer/cli/discover.py +5 -0
  41. mcp_ticketer/cli/gemini_configure.py +17 -5
  42. mcp_ticketer/cli/init_command.py +880 -0
  43. mcp_ticketer/cli/install_mcp_server.py +418 -0
  44. mcp_ticketer/cli/instruction_commands.py +6 -0
  45. mcp_ticketer/cli/main.py +267 -3175
  46. mcp_ticketer/cli/mcp_configure.py +821 -119
  47. mcp_ticketer/cli/mcp_server_commands.py +415 -0
  48. mcp_ticketer/cli/platform_detection.py +77 -12
  49. mcp_ticketer/cli/platform_installer.py +545 -0
  50. mcp_ticketer/cli/project_update_commands.py +350 -0
  51. mcp_ticketer/cli/setup_command.py +795 -0
  52. mcp_ticketer/cli/simple_health.py +12 -10
  53. mcp_ticketer/cli/ticket_commands.py +705 -103
  54. mcp_ticketer/cli/utils.py +113 -0
  55. mcp_ticketer/core/__init__.py +56 -6
  56. mcp_ticketer/core/adapter.py +533 -2
  57. mcp_ticketer/core/config.py +21 -21
  58. mcp_ticketer/core/exceptions.py +7 -1
  59. mcp_ticketer/core/label_manager.py +732 -0
  60. mcp_ticketer/core/mappers.py +31 -19
  61. mcp_ticketer/core/milestone_manager.py +252 -0
  62. mcp_ticketer/core/models.py +480 -0
  63. mcp_ticketer/core/onepassword_secrets.py +1 -1
  64. mcp_ticketer/core/priority_matcher.py +463 -0
  65. mcp_ticketer/core/project_config.py +132 -14
  66. mcp_ticketer/core/project_utils.py +281 -0
  67. mcp_ticketer/core/project_validator.py +376 -0
  68. mcp_ticketer/core/session_state.py +176 -0
  69. mcp_ticketer/core/state_matcher.py +625 -0
  70. mcp_ticketer/core/url_parser.py +425 -0
  71. mcp_ticketer/core/validators.py +69 -0
  72. mcp_ticketer/mcp/server/__main__.py +2 -1
  73. mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
  74. mcp_ticketer/mcp/server/main.py +106 -25
  75. mcp_ticketer/mcp/server/routing.py +723 -0
  76. mcp_ticketer/mcp/server/server_sdk.py +58 -0
  77. mcp_ticketer/mcp/server/tools/__init__.py +33 -11
  78. mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
  79. mcp_ticketer/mcp/server/tools/attachment_tools.py +5 -5
  80. mcp_ticketer/mcp/server/tools/bulk_tools.py +259 -202
  81. mcp_ticketer/mcp/server/tools/comment_tools.py +74 -12
  82. mcp_ticketer/mcp/server/tools/config_tools.py +1391 -145
  83. mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
  84. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +870 -460
  85. mcp_ticketer/mcp/server/tools/instruction_tools.py +7 -5
  86. mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
  87. mcp_ticketer/mcp/server/tools/milestone_tools.py +338 -0
  88. mcp_ticketer/mcp/server/tools/pr_tools.py +3 -7
  89. mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
  90. mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
  91. mcp_ticketer/mcp/server/tools/search_tools.py +209 -97
  92. mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
  93. mcp_ticketer/mcp/server/tools/ticket_tools.py +1107 -124
  94. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +218 -236
  95. mcp_ticketer/queue/queue.py +68 -0
  96. mcp_ticketer/queue/worker.py +1 -1
  97. mcp_ticketer/utils/__init__.py +5 -0
  98. mcp_ticketer/utils/token_utils.py +246 -0
  99. mcp_ticketer-2.2.13.dist-info/METADATA +1396 -0
  100. mcp_ticketer-2.2.13.dist-info/RECORD +158 -0
  101. mcp_ticketer-2.2.13.dist-info/top_level.txt +2 -0
  102. py_mcp_installer/examples/phase3_demo.py +178 -0
  103. py_mcp_installer/scripts/manage_version.py +54 -0
  104. py_mcp_installer/setup.py +6 -0
  105. py_mcp_installer/src/py_mcp_installer/__init__.py +153 -0
  106. py_mcp_installer/src/py_mcp_installer/command_builder.py +445 -0
  107. py_mcp_installer/src/py_mcp_installer/config_manager.py +541 -0
  108. py_mcp_installer/src/py_mcp_installer/exceptions.py +243 -0
  109. py_mcp_installer/src/py_mcp_installer/installation_strategy.py +617 -0
  110. py_mcp_installer/src/py_mcp_installer/installer.py +656 -0
  111. py_mcp_installer/src/py_mcp_installer/mcp_inspector.py +750 -0
  112. py_mcp_installer/src/py_mcp_installer/platform_detector.py +451 -0
  113. py_mcp_installer/src/py_mcp_installer/platforms/__init__.py +26 -0
  114. py_mcp_installer/src/py_mcp_installer/platforms/claude_code.py +225 -0
  115. py_mcp_installer/src/py_mcp_installer/platforms/codex.py +181 -0
  116. py_mcp_installer/src/py_mcp_installer/platforms/cursor.py +191 -0
  117. py_mcp_installer/src/py_mcp_installer/types.py +222 -0
  118. py_mcp_installer/src/py_mcp_installer/utils.py +463 -0
  119. py_mcp_installer/tests/__init__.py +0 -0
  120. py_mcp_installer/tests/platforms/__init__.py +0 -0
  121. py_mcp_installer/tests/test_platform_detector.py +17 -0
  122. mcp_ticketer/adapters/github.py +0 -1574
  123. mcp_ticketer/adapters/jira.py +0 -1258
  124. mcp_ticketer-0.12.0.dist-info/METADATA +0 -550
  125. mcp_ticketer-0.12.0.dist-info/RECORD +0 -91
  126. mcp_ticketer-0.12.0.dist-info/top_level.txt +0 -1
  127. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.2.13.dist-info}/WHEEL +0 -0
  128. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.2.13.dist-info}/entry_points.txt +0 -0
  129. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.2.13.dist-info}/licenses/LICENSE +0 -0
@@ -60,7 +60,7 @@ async def ticket_attach(
60
60
  mime_type = mimetypes.guess_type(file_path)[0]
61
61
 
62
62
  # Upload file to Linear's storage
63
- file_url = await adapter.upload_file(file_path, mime_type) # type: ignore
63
+ file_url = await adapter.upload_file(file_path, mime_type)
64
64
 
65
65
  # Determine ticket type and attach accordingly
66
66
  ticket_type = getattr(ticket, "ticket_type", None)
@@ -70,7 +70,7 @@ async def ticket_attach(
70
70
  adapter, "attach_file_to_epic"
71
71
  ):
72
72
  # Attach to epic (project)
73
- result = await adapter.attach_file_to_epic( # type: ignore
73
+ result = await adapter.attach_file_to_epic(
74
74
  epic_id=ticket_id,
75
75
  file_url=file_url,
76
76
  title=description or filename,
@@ -78,7 +78,7 @@ async def ticket_attach(
78
78
  )
79
79
  else:
80
80
  # Attach to issue/task
81
- result = await adapter.attach_file_to_issue( # type: ignore
81
+ result = await adapter.attach_file_to_issue(
82
82
  issue_id=ticket_id,
83
83
  file_url=file_url,
84
84
  title=description or filename,
@@ -99,7 +99,7 @@ async def ticket_attach(
99
99
 
100
100
  # Try legacy add_attachment method
101
101
  if hasattr(adapter, "add_attachment"):
102
- attachment = await adapter.add_attachment( # type: ignore
102
+ attachment = await adapter.add_attachment(
103
103
  ticket_id=ticket_id, file_path=file_path, description=description
104
104
  )
105
105
 
@@ -182,7 +182,7 @@ async def ticket_attachments(
182
182
  }
183
183
 
184
184
  # Get attachments via adapter
185
- attachments = await adapter.get_attachments(ticket_id) # type: ignore
185
+ attachments = await adapter.get_attachments(ticket_id)
186
186
 
187
187
  return {
188
188
  "status": "completed",
@@ -2,6 +2,16 @@
2
2
 
3
3
  This module implements tools for batch operations on tickets to improve
4
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
+ }
5
15
  """
6
16
 
7
17
  from typing import Any
@@ -11,263 +21,310 @@ from ..server_sdk import get_adapter, mcp
11
21
 
12
22
 
13
23
  @mcp.tool()
14
- async def ticket_bulk_create(
15
- tickets: list[dict[str, Any]],
24
+ async def ticket_bulk(
25
+ action: str,
26
+ tickets: list[dict[str, Any]] | None = None,
27
+ updates: list[dict[str, Any]] | None = None,
16
28
  ) -> dict[str, Any]:
17
- """Create multiple tickets in a single operation.
29
+ """Unified bulk ticket operations tool.
18
30
 
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.
31
+ Performs bulk create or update operations on tickets through a single
32
+ interface.
21
33
 
22
34
  Args:
23
- tickets: List of ticket dictionaries to create
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
24
44
 
25
45
  Returns:
26
- Results of bulk creation including successes and failures
27
-
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
28
74
  """
29
- try:
30
- adapter = get_adapter()
75
+ action_lower = action.lower()
31
76
 
32
- if not tickets:
77
+ # Route to appropriate handler based on action
78
+ if action_lower == "create":
79
+ if tickets is None:
33
80
  return {
34
81
  "status": "error",
35
- "error": "No tickets provided for bulk creation",
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": [],
36
98
  }
37
99
 
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:
100
+ for i, ticket_data in enumerate(tickets):
101
+ try:
102
+ # Validate required fields
103
+ if "title" not in ticket_data:
77
104
  results["failed"].append(
78
105
  {
79
106
  "index": i,
80
- "error": f"Invalid ticket_type: {ticket_data['ticket_type']}",
107
+ "error": "Missing required field: title",
81
108
  "data": ticket_data,
82
109
  }
83
110
  )
84
111
  continue
85
112
 
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.
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
+ )
141
154
 
142
- Args:
143
- updates: List of update operation dictionaries
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
+ )
144
163
 
145
- Returns:
146
- Results of bulk update including successes and failures
164
+ except Exception as e:
165
+ results["failed"].append(
166
+ {
167
+ "index": i,
168
+ "error": str(e),
169
+ "data": ticket_data,
170
+ }
171
+ )
147
172
 
148
- """
149
- try:
150
- adapter = get_adapter()
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
+ }
151
182
 
152
- if not updates:
183
+ except Exception as e:
153
184
  return {
154
185
  "status": "error",
155
- "error": "No updates provided for bulk operation",
186
+ "error": f"Bulk creation failed: {str(e)}",
156
187
  }
157
188
 
158
- results = {
159
- "updated": [],
160
- "failed": [],
161
- }
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
+ }
162
210
 
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:
211
+ for i, update_data in enumerate(updates):
212
+ try:
213
+ # Validate required fields
214
+ if "ticket_id" not in update_data:
197
215
  results["failed"].append(
198
216
  {
199
217
  "index": i,
200
- "error": f"Invalid priority: {update_data['priority']}",
218
+ "error": "Missing required field: ticket_id",
201
219
  "data": update_data,
202
220
  }
203
221
  )
204
222
  continue
205
223
 
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:
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:
213
271
  results["failed"].append(
214
272
  {
215
273
  "index": i,
216
- "error": f"Invalid state: {update_data['state']}",
274
+ "error": "No valid update fields provided",
217
275
  "data": update_data,
218
276
  }
219
277
  )
220
278
  continue
221
279
 
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
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
+ )
231
297
 
232
- # Update via adapter
233
- updated = await adapter.update(ticket_id, update_fields)
234
- if updated is None:
298
+ except Exception as e:
235
299
  results["failed"].append(
236
300
  {
237
301
  "index": i,
238
- "error": f"Ticket {ticket_id} not found or update failed",
302
+ "error": str(e),
239
303
  "data": update_data,
240
304
  }
241
305
  )
242
- else:
243
- results["updated"].append(
244
- {
245
- "index": i,
246
- "ticket": updated.model_dump(),
247
- }
248
- )
249
306
 
250
- except Exception as e:
251
- results["failed"].append(
252
- {
253
- "index": i,
254
- "error": str(e),
255
- "data": update_data,
256
- }
257
- )
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
+ }
258
316
 
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
- }
317
+ except Exception as e:
318
+ return {
319
+ "status": "error",
320
+ "error": f"Bulk update failed: {str(e)}",
321
+ }
268
322
 
269
- except Exception as e:
323
+ else:
324
+ valid_actions = ["create", "update"]
270
325
  return {
271
326
  "status": "error",
272
- "error": f"Bulk update failed: {str(e)}",
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', ...)",
273
330
  }