mcp-ticketer 0.3.5__py3-none-any.whl → 0.12.0__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.
- mcp_ticketer/__version__.py +3 -3
- mcp_ticketer/adapters/__init__.py +2 -0
- mcp_ticketer/adapters/aitrackdown.py +263 -14
- mcp_ticketer/adapters/asana/__init__.py +15 -0
- mcp_ticketer/adapters/asana/adapter.py +1308 -0
- mcp_ticketer/adapters/asana/client.py +292 -0
- mcp_ticketer/adapters/asana/mappers.py +334 -0
- mcp_ticketer/adapters/asana/types.py +146 -0
- mcp_ticketer/adapters/github.py +326 -109
- mcp_ticketer/adapters/hybrid.py +11 -11
- mcp_ticketer/adapters/jira.py +271 -25
- mcp_ticketer/adapters/linear/adapter.py +693 -39
- mcp_ticketer/adapters/linear/client.py +61 -9
- mcp_ticketer/adapters/linear/mappers.py +9 -3
- mcp_ticketer/adapters/linear/queries.py +9 -7
- mcp_ticketer/cache/memory.py +9 -8
- mcp_ticketer/cli/adapter_diagnostics.py +1 -1
- mcp_ticketer/cli/auggie_configure.py +104 -15
- mcp_ticketer/cli/codex_configure.py +188 -32
- mcp_ticketer/cli/configure.py +37 -48
- mcp_ticketer/cli/diagnostics.py +20 -18
- mcp_ticketer/cli/discover.py +292 -26
- mcp_ticketer/cli/gemini_configure.py +107 -26
- mcp_ticketer/cli/instruction_commands.py +429 -0
- mcp_ticketer/cli/linear_commands.py +105 -22
- mcp_ticketer/cli/main.py +1830 -435
- mcp_ticketer/cli/mcp_configure.py +296 -89
- mcp_ticketer/cli/migrate_config.py +12 -8
- mcp_ticketer/cli/platform_commands.py +123 -0
- mcp_ticketer/cli/platform_detection.py +412 -0
- mcp_ticketer/cli/python_detection.py +126 -0
- mcp_ticketer/cli/queue_commands.py +15 -15
- mcp_ticketer/cli/simple_health.py +1 -1
- mcp_ticketer/cli/ticket_commands.py +773 -0
- mcp_ticketer/cli/update_checker.py +313 -0
- mcp_ticketer/cli/utils.py +67 -62
- mcp_ticketer/core/__init__.py +14 -1
- mcp_ticketer/core/adapter.py +84 -15
- mcp_ticketer/core/config.py +44 -39
- mcp_ticketer/core/env_discovery.py +42 -12
- mcp_ticketer/core/env_loader.py +15 -14
- mcp_ticketer/core/exceptions.py +3 -3
- mcp_ticketer/core/http_client.py +26 -26
- mcp_ticketer/core/instructions.py +405 -0
- mcp_ticketer/core/mappers.py +11 -11
- mcp_ticketer/core/models.py +50 -20
- mcp_ticketer/core/onepassword_secrets.py +379 -0
- mcp_ticketer/core/project_config.py +57 -35
- mcp_ticketer/core/registry.py +3 -3
- mcp_ticketer/defaults/ticket_instructions.md +644 -0
- mcp_ticketer/mcp/__init__.py +29 -1
- mcp_ticketer/mcp/__main__.py +60 -0
- mcp_ticketer/mcp/server/__init__.py +25 -0
- mcp_ticketer/mcp/server/__main__.py +60 -0
- mcp_ticketer/mcp/{dto.py → server/dto.py} +32 -32
- mcp_ticketer/mcp/{server.py → server/main.py} +127 -74
- mcp_ticketer/mcp/{response_builder.py → server/response_builder.py} +2 -2
- mcp_ticketer/mcp/server/server_sdk.py +93 -0
- mcp_ticketer/mcp/server/tools/__init__.py +47 -0
- mcp_ticketer/mcp/server/tools/attachment_tools.py +226 -0
- mcp_ticketer/mcp/server/tools/bulk_tools.py +273 -0
- mcp_ticketer/mcp/server/tools/comment_tools.py +90 -0
- mcp_ticketer/mcp/server/tools/config_tools.py +381 -0
- mcp_ticketer/mcp/server/tools/hierarchy_tools.py +532 -0
- mcp_ticketer/mcp/server/tools/instruction_tools.py +293 -0
- mcp_ticketer/mcp/server/tools/pr_tools.py +154 -0
- mcp_ticketer/mcp/server/tools/search_tools.py +206 -0
- mcp_ticketer/mcp/server/tools/ticket_tools.py +430 -0
- mcp_ticketer/mcp/server/tools/user_ticket_tools.py +382 -0
- mcp_ticketer/queue/__init__.py +1 -0
- mcp_ticketer/queue/health_monitor.py +5 -4
- mcp_ticketer/queue/manager.py +15 -51
- mcp_ticketer/queue/queue.py +19 -19
- mcp_ticketer/queue/run_worker.py +1 -1
- mcp_ticketer/queue/ticket_registry.py +14 -14
- mcp_ticketer/queue/worker.py +16 -14
- {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/METADATA +168 -32
- mcp_ticketer-0.12.0.dist-info/RECORD +91 -0
- mcp_ticketer-0.3.5.dist-info/RECORD +0 -62
- /mcp_ticketer/mcp/{constants.py → server/constants.py} +0 -0
- {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
"""Basic CRUD operations for tickets.
|
|
2
|
+
|
|
3
|
+
This module implements the core create, read, update, delete, and list
|
|
4
|
+
operations for tickets using the FastMCP SDK.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from ....core.models import Priority, Task, TicketState
|
|
11
|
+
from ....core.project_config import ConfigResolver, TicketerConfig
|
|
12
|
+
from ..server_sdk import get_adapter, mcp
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
async def detect_and_apply_labels(
|
|
16
|
+
adapter: Any,
|
|
17
|
+
ticket_title: str,
|
|
18
|
+
ticket_description: str,
|
|
19
|
+
existing_labels: list[str] | None = None,
|
|
20
|
+
) -> list[str]:
|
|
21
|
+
"""Detect and suggest labels/tags based on ticket content.
|
|
22
|
+
|
|
23
|
+
This function analyzes the ticket title and description to automatically
|
|
24
|
+
detect relevant labels/tags from the adapter's available labels.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
adapter: The ticket adapter instance
|
|
28
|
+
ticket_title: Ticket title text
|
|
29
|
+
ticket_description: Ticket description text
|
|
30
|
+
existing_labels: Labels already specified by user (optional)
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
List of label/tag identifiers to apply (combines auto-detected + user-specified)
|
|
34
|
+
|
|
35
|
+
"""
|
|
36
|
+
# Get available labels from adapter
|
|
37
|
+
available_labels = []
|
|
38
|
+
try:
|
|
39
|
+
if hasattr(adapter, "list_labels"):
|
|
40
|
+
available_labels = await adapter.list_labels()
|
|
41
|
+
elif hasattr(adapter, "get_labels"):
|
|
42
|
+
available_labels = await adapter.get_labels()
|
|
43
|
+
except Exception:
|
|
44
|
+
# Adapter doesn't support labels or listing failed - return user labels only
|
|
45
|
+
return existing_labels or []
|
|
46
|
+
|
|
47
|
+
if not available_labels:
|
|
48
|
+
return existing_labels or []
|
|
49
|
+
|
|
50
|
+
# Combine title and description for matching (lowercase for case-insensitive matching)
|
|
51
|
+
content = f"{ticket_title} {ticket_description or ''}".lower()
|
|
52
|
+
|
|
53
|
+
# Common label keyword patterns
|
|
54
|
+
label_keywords = {
|
|
55
|
+
"bug": ["bug", "error", "broken", "crash", "fix", "issue", "defect"],
|
|
56
|
+
"feature": ["feature", "add", "new", "implement", "create", "enhancement"],
|
|
57
|
+
"improvement": [
|
|
58
|
+
"enhance",
|
|
59
|
+
"improve",
|
|
60
|
+
"update",
|
|
61
|
+
"upgrade",
|
|
62
|
+
"refactor",
|
|
63
|
+
"optimize",
|
|
64
|
+
],
|
|
65
|
+
"documentation": ["doc", "documentation", "readme", "guide", "manual"],
|
|
66
|
+
"test": ["test", "testing", "qa", "validation", "verify"],
|
|
67
|
+
"security": ["security", "vulnerability", "auth", "permission", "exploit"],
|
|
68
|
+
"performance": ["performance", "slow", "optimize", "speed", "latency"],
|
|
69
|
+
"ui": ["ui", "ux", "interface", "design", "layout", "frontend"],
|
|
70
|
+
"api": ["api", "endpoint", "rest", "graphql", "backend"],
|
|
71
|
+
"backend": ["backend", "server", "database", "storage"],
|
|
72
|
+
"frontend": ["frontend", "client", "web", "react", "vue"],
|
|
73
|
+
"critical": ["critical", "urgent", "emergency", "blocker"],
|
|
74
|
+
"high-priority": ["urgent", "asap", "important", "critical"],
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# Match labels against content
|
|
78
|
+
matched_labels = []
|
|
79
|
+
|
|
80
|
+
for label in available_labels:
|
|
81
|
+
# Extract label name (handle both dict and string formats)
|
|
82
|
+
if isinstance(label, dict):
|
|
83
|
+
label_name = label.get("name", "")
|
|
84
|
+
label_id = label.get("id", label_name)
|
|
85
|
+
else:
|
|
86
|
+
label_name = str(label)
|
|
87
|
+
label_id = label_name
|
|
88
|
+
|
|
89
|
+
label_name_lower = label_name.lower()
|
|
90
|
+
|
|
91
|
+
# Direct match: label name appears in content
|
|
92
|
+
if label_name_lower in content:
|
|
93
|
+
if label_id not in matched_labels:
|
|
94
|
+
matched_labels.append(label_id)
|
|
95
|
+
continue
|
|
96
|
+
|
|
97
|
+
# Keyword match: check if label matches any keyword category
|
|
98
|
+
for keyword_category, keywords in label_keywords.items():
|
|
99
|
+
# Check if label name relates to the category
|
|
100
|
+
if (
|
|
101
|
+
keyword_category in label_name_lower
|
|
102
|
+
or label_name_lower in keyword_category
|
|
103
|
+
):
|
|
104
|
+
# Check if any keyword from this category appears in content
|
|
105
|
+
if any(kw in content for kw in keywords):
|
|
106
|
+
if label_id not in matched_labels:
|
|
107
|
+
matched_labels.append(label_id)
|
|
108
|
+
break
|
|
109
|
+
|
|
110
|
+
# Combine user-specified labels with auto-detected ones
|
|
111
|
+
final_labels = list(existing_labels or [])
|
|
112
|
+
for label in matched_labels:
|
|
113
|
+
if label not in final_labels:
|
|
114
|
+
final_labels.append(label)
|
|
115
|
+
|
|
116
|
+
return final_labels
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@mcp.tool()
|
|
120
|
+
async def ticket_create(
|
|
121
|
+
title: str,
|
|
122
|
+
description: str = "",
|
|
123
|
+
priority: str = "medium",
|
|
124
|
+
tags: list[str] | None = None,
|
|
125
|
+
assignee: str | None = None,
|
|
126
|
+
parent_epic: str | None = None,
|
|
127
|
+
auto_detect_labels: bool = True,
|
|
128
|
+
) -> dict[str, Any]:
|
|
129
|
+
"""Create a new ticket with automatic label/tag detection.
|
|
130
|
+
|
|
131
|
+
This tool automatically scans available labels/tags and intelligently
|
|
132
|
+
applies relevant ones based on the ticket title and description.
|
|
133
|
+
|
|
134
|
+
Label Detection:
|
|
135
|
+
- Scans all available labels in the configured adapter
|
|
136
|
+
- Matches labels based on keywords in title/description
|
|
137
|
+
- Combines auto-detected labels with user-specified ones
|
|
138
|
+
- Can be disabled by setting auto_detect_labels=false
|
|
139
|
+
|
|
140
|
+
Common label patterns detected:
|
|
141
|
+
- bug, feature, improvement, documentation
|
|
142
|
+
- test, security, performance
|
|
143
|
+
- ui, api, backend, frontend
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
title: Ticket title (required)
|
|
147
|
+
description: Detailed description of the ticket
|
|
148
|
+
priority: Priority level - must be one of: low, medium, high, critical
|
|
149
|
+
tags: List of tags to categorize the ticket (auto-detection adds to these)
|
|
150
|
+
assignee: User ID or email to assign the ticket to
|
|
151
|
+
parent_epic: Parent epic/project ID to assign this ticket to (optional)
|
|
152
|
+
auto_detect_labels: Automatically detect and apply relevant labels (default: True)
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Created ticket details including ID and metadata, or error information
|
|
156
|
+
|
|
157
|
+
"""
|
|
158
|
+
try:
|
|
159
|
+
adapter = get_adapter()
|
|
160
|
+
|
|
161
|
+
# Validate and convert priority
|
|
162
|
+
try:
|
|
163
|
+
priority_enum = Priority(priority.lower())
|
|
164
|
+
except ValueError:
|
|
165
|
+
return {
|
|
166
|
+
"status": "error",
|
|
167
|
+
"error": f"Invalid priority '{priority}'. Must be one of: low, medium, high, critical",
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
# Use default_user if no assignee specified
|
|
171
|
+
final_assignee = assignee
|
|
172
|
+
if final_assignee is None:
|
|
173
|
+
resolver = ConfigResolver(project_path=Path.cwd())
|
|
174
|
+
config = resolver.load_project_config() or TicketerConfig()
|
|
175
|
+
if config.default_user:
|
|
176
|
+
final_assignee = config.default_user
|
|
177
|
+
|
|
178
|
+
# Use default_project if no parent_epic specified
|
|
179
|
+
final_parent_epic = parent_epic
|
|
180
|
+
if final_parent_epic is None:
|
|
181
|
+
resolver = ConfigResolver(project_path=Path.cwd())
|
|
182
|
+
config = resolver.load_project_config() or TicketerConfig()
|
|
183
|
+
# Try default_project first, fall back to default_epic
|
|
184
|
+
if config.default_project:
|
|
185
|
+
final_parent_epic = config.default_project
|
|
186
|
+
elif config.default_epic:
|
|
187
|
+
final_parent_epic = config.default_epic
|
|
188
|
+
|
|
189
|
+
# Auto-detect labels if enabled
|
|
190
|
+
final_tags = tags
|
|
191
|
+
if auto_detect_labels:
|
|
192
|
+
final_tags = await detect_and_apply_labels(
|
|
193
|
+
adapter, title, description or "", tags
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# Create task object
|
|
197
|
+
task = Task(
|
|
198
|
+
title=title,
|
|
199
|
+
description=description or "",
|
|
200
|
+
priority=priority_enum,
|
|
201
|
+
tags=final_tags or [],
|
|
202
|
+
assignee=final_assignee,
|
|
203
|
+
parent_epic=final_parent_epic,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
# Create via adapter
|
|
207
|
+
created = await adapter.create(task)
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
"status": "completed",
|
|
211
|
+
"ticket": created.model_dump(),
|
|
212
|
+
"labels_applied": created.tags or [],
|
|
213
|
+
"auto_detected": auto_detect_labels,
|
|
214
|
+
}
|
|
215
|
+
except Exception as e:
|
|
216
|
+
return {
|
|
217
|
+
"status": "error",
|
|
218
|
+
"error": f"Failed to create ticket: {str(e)}",
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@mcp.tool()
|
|
223
|
+
async def ticket_read(ticket_id: str) -> dict[str, Any]:
|
|
224
|
+
"""Read a ticket by its ID.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
ticket_id: Unique identifier of the ticket to retrieve
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
Ticket details if found, or error information
|
|
231
|
+
|
|
232
|
+
"""
|
|
233
|
+
try:
|
|
234
|
+
adapter = get_adapter()
|
|
235
|
+
ticket = await adapter.read(ticket_id)
|
|
236
|
+
|
|
237
|
+
if ticket is None:
|
|
238
|
+
return {
|
|
239
|
+
"status": "error",
|
|
240
|
+
"error": f"Ticket {ticket_id} not found",
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
"status": "completed",
|
|
245
|
+
"ticket": ticket.model_dump(),
|
|
246
|
+
}
|
|
247
|
+
except Exception as e:
|
|
248
|
+
return {
|
|
249
|
+
"status": "error",
|
|
250
|
+
"error": f"Failed to read ticket: {str(e)}",
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
@mcp.tool()
|
|
255
|
+
async def ticket_update(
|
|
256
|
+
ticket_id: str,
|
|
257
|
+
title: str | None = None,
|
|
258
|
+
description: str | None = None,
|
|
259
|
+
priority: str | None = None,
|
|
260
|
+
state: str | None = None,
|
|
261
|
+
assignee: str | None = None,
|
|
262
|
+
tags: list[str] | None = None,
|
|
263
|
+
) -> dict[str, Any]:
|
|
264
|
+
"""Update an existing ticket.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
ticket_id: Unique identifier of the ticket to update
|
|
268
|
+
title: New title for the ticket
|
|
269
|
+
description: New description for the ticket
|
|
270
|
+
priority: New priority - must be one of: low, medium, high, critical
|
|
271
|
+
state: New state - must be one of: open, in_progress, ready, tested, done, closed, waiting, blocked
|
|
272
|
+
assignee: User ID or email to assign the ticket to
|
|
273
|
+
tags: New list of tags (replaces existing tags)
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
Updated ticket details, or error information
|
|
277
|
+
|
|
278
|
+
"""
|
|
279
|
+
try:
|
|
280
|
+
adapter = get_adapter()
|
|
281
|
+
|
|
282
|
+
# Build updates dictionary with only provided fields
|
|
283
|
+
updates: dict[str, Any] = {}
|
|
284
|
+
|
|
285
|
+
if title is not None:
|
|
286
|
+
updates["title"] = title
|
|
287
|
+
if description is not None:
|
|
288
|
+
updates["description"] = description
|
|
289
|
+
if assignee is not None:
|
|
290
|
+
updates["assignee"] = assignee
|
|
291
|
+
if tags is not None:
|
|
292
|
+
updates["tags"] = tags
|
|
293
|
+
|
|
294
|
+
# Validate and convert priority if provided
|
|
295
|
+
if priority is not None:
|
|
296
|
+
try:
|
|
297
|
+
updates["priority"] = Priority(priority.lower())
|
|
298
|
+
except ValueError:
|
|
299
|
+
return {
|
|
300
|
+
"status": "error",
|
|
301
|
+
"error": f"Invalid priority '{priority}'. Must be one of: low, medium, high, critical",
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
# Validate and convert state if provided
|
|
305
|
+
if state is not None:
|
|
306
|
+
try:
|
|
307
|
+
updates["state"] = TicketState(state.lower())
|
|
308
|
+
except ValueError:
|
|
309
|
+
return {
|
|
310
|
+
"status": "error",
|
|
311
|
+
"error": f"Invalid state '{state}'. Must be one of: open, in_progress, ready, tested, done, closed, waiting, blocked",
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
# Update via adapter
|
|
315
|
+
updated = await adapter.update(ticket_id, updates)
|
|
316
|
+
|
|
317
|
+
if updated is None:
|
|
318
|
+
return {
|
|
319
|
+
"status": "error",
|
|
320
|
+
"error": f"Ticket {ticket_id} not found or update failed",
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
"status": "completed",
|
|
325
|
+
"ticket": updated.model_dump(),
|
|
326
|
+
}
|
|
327
|
+
except Exception as e:
|
|
328
|
+
return {
|
|
329
|
+
"status": "error",
|
|
330
|
+
"error": f"Failed to update ticket: {str(e)}",
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
@mcp.tool()
|
|
335
|
+
async def ticket_delete(ticket_id: str) -> dict[str, Any]:
|
|
336
|
+
"""Delete a ticket by its ID.
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
ticket_id: Unique identifier of the ticket to delete
|
|
340
|
+
|
|
341
|
+
Returns:
|
|
342
|
+
Success confirmation or error information
|
|
343
|
+
|
|
344
|
+
"""
|
|
345
|
+
try:
|
|
346
|
+
adapter = get_adapter()
|
|
347
|
+
success = await adapter.delete(ticket_id)
|
|
348
|
+
|
|
349
|
+
if not success:
|
|
350
|
+
return {
|
|
351
|
+
"status": "error",
|
|
352
|
+
"error": f"Ticket {ticket_id} not found or delete failed",
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return {
|
|
356
|
+
"status": "completed",
|
|
357
|
+
"message": f"Ticket {ticket_id} deleted successfully",
|
|
358
|
+
}
|
|
359
|
+
except Exception as e:
|
|
360
|
+
return {
|
|
361
|
+
"status": "error",
|
|
362
|
+
"error": f"Failed to delete ticket: {str(e)}",
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
@mcp.tool()
|
|
367
|
+
async def ticket_list(
|
|
368
|
+
limit: int = 10,
|
|
369
|
+
offset: int = 0,
|
|
370
|
+
state: str | None = None,
|
|
371
|
+
priority: str | None = None,
|
|
372
|
+
assignee: str | None = None,
|
|
373
|
+
) -> dict[str, Any]:
|
|
374
|
+
"""List tickets with pagination and optional filters.
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
limit: Maximum number of tickets to return (default: 10)
|
|
378
|
+
offset: Number of tickets to skip for pagination (default: 0)
|
|
379
|
+
state: Filter by state - must be one of: open, in_progress, ready, tested, done, closed, waiting, blocked
|
|
380
|
+
priority: Filter by priority - must be one of: low, medium, high, critical
|
|
381
|
+
assignee: Filter by assigned user ID or email
|
|
382
|
+
|
|
383
|
+
Returns:
|
|
384
|
+
List of tickets matching criteria, or error information
|
|
385
|
+
|
|
386
|
+
"""
|
|
387
|
+
try:
|
|
388
|
+
adapter = get_adapter()
|
|
389
|
+
|
|
390
|
+
# Build filters dictionary
|
|
391
|
+
filters: dict[str, Any] = {}
|
|
392
|
+
|
|
393
|
+
if state is not None:
|
|
394
|
+
try:
|
|
395
|
+
filters["state"] = TicketState(state.lower())
|
|
396
|
+
except ValueError:
|
|
397
|
+
return {
|
|
398
|
+
"status": "error",
|
|
399
|
+
"error": f"Invalid state '{state}'. Must be one of: open, in_progress, ready, tested, done, closed, waiting, blocked",
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if priority is not None:
|
|
403
|
+
try:
|
|
404
|
+
filters["priority"] = Priority(priority.lower())
|
|
405
|
+
except ValueError:
|
|
406
|
+
return {
|
|
407
|
+
"status": "error",
|
|
408
|
+
"error": f"Invalid priority '{priority}'. Must be one of: low, medium, high, critical",
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if assignee is not None:
|
|
412
|
+
filters["assignee"] = assignee
|
|
413
|
+
|
|
414
|
+
# List tickets via adapter
|
|
415
|
+
tickets = await adapter.list(
|
|
416
|
+
limit=limit, offset=offset, filters=filters if filters else None
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
return {
|
|
420
|
+
"status": "completed",
|
|
421
|
+
"tickets": [ticket.model_dump() for ticket in tickets],
|
|
422
|
+
"count": len(tickets),
|
|
423
|
+
"limit": limit,
|
|
424
|
+
"offset": offset,
|
|
425
|
+
}
|
|
426
|
+
except Exception as e:
|
|
427
|
+
return {
|
|
428
|
+
"status": "error",
|
|
429
|
+
"error": f"Failed to list tickets: {str(e)}",
|
|
430
|
+
}
|