mcp-ticketer 0.4.1__py3-none-any.whl → 0.4.3__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/__init__.py +3 -12
- mcp_ticketer/__version__.py +1 -1
- mcp_ticketer/adapters/aitrackdown.py +243 -11
- mcp_ticketer/adapters/github.py +15 -14
- mcp_ticketer/adapters/hybrid.py +11 -11
- mcp_ticketer/adapters/jira.py +22 -25
- mcp_ticketer/adapters/linear/adapter.py +9 -21
- mcp_ticketer/adapters/linear/client.py +2 -1
- mcp_ticketer/adapters/linear/mappers.py +2 -1
- mcp_ticketer/cache/memory.py +6 -5
- mcp_ticketer/cli/adapter_diagnostics.py +4 -2
- mcp_ticketer/cli/auggie_configure.py +66 -0
- mcp_ticketer/cli/codex_configure.py +70 -2
- mcp_ticketer/cli/configure.py +7 -14
- mcp_ticketer/cli/diagnostics.py +2 -2
- mcp_ticketer/cli/discover.py +6 -11
- mcp_ticketer/cli/gemini_configure.py +68 -2
- mcp_ticketer/cli/linear_commands.py +6 -7
- mcp_ticketer/cli/main.py +341 -203
- mcp_ticketer/cli/mcp_configure.py +61 -2
- mcp_ticketer/cli/ticket_commands.py +27 -30
- mcp_ticketer/cli/utils.py +23 -22
- mcp_ticketer/core/__init__.py +3 -1
- mcp_ticketer/core/adapter.py +82 -13
- mcp_ticketer/core/config.py +27 -29
- mcp_ticketer/core/env_discovery.py +10 -10
- mcp_ticketer/core/env_loader.py +8 -8
- mcp_ticketer/core/http_client.py +16 -16
- mcp_ticketer/core/mappers.py +10 -10
- mcp_ticketer/core/models.py +50 -20
- mcp_ticketer/core/project_config.py +40 -34
- mcp_ticketer/core/registry.py +2 -2
- mcp_ticketer/mcp/dto.py +32 -32
- mcp_ticketer/mcp/response_builder.py +2 -2
- mcp_ticketer/mcp/server.py +17 -37
- mcp_ticketer/mcp/server_sdk.py +93 -0
- mcp_ticketer/mcp/tools/__init__.py +36 -0
- mcp_ticketer/mcp/tools/attachment_tools.py +179 -0
- mcp_ticketer/mcp/tools/bulk_tools.py +273 -0
- mcp_ticketer/mcp/tools/comment_tools.py +90 -0
- mcp_ticketer/mcp/tools/hierarchy_tools.py +383 -0
- mcp_ticketer/mcp/tools/pr_tools.py +154 -0
- mcp_ticketer/mcp/tools/search_tools.py +206 -0
- mcp_ticketer/mcp/tools/ticket_tools.py +277 -0
- mcp_ticketer/queue/health_monitor.py +4 -4
- mcp_ticketer/queue/manager.py +2 -2
- mcp_ticketer/queue/queue.py +16 -16
- mcp_ticketer/queue/ticket_registry.py +7 -7
- mcp_ticketer/queue/worker.py +2 -2
- {mcp_ticketer-0.4.1.dist-info → mcp_ticketer-0.4.3.dist-info}/METADATA +90 -17
- mcp_ticketer-0.4.3.dist-info/RECORD +73 -0
- mcp_ticketer-0.4.1.dist-info/RECORD +0 -64
- {mcp_ticketer-0.4.1.dist-info → mcp_ticketer-0.4.3.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.4.1.dist-info → mcp_ticketer-0.4.3.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.4.1.dist-info → mcp_ticketer-0.4.3.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.4.1.dist-info → mcp_ticketer-0.4.3.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""Search and query tools for finding tickets.
|
|
2
|
+
|
|
3
|
+
This module implements advanced search capabilities for tickets using
|
|
4
|
+
various filters and criteria.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from ...core.models import Priority, SearchQuery, TicketState
|
|
10
|
+
from ..server_sdk import get_adapter, mcp
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@mcp.tool()
|
|
14
|
+
async def ticket_search(
|
|
15
|
+
query: str | None = None,
|
|
16
|
+
state: str | None = None,
|
|
17
|
+
priority: str | None = None,
|
|
18
|
+
tags: list[str] | None = None,
|
|
19
|
+
assignee: str | None = None,
|
|
20
|
+
limit: int = 10,
|
|
21
|
+
) -> dict[str, Any]:
|
|
22
|
+
"""Search tickets using advanced filters.
|
|
23
|
+
|
|
24
|
+
Searches for tickets matching the specified criteria. All filters are
|
|
25
|
+
optional and can be combined.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
query: Text search query to match against title and description
|
|
29
|
+
state: Filter by state - must be one of: open, in_progress, ready, tested, done, closed, waiting, blocked
|
|
30
|
+
priority: Filter by priority - must be one of: low, medium, high, critical
|
|
31
|
+
tags: Filter by tags - tickets must have all specified tags
|
|
32
|
+
assignee: Filter by assigned user ID or email
|
|
33
|
+
limit: Maximum number of results to return (default: 10, max: 100)
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
List of tickets matching search criteria, or error information
|
|
37
|
+
|
|
38
|
+
"""
|
|
39
|
+
try:
|
|
40
|
+
adapter = get_adapter()
|
|
41
|
+
|
|
42
|
+
# Validate and build search query
|
|
43
|
+
state_enum = None
|
|
44
|
+
if state is not None:
|
|
45
|
+
try:
|
|
46
|
+
state_enum = TicketState(state.lower())
|
|
47
|
+
except ValueError:
|
|
48
|
+
return {
|
|
49
|
+
"status": "error",
|
|
50
|
+
"error": f"Invalid state '{state}'. Must be one of: open, in_progress, ready, tested, done, closed, waiting, blocked",
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
priority_enum = None
|
|
54
|
+
if priority is not None:
|
|
55
|
+
try:
|
|
56
|
+
priority_enum = Priority(priority.lower())
|
|
57
|
+
except ValueError:
|
|
58
|
+
return {
|
|
59
|
+
"status": "error",
|
|
60
|
+
"error": f"Invalid priority '{priority}'. Must be one of: low, medium, high, critical",
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# Create search query
|
|
64
|
+
search_query = SearchQuery(
|
|
65
|
+
query=query,
|
|
66
|
+
state=state_enum,
|
|
67
|
+
priority=priority_enum,
|
|
68
|
+
tags=tags,
|
|
69
|
+
assignee=assignee,
|
|
70
|
+
limit=min(limit, 100), # Enforce max limit
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# Execute search via adapter
|
|
74
|
+
results = await adapter.search(search_query)
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
"status": "completed",
|
|
78
|
+
"tickets": [ticket.model_dump() for ticket in results],
|
|
79
|
+
"count": len(results),
|
|
80
|
+
"query": {
|
|
81
|
+
"text": query,
|
|
82
|
+
"state": state,
|
|
83
|
+
"priority": priority,
|
|
84
|
+
"tags": tags,
|
|
85
|
+
"assignee": assignee,
|
|
86
|
+
},
|
|
87
|
+
}
|
|
88
|
+
except Exception as e:
|
|
89
|
+
return {
|
|
90
|
+
"status": "error",
|
|
91
|
+
"error": f"Failed to search tickets: {str(e)}",
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@mcp.tool()
|
|
96
|
+
async def ticket_search_hierarchy(
|
|
97
|
+
query: str,
|
|
98
|
+
include_children: bool = True,
|
|
99
|
+
max_depth: int = 3,
|
|
100
|
+
) -> dict[str, Any]:
|
|
101
|
+
"""Search tickets and include their hierarchy.
|
|
102
|
+
|
|
103
|
+
Performs a text search and returns matching tickets along with their
|
|
104
|
+
hierarchical context (parent epics/issues and child issues/tasks).
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
query: Text search query to match against title and description
|
|
108
|
+
include_children: Whether to include child tickets in results
|
|
109
|
+
max_depth: Maximum hierarchy depth to include (1-3, default: 3)
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
List of tickets with hierarchy information, or error information
|
|
113
|
+
|
|
114
|
+
"""
|
|
115
|
+
try:
|
|
116
|
+
adapter = get_adapter()
|
|
117
|
+
|
|
118
|
+
# Validate max_depth
|
|
119
|
+
if max_depth < 1 or max_depth > 3:
|
|
120
|
+
return {
|
|
121
|
+
"status": "error",
|
|
122
|
+
"error": "max_depth must be between 1 and 3",
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
# Create search query
|
|
126
|
+
search_query = SearchQuery(
|
|
127
|
+
query=query,
|
|
128
|
+
limit=50, # Reasonable limit for hierarchical search
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# Execute search via adapter
|
|
132
|
+
results = await adapter.search(search_query)
|
|
133
|
+
|
|
134
|
+
# Build hierarchical results
|
|
135
|
+
hierarchical_results = []
|
|
136
|
+
for ticket in results:
|
|
137
|
+
ticket_data = {
|
|
138
|
+
"ticket": ticket.model_dump(),
|
|
139
|
+
"hierarchy": {},
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
# Get parent epic if applicable
|
|
143
|
+
parent_epic_id = getattr(ticket, "parent_epic", None)
|
|
144
|
+
if parent_epic_id and max_depth >= 2:
|
|
145
|
+
try:
|
|
146
|
+
parent_epic = await adapter.read(parent_epic_id)
|
|
147
|
+
if parent_epic:
|
|
148
|
+
ticket_data["hierarchy"][
|
|
149
|
+
"parent_epic"
|
|
150
|
+
] = parent_epic.model_dump()
|
|
151
|
+
except Exception:
|
|
152
|
+
pass # Parent not found, continue
|
|
153
|
+
|
|
154
|
+
# Get parent issue if applicable (for tasks)
|
|
155
|
+
parent_issue_id = getattr(ticket, "parent_issue", None)
|
|
156
|
+
if parent_issue_id and max_depth >= 2:
|
|
157
|
+
try:
|
|
158
|
+
parent_issue = await adapter.read(parent_issue_id)
|
|
159
|
+
if parent_issue:
|
|
160
|
+
ticket_data["hierarchy"][
|
|
161
|
+
"parent_issue"
|
|
162
|
+
] = parent_issue.model_dump()
|
|
163
|
+
except Exception:
|
|
164
|
+
pass # Parent not found, continue
|
|
165
|
+
|
|
166
|
+
# Get children if requested
|
|
167
|
+
if include_children and max_depth >= 2:
|
|
168
|
+
children = []
|
|
169
|
+
|
|
170
|
+
# Get child issues (for epics)
|
|
171
|
+
child_issue_ids = getattr(ticket, "child_issues", [])
|
|
172
|
+
for child_id in child_issue_ids:
|
|
173
|
+
try:
|
|
174
|
+
child = await adapter.read(child_id)
|
|
175
|
+
if child:
|
|
176
|
+
children.append(child.model_dump())
|
|
177
|
+
except Exception:
|
|
178
|
+
pass # Child not found, continue
|
|
179
|
+
|
|
180
|
+
# Get child tasks (for issues)
|
|
181
|
+
child_task_ids = getattr(ticket, "children", [])
|
|
182
|
+
for child_id in child_task_ids:
|
|
183
|
+
try:
|
|
184
|
+
child = await adapter.read(child_id)
|
|
185
|
+
if child:
|
|
186
|
+
children.append(child.model_dump())
|
|
187
|
+
except Exception:
|
|
188
|
+
pass # Child not found, continue
|
|
189
|
+
|
|
190
|
+
if children:
|
|
191
|
+
ticket_data["hierarchy"]["children"] = children
|
|
192
|
+
|
|
193
|
+
hierarchical_results.append(ticket_data)
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
"status": "completed",
|
|
197
|
+
"results": hierarchical_results,
|
|
198
|
+
"count": len(hierarchical_results),
|
|
199
|
+
"query": query,
|
|
200
|
+
"max_depth": max_depth,
|
|
201
|
+
}
|
|
202
|
+
except Exception as e:
|
|
203
|
+
return {
|
|
204
|
+
"status": "error",
|
|
205
|
+
"error": f"Failed to search with hierarchy: {str(e)}",
|
|
206
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
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 typing import Any
|
|
8
|
+
|
|
9
|
+
from ...core.models import Priority, Task, TicketState
|
|
10
|
+
from ..server_sdk import get_adapter, mcp
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@mcp.tool()
|
|
14
|
+
async def ticket_create(
|
|
15
|
+
title: str,
|
|
16
|
+
description: str = "",
|
|
17
|
+
priority: str = "medium",
|
|
18
|
+
tags: list[str] | None = None,
|
|
19
|
+
assignee: str | None = None,
|
|
20
|
+
) -> dict[str, Any]:
|
|
21
|
+
"""Create a new ticket with specified details.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
title: Ticket title (required)
|
|
25
|
+
description: Detailed description of the ticket
|
|
26
|
+
priority: Priority level - must be one of: low, medium, high, critical
|
|
27
|
+
tags: List of tags to categorize the ticket
|
|
28
|
+
assignee: User ID or email to assign the ticket to
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Created ticket details including ID and metadata, or error information
|
|
32
|
+
|
|
33
|
+
"""
|
|
34
|
+
try:
|
|
35
|
+
adapter = get_adapter()
|
|
36
|
+
|
|
37
|
+
# Validate and convert priority
|
|
38
|
+
try:
|
|
39
|
+
priority_enum = Priority(priority.lower())
|
|
40
|
+
except ValueError:
|
|
41
|
+
return {
|
|
42
|
+
"status": "error",
|
|
43
|
+
"error": f"Invalid priority '{priority}'. Must be one of: low, medium, high, critical",
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# Create task object
|
|
47
|
+
task = Task(
|
|
48
|
+
title=title,
|
|
49
|
+
description=description or "",
|
|
50
|
+
priority=priority_enum,
|
|
51
|
+
tags=tags or [],
|
|
52
|
+
assignee=assignee,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Create via adapter
|
|
56
|
+
created = await adapter.create(task)
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
"status": "completed",
|
|
60
|
+
"ticket": created.model_dump(),
|
|
61
|
+
}
|
|
62
|
+
except Exception as e:
|
|
63
|
+
return {
|
|
64
|
+
"status": "error",
|
|
65
|
+
"error": f"Failed to create ticket: {str(e)}",
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@mcp.tool()
|
|
70
|
+
async def ticket_read(ticket_id: str) -> dict[str, Any]:
|
|
71
|
+
"""Read a ticket by its ID.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
ticket_id: Unique identifier of the ticket to retrieve
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Ticket details if found, or error information
|
|
78
|
+
|
|
79
|
+
"""
|
|
80
|
+
try:
|
|
81
|
+
adapter = get_adapter()
|
|
82
|
+
ticket = await adapter.read(ticket_id)
|
|
83
|
+
|
|
84
|
+
if ticket is None:
|
|
85
|
+
return {
|
|
86
|
+
"status": "error",
|
|
87
|
+
"error": f"Ticket {ticket_id} not found",
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
"status": "completed",
|
|
92
|
+
"ticket": ticket.model_dump(),
|
|
93
|
+
}
|
|
94
|
+
except Exception as e:
|
|
95
|
+
return {
|
|
96
|
+
"status": "error",
|
|
97
|
+
"error": f"Failed to read ticket: {str(e)}",
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@mcp.tool()
|
|
102
|
+
async def ticket_update(
|
|
103
|
+
ticket_id: str,
|
|
104
|
+
title: str | None = None,
|
|
105
|
+
description: str | None = None,
|
|
106
|
+
priority: str | None = None,
|
|
107
|
+
state: str | None = None,
|
|
108
|
+
assignee: str | None = None,
|
|
109
|
+
tags: list[str] | None = None,
|
|
110
|
+
) -> dict[str, Any]:
|
|
111
|
+
"""Update an existing ticket.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
ticket_id: Unique identifier of the ticket to update
|
|
115
|
+
title: New title for the ticket
|
|
116
|
+
description: New description for the ticket
|
|
117
|
+
priority: New priority - must be one of: low, medium, high, critical
|
|
118
|
+
state: New state - must be one of: open, in_progress, ready, tested, done, closed, waiting, blocked
|
|
119
|
+
assignee: User ID or email to assign the ticket to
|
|
120
|
+
tags: New list of tags (replaces existing tags)
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Updated ticket details, or error information
|
|
124
|
+
|
|
125
|
+
"""
|
|
126
|
+
try:
|
|
127
|
+
adapter = get_adapter()
|
|
128
|
+
|
|
129
|
+
# Build updates dictionary with only provided fields
|
|
130
|
+
updates: dict[str, Any] = {}
|
|
131
|
+
|
|
132
|
+
if title is not None:
|
|
133
|
+
updates["title"] = title
|
|
134
|
+
if description is not None:
|
|
135
|
+
updates["description"] = description
|
|
136
|
+
if assignee is not None:
|
|
137
|
+
updates["assignee"] = assignee
|
|
138
|
+
if tags is not None:
|
|
139
|
+
updates["tags"] = tags
|
|
140
|
+
|
|
141
|
+
# Validate and convert priority if provided
|
|
142
|
+
if priority is not None:
|
|
143
|
+
try:
|
|
144
|
+
updates["priority"] = Priority(priority.lower())
|
|
145
|
+
except ValueError:
|
|
146
|
+
return {
|
|
147
|
+
"status": "error",
|
|
148
|
+
"error": f"Invalid priority '{priority}'. Must be one of: low, medium, high, critical",
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
# Validate and convert state if provided
|
|
152
|
+
if state is not None:
|
|
153
|
+
try:
|
|
154
|
+
updates["state"] = TicketState(state.lower())
|
|
155
|
+
except ValueError:
|
|
156
|
+
return {
|
|
157
|
+
"status": "error",
|
|
158
|
+
"error": f"Invalid state '{state}'. Must be one of: open, in_progress, ready, tested, done, closed, waiting, blocked",
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
# Update via adapter
|
|
162
|
+
updated = await adapter.update(ticket_id, updates)
|
|
163
|
+
|
|
164
|
+
if updated is None:
|
|
165
|
+
return {
|
|
166
|
+
"status": "error",
|
|
167
|
+
"error": f"Ticket {ticket_id} not found or update failed",
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
"status": "completed",
|
|
172
|
+
"ticket": updated.model_dump(),
|
|
173
|
+
}
|
|
174
|
+
except Exception as e:
|
|
175
|
+
return {
|
|
176
|
+
"status": "error",
|
|
177
|
+
"error": f"Failed to update ticket: {str(e)}",
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@mcp.tool()
|
|
182
|
+
async def ticket_delete(ticket_id: str) -> dict[str, Any]:
|
|
183
|
+
"""Delete a ticket by its ID.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
ticket_id: Unique identifier of the ticket to delete
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
Success confirmation or error information
|
|
190
|
+
|
|
191
|
+
"""
|
|
192
|
+
try:
|
|
193
|
+
adapter = get_adapter()
|
|
194
|
+
success = await adapter.delete(ticket_id)
|
|
195
|
+
|
|
196
|
+
if not success:
|
|
197
|
+
return {
|
|
198
|
+
"status": "error",
|
|
199
|
+
"error": f"Ticket {ticket_id} not found or delete failed",
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
"status": "completed",
|
|
204
|
+
"message": f"Ticket {ticket_id} deleted successfully",
|
|
205
|
+
}
|
|
206
|
+
except Exception as e:
|
|
207
|
+
return {
|
|
208
|
+
"status": "error",
|
|
209
|
+
"error": f"Failed to delete ticket: {str(e)}",
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@mcp.tool()
|
|
214
|
+
async def ticket_list(
|
|
215
|
+
limit: int = 10,
|
|
216
|
+
offset: int = 0,
|
|
217
|
+
state: str | None = None,
|
|
218
|
+
priority: str | None = None,
|
|
219
|
+
assignee: str | None = None,
|
|
220
|
+
) -> dict[str, Any]:
|
|
221
|
+
"""List tickets with pagination and optional filters.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
limit: Maximum number of tickets to return (default: 10)
|
|
225
|
+
offset: Number of tickets to skip for pagination (default: 0)
|
|
226
|
+
state: Filter by state - must be one of: open, in_progress, ready, tested, done, closed, waiting, blocked
|
|
227
|
+
priority: Filter by priority - must be one of: low, medium, high, critical
|
|
228
|
+
assignee: Filter by assigned user ID or email
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
List of tickets matching criteria, or error information
|
|
232
|
+
|
|
233
|
+
"""
|
|
234
|
+
try:
|
|
235
|
+
adapter = get_adapter()
|
|
236
|
+
|
|
237
|
+
# Build filters dictionary
|
|
238
|
+
filters: dict[str, Any] = {}
|
|
239
|
+
|
|
240
|
+
if state is not None:
|
|
241
|
+
try:
|
|
242
|
+
filters["state"] = TicketState(state.lower())
|
|
243
|
+
except ValueError:
|
|
244
|
+
return {
|
|
245
|
+
"status": "error",
|
|
246
|
+
"error": f"Invalid state '{state}'. Must be one of: open, in_progress, ready, tested, done, closed, waiting, blocked",
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if priority is not None:
|
|
250
|
+
try:
|
|
251
|
+
filters["priority"] = Priority(priority.lower())
|
|
252
|
+
except ValueError:
|
|
253
|
+
return {
|
|
254
|
+
"status": "error",
|
|
255
|
+
"error": f"Invalid priority '{priority}'. Must be one of: low, medium, high, critical",
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if assignee is not None:
|
|
259
|
+
filters["assignee"] = assignee
|
|
260
|
+
|
|
261
|
+
# List tickets via adapter
|
|
262
|
+
tickets = await adapter.list(
|
|
263
|
+
limit=limit, offset=offset, filters=filters if filters else None
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
"status": "completed",
|
|
268
|
+
"tickets": [ticket.model_dump() for ticket in tickets],
|
|
269
|
+
"count": len(tickets),
|
|
270
|
+
"limit": limit,
|
|
271
|
+
"offset": offset,
|
|
272
|
+
}
|
|
273
|
+
except Exception as e:
|
|
274
|
+
return {
|
|
275
|
+
"status": "error",
|
|
276
|
+
"error": f"Failed to list tickets: {str(e)}",
|
|
277
|
+
}
|
|
@@ -4,7 +4,7 @@ import logging
|
|
|
4
4
|
import time
|
|
5
5
|
from datetime import datetime, timedelta
|
|
6
6
|
from enum import Enum
|
|
7
|
-
from typing import Any
|
|
7
|
+
from typing import Any
|
|
8
8
|
|
|
9
9
|
import psutil
|
|
10
10
|
|
|
@@ -30,8 +30,8 @@ class HealthAlert:
|
|
|
30
30
|
self,
|
|
31
31
|
level: HealthStatus,
|
|
32
32
|
message: str,
|
|
33
|
-
details:
|
|
34
|
-
timestamp:
|
|
33
|
+
details: dict[str, Any] | None = None,
|
|
34
|
+
timestamp: datetime | None = None,
|
|
35
35
|
):
|
|
36
36
|
self.level = level
|
|
37
37
|
self.message = message
|
|
@@ -52,7 +52,7 @@ class QueueHealthMonitor:
|
|
|
52
52
|
QUEUE_BACKLOG_WARNING = 10 # Warn if more than 10 pending items
|
|
53
53
|
QUEUE_BACKLOG_CRITICAL = 50 # Critical if more than 50 pending items
|
|
54
54
|
|
|
55
|
-
def __init__(self, queue:
|
|
55
|
+
def __init__(self, queue: Queue | None = None):
|
|
56
56
|
"""Initialize health monitor.
|
|
57
57
|
|
|
58
58
|
Args:
|
mcp_ticketer/queue/manager.py
CHANGED
|
@@ -7,7 +7,7 @@ import subprocess
|
|
|
7
7
|
import sys
|
|
8
8
|
import time
|
|
9
9
|
from pathlib import Path
|
|
10
|
-
from typing import Any
|
|
10
|
+
from typing import Any
|
|
11
11
|
|
|
12
12
|
import psutil
|
|
13
13
|
|
|
@@ -304,7 +304,7 @@ class WorkerManager:
|
|
|
304
304
|
|
|
305
305
|
return status
|
|
306
306
|
|
|
307
|
-
def _get_pid(self) ->
|
|
307
|
+
def _get_pid(self) -> int | None:
|
|
308
308
|
"""Get worker PID from file.
|
|
309
309
|
|
|
310
310
|
Returns:
|
mcp_ticketer/queue/queue.py
CHANGED
|
@@ -8,7 +8,7 @@ from dataclasses import asdict, dataclass
|
|
|
8
8
|
from datetime import datetime, timedelta
|
|
9
9
|
from enum import Enum
|
|
10
10
|
from pathlib import Path
|
|
11
|
-
from typing import Any
|
|
11
|
+
from typing import Any
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class QueueStatus(str, Enum):
|
|
@@ -30,12 +30,12 @@ class QueueItem:
|
|
|
30
30
|
operation: str
|
|
31
31
|
status: QueueStatus
|
|
32
32
|
created_at: datetime
|
|
33
|
-
processed_at:
|
|
34
|
-
error_message:
|
|
33
|
+
processed_at: datetime | None = None
|
|
34
|
+
error_message: str | None = None
|
|
35
35
|
retry_count: int = 0
|
|
36
|
-
result:
|
|
37
|
-
project_dir:
|
|
38
|
-
adapter_config:
|
|
36
|
+
result: dict[str, Any] | None = None
|
|
37
|
+
project_dir: str | None = None
|
|
38
|
+
adapter_config: dict[str, Any] | None = None # Adapter configuration
|
|
39
39
|
|
|
40
40
|
def to_dict(self) -> dict:
|
|
41
41
|
"""Convert to dictionary for storage."""
|
|
@@ -67,7 +67,7 @@ class QueueItem:
|
|
|
67
67
|
class Queue:
|
|
68
68
|
"""Thread-safe SQLite queue for ticket operations."""
|
|
69
69
|
|
|
70
|
-
def __init__(self, db_path:
|
|
70
|
+
def __init__(self, db_path: Path | None = None):
|
|
71
71
|
"""Initialize queue with database connection.
|
|
72
72
|
|
|
73
73
|
Args:
|
|
@@ -139,8 +139,8 @@ class Queue:
|
|
|
139
139
|
ticket_data: dict[str, Any],
|
|
140
140
|
adapter: str,
|
|
141
141
|
operation: str,
|
|
142
|
-
project_dir:
|
|
143
|
-
adapter_config:
|
|
142
|
+
project_dir: str | None = None,
|
|
143
|
+
adapter_config: dict[str, Any] | None = None,
|
|
144
144
|
) -> str:
|
|
145
145
|
"""Add item to queue.
|
|
146
146
|
|
|
@@ -186,7 +186,7 @@ class Queue:
|
|
|
186
186
|
|
|
187
187
|
return queue_id
|
|
188
188
|
|
|
189
|
-
def get_next_pending(self) ->
|
|
189
|
+
def get_next_pending(self) -> QueueItem | None:
|
|
190
190
|
"""Get next pending item from queue atomically.
|
|
191
191
|
|
|
192
192
|
Returns:
|
|
@@ -251,9 +251,9 @@ class Queue:
|
|
|
251
251
|
self,
|
|
252
252
|
queue_id: str,
|
|
253
253
|
status: QueueStatus,
|
|
254
|
-
error_message:
|
|
255
|
-
result:
|
|
256
|
-
expected_status:
|
|
254
|
+
error_message: str | None = None,
|
|
255
|
+
result: dict[str, Any] | None = None,
|
|
256
|
+
expected_status: QueueStatus | None = None,
|
|
257
257
|
) -> bool:
|
|
258
258
|
"""Update queue item status atomically.
|
|
259
259
|
|
|
@@ -328,7 +328,7 @@ class Queue:
|
|
|
328
328
|
raise
|
|
329
329
|
|
|
330
330
|
def increment_retry(
|
|
331
|
-
self, queue_id: str, expected_status:
|
|
331
|
+
self, queue_id: str, expected_status: QueueStatus | None = None
|
|
332
332
|
) -> int:
|
|
333
333
|
"""Increment retry count and reset to pending atomically.
|
|
334
334
|
|
|
@@ -387,7 +387,7 @@ class Queue:
|
|
|
387
387
|
conn.rollback()
|
|
388
388
|
raise
|
|
389
389
|
|
|
390
|
-
def get_item(self, queue_id: str) ->
|
|
390
|
+
def get_item(self, queue_id: str) -> QueueItem | None:
|
|
391
391
|
"""Get specific queue item by ID.
|
|
392
392
|
|
|
393
393
|
Args:
|
|
@@ -409,7 +409,7 @@ class Queue:
|
|
|
409
409
|
return QueueItem.from_row(row) if row else None
|
|
410
410
|
|
|
411
411
|
def list_items(
|
|
412
|
-
self, status:
|
|
412
|
+
self, status: QueueStatus | None = None, limit: int = 50
|
|
413
413
|
) -> list[QueueItem]:
|
|
414
414
|
"""List queue items.
|
|
415
415
|
|
|
@@ -5,13 +5,13 @@ import sqlite3
|
|
|
5
5
|
import threading
|
|
6
6
|
from datetime import datetime, timedelta
|
|
7
7
|
from pathlib import Path
|
|
8
|
-
from typing import Any
|
|
8
|
+
from typing import Any
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class TicketRegistry:
|
|
12
12
|
"""Persistent registry for tracking ticket IDs and their lifecycle."""
|
|
13
13
|
|
|
14
|
-
def __init__(self, db_path:
|
|
14
|
+
def __init__(self, db_path: Path | None = None):
|
|
15
15
|
"""Initialize ticket registry.
|
|
16
16
|
|
|
17
17
|
Args:
|
|
@@ -130,10 +130,10 @@ class TicketRegistry:
|
|
|
130
130
|
self,
|
|
131
131
|
queue_id: str,
|
|
132
132
|
status: str,
|
|
133
|
-
ticket_id:
|
|
134
|
-
result_data:
|
|
135
|
-
error_message:
|
|
136
|
-
retry_count:
|
|
133
|
+
ticket_id: str | None = None,
|
|
134
|
+
result_data: dict[str, Any] | None = None,
|
|
135
|
+
error_message: str | None = None,
|
|
136
|
+
retry_count: int | None = None,
|
|
137
137
|
) -> None:
|
|
138
138
|
"""Update ticket operation status.
|
|
139
139
|
|
|
@@ -179,7 +179,7 @@ class TicketRegistry:
|
|
|
179
179
|
)
|
|
180
180
|
conn.commit()
|
|
181
181
|
|
|
182
|
-
def get_ticket_info(self, queue_id: str) ->
|
|
182
|
+
def get_ticket_info(self, queue_id: str) -> dict[str, Any] | None:
|
|
183
183
|
"""Get ticket information by queue ID.
|
|
184
184
|
|
|
185
185
|
Args:
|
mcp_ticketer/queue/worker.py
CHANGED
|
@@ -7,7 +7,7 @@ import threading
|
|
|
7
7
|
import time
|
|
8
8
|
from datetime import datetime
|
|
9
9
|
from pathlib import Path
|
|
10
|
-
from typing import Any
|
|
10
|
+
from typing import Any
|
|
11
11
|
|
|
12
12
|
from dotenv import load_dotenv
|
|
13
13
|
|
|
@@ -58,7 +58,7 @@ class Worker:
|
|
|
58
58
|
|
|
59
59
|
def __init__(
|
|
60
60
|
self,
|
|
61
|
-
queue:
|
|
61
|
+
queue: Queue | None = None,
|
|
62
62
|
batch_size: int = DEFAULT_BATCH_SIZE,
|
|
63
63
|
max_concurrent: int = DEFAULT_MAX_CONCURRENT,
|
|
64
64
|
):
|