mcp-ticketer 0.12.0__py3-none-any.whl → 2.0.1__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 (87) hide show
  1. mcp_ticketer/__init__.py +10 -10
  2. mcp_ticketer/__version__.py +1 -1
  3. mcp_ticketer/adapters/aitrackdown.py +385 -6
  4. mcp_ticketer/adapters/asana/adapter.py +108 -0
  5. mcp_ticketer/adapters/asana/mappers.py +14 -0
  6. mcp_ticketer/adapters/github.py +525 -11
  7. mcp_ticketer/adapters/hybrid.py +47 -5
  8. mcp_ticketer/adapters/jira.py +521 -0
  9. mcp_ticketer/adapters/linear/adapter.py +1784 -101
  10. mcp_ticketer/adapters/linear/client.py +85 -3
  11. mcp_ticketer/adapters/linear/mappers.py +96 -8
  12. mcp_ticketer/adapters/linear/queries.py +168 -1
  13. mcp_ticketer/adapters/linear/types.py +80 -4
  14. mcp_ticketer/analysis/__init__.py +56 -0
  15. mcp_ticketer/analysis/dependency_graph.py +255 -0
  16. mcp_ticketer/analysis/health_assessment.py +304 -0
  17. mcp_ticketer/analysis/orphaned.py +218 -0
  18. mcp_ticketer/analysis/project_status.py +594 -0
  19. mcp_ticketer/analysis/similarity.py +224 -0
  20. mcp_ticketer/analysis/staleness.py +266 -0
  21. mcp_ticketer/automation/__init__.py +11 -0
  22. mcp_ticketer/automation/project_updates.py +378 -0
  23. mcp_ticketer/cli/adapter_diagnostics.py +3 -1
  24. mcp_ticketer/cli/auggie_configure.py +17 -5
  25. mcp_ticketer/cli/codex_configure.py +97 -61
  26. mcp_ticketer/cli/configure.py +851 -103
  27. mcp_ticketer/cli/cursor_configure.py +314 -0
  28. mcp_ticketer/cli/diagnostics.py +13 -12
  29. mcp_ticketer/cli/discover.py +5 -0
  30. mcp_ticketer/cli/gemini_configure.py +17 -5
  31. mcp_ticketer/cli/init_command.py +880 -0
  32. mcp_ticketer/cli/instruction_commands.py +6 -0
  33. mcp_ticketer/cli/main.py +233 -3151
  34. mcp_ticketer/cli/mcp_configure.py +672 -98
  35. mcp_ticketer/cli/mcp_server_commands.py +415 -0
  36. mcp_ticketer/cli/platform_detection.py +77 -12
  37. mcp_ticketer/cli/platform_installer.py +536 -0
  38. mcp_ticketer/cli/project_update_commands.py +350 -0
  39. mcp_ticketer/cli/setup_command.py +639 -0
  40. mcp_ticketer/cli/simple_health.py +12 -10
  41. mcp_ticketer/cli/ticket_commands.py +264 -24
  42. mcp_ticketer/core/__init__.py +28 -6
  43. mcp_ticketer/core/adapter.py +166 -1
  44. mcp_ticketer/core/config.py +21 -21
  45. mcp_ticketer/core/exceptions.py +7 -1
  46. mcp_ticketer/core/label_manager.py +732 -0
  47. mcp_ticketer/core/mappers.py +31 -19
  48. mcp_ticketer/core/models.py +135 -0
  49. mcp_ticketer/core/onepassword_secrets.py +1 -1
  50. mcp_ticketer/core/priority_matcher.py +463 -0
  51. mcp_ticketer/core/project_config.py +132 -14
  52. mcp_ticketer/core/session_state.py +171 -0
  53. mcp_ticketer/core/state_matcher.py +592 -0
  54. mcp_ticketer/core/url_parser.py +425 -0
  55. mcp_ticketer/core/validators.py +69 -0
  56. mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
  57. mcp_ticketer/mcp/server/main.py +106 -25
  58. mcp_ticketer/mcp/server/routing.py +655 -0
  59. mcp_ticketer/mcp/server/server_sdk.py +58 -0
  60. mcp_ticketer/mcp/server/tools/__init__.py +31 -12
  61. mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
  62. mcp_ticketer/mcp/server/tools/attachment_tools.py +6 -8
  63. mcp_ticketer/mcp/server/tools/bulk_tools.py +259 -202
  64. mcp_ticketer/mcp/server/tools/comment_tools.py +74 -12
  65. mcp_ticketer/mcp/server/tools/config_tools.py +1184 -136
  66. mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
  67. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +870 -460
  68. mcp_ticketer/mcp/server/tools/instruction_tools.py +7 -5
  69. mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
  70. mcp_ticketer/mcp/server/tools/pr_tools.py +3 -7
  71. mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
  72. mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
  73. mcp_ticketer/mcp/server/tools/search_tools.py +180 -97
  74. mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
  75. mcp_ticketer/mcp/server/tools/ticket_tools.py +1070 -123
  76. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +218 -236
  77. mcp_ticketer/queue/worker.py +1 -1
  78. mcp_ticketer/utils/__init__.py +5 -0
  79. mcp_ticketer/utils/token_utils.py +246 -0
  80. mcp_ticketer-2.0.1.dist-info/METADATA +1366 -0
  81. mcp_ticketer-2.0.1.dist-info/RECORD +122 -0
  82. mcp_ticketer-0.12.0.dist-info/METADATA +0 -550
  83. mcp_ticketer-0.12.0.dist-info/RECORD +0 -91
  84. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/WHEEL +0 -0
  85. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/entry_points.txt +0 -0
  86. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/licenses/LICENSE +0 -0
  87. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/top_level.txt +0 -0
@@ -10,10 +10,9 @@ from pathlib import Path
10
10
  from typing import Any
11
11
 
12
12
  from ....core.models import Comment, TicketType
13
- from ..server_sdk import get_adapter, mcp
13
+ from ..server_sdk import get_adapter
14
14
 
15
15
 
16
- @mcp.tool()
17
16
  async def ticket_attach(
18
17
  ticket_id: str,
19
18
  file_path: str,
@@ -60,7 +59,7 @@ async def ticket_attach(
60
59
  mime_type = mimetypes.guess_type(file_path)[0]
61
60
 
62
61
  # Upload file to Linear's storage
63
- file_url = await adapter.upload_file(file_path, mime_type) # type: ignore
62
+ file_url = await adapter.upload_file(file_path, mime_type)
64
63
 
65
64
  # Determine ticket type and attach accordingly
66
65
  ticket_type = getattr(ticket, "ticket_type", None)
@@ -70,7 +69,7 @@ async def ticket_attach(
70
69
  adapter, "attach_file_to_epic"
71
70
  ):
72
71
  # Attach to epic (project)
73
- result = await adapter.attach_file_to_epic( # type: ignore
72
+ result = await adapter.attach_file_to_epic(
74
73
  epic_id=ticket_id,
75
74
  file_url=file_url,
76
75
  title=description or filename,
@@ -78,7 +77,7 @@ async def ticket_attach(
78
77
  )
79
78
  else:
80
79
  # Attach to issue/task
81
- result = await adapter.attach_file_to_issue( # type: ignore
80
+ result = await adapter.attach_file_to_issue(
82
81
  issue_id=ticket_id,
83
82
  file_url=file_url,
84
83
  title=description or filename,
@@ -99,7 +98,7 @@ async def ticket_attach(
99
98
 
100
99
  # Try legacy add_attachment method
101
100
  if hasattr(adapter, "add_attachment"):
102
- attachment = await adapter.add_attachment( # type: ignore
101
+ attachment = await adapter.add_attachment(
103
102
  ticket_id=ticket_id, file_path=file_path, description=description
104
103
  )
105
104
 
@@ -145,7 +144,6 @@ async def ticket_attach(
145
144
  }
146
145
 
147
146
 
148
- @mcp.tool()
149
147
  async def ticket_attachments(
150
148
  ticket_id: str,
151
149
  ) -> dict[str, Any]: # Keep as dict for MCP compatibility
@@ -182,7 +180,7 @@ async def ticket_attachments(
182
180
  }
183
181
 
184
182
  # Get attachments via adapter
185
- attachments = await adapter.get_attachments(ticket_id) # type: ignore
183
+ attachments = await adapter.get_attachments(ticket_id)
186
184
 
187
185
  return {
188
186
  "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
  }