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,293 @@
|
|
|
1
|
+
"""Ticket instructions management tools.
|
|
2
|
+
|
|
3
|
+
This module implements MCP tools for managing ticket writing instructions,
|
|
4
|
+
allowing AI agents to query and customize the guidelines that help create
|
|
5
|
+
well-structured, consistent tickets.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from ....core.instructions import (
|
|
12
|
+
InstructionsError,
|
|
13
|
+
InstructionsValidationError,
|
|
14
|
+
TicketInstructionsManager,
|
|
15
|
+
)
|
|
16
|
+
from ..server_sdk import mcp
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@mcp.tool()
|
|
20
|
+
async def instructions_get() -> dict[str, Any]:
|
|
21
|
+
"""Get current ticket writing instructions.
|
|
22
|
+
|
|
23
|
+
Retrieves the active instructions for the current project, which may be
|
|
24
|
+
custom project-specific instructions or the default embedded instructions.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
A dictionary containing:
|
|
28
|
+
- status: "completed" or "error"
|
|
29
|
+
- instructions: The full instruction text (if successful)
|
|
30
|
+
- source: "custom" or "default" indicating which instructions are active
|
|
31
|
+
- path: Path to custom instructions file (if exists)
|
|
32
|
+
- error: Error message (if failed)
|
|
33
|
+
|
|
34
|
+
Example response:
|
|
35
|
+
{
|
|
36
|
+
"status": "completed",
|
|
37
|
+
"instructions": "# Ticket Writing Guidelines...",
|
|
38
|
+
"source": "custom",
|
|
39
|
+
"path": "/path/to/project/.mcp-ticketer/instructions.md"
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
"""
|
|
43
|
+
try:
|
|
44
|
+
# Use current working directory as project directory
|
|
45
|
+
manager = TicketInstructionsManager(project_dir=Path.cwd())
|
|
46
|
+
|
|
47
|
+
# Get instructions
|
|
48
|
+
instructions = manager.get_instructions()
|
|
49
|
+
|
|
50
|
+
# Determine source
|
|
51
|
+
source = "custom" if manager.has_custom_instructions() else "default"
|
|
52
|
+
|
|
53
|
+
# Build response
|
|
54
|
+
response: dict[str, Any] = {
|
|
55
|
+
"status": "completed",
|
|
56
|
+
"instructions": instructions,
|
|
57
|
+
"source": source,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
# Add path if custom instructions exist
|
|
61
|
+
if source == "custom":
|
|
62
|
+
response["path"] = str(manager.get_instructions_path())
|
|
63
|
+
|
|
64
|
+
return response
|
|
65
|
+
|
|
66
|
+
except InstructionsError as e:
|
|
67
|
+
return {
|
|
68
|
+
"status": "error",
|
|
69
|
+
"error": f"Failed to get instructions: {str(e)}",
|
|
70
|
+
}
|
|
71
|
+
except Exception as e:
|
|
72
|
+
return {
|
|
73
|
+
"status": "error",
|
|
74
|
+
"error": f"Unexpected error: {str(e)}",
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@mcp.tool()
|
|
79
|
+
async def instructions_set(content: str, source: str = "inline") -> dict[str, Any]:
|
|
80
|
+
r"""Set custom ticket writing instructions for the project.
|
|
81
|
+
|
|
82
|
+
Creates or overwrites custom instructions with the provided content.
|
|
83
|
+
The content is validated before saving.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
content: The custom instructions content (markdown text)
|
|
87
|
+
source: Source type - "inline" for direct content or "file" for file path
|
|
88
|
+
(currently only "inline" is supported by MCP tools)
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
A dictionary containing:
|
|
92
|
+
- status: "completed" or "error"
|
|
93
|
+
- message: Success or error message
|
|
94
|
+
- path: Path where instructions were saved (if successful)
|
|
95
|
+
- error: Detailed error message (if failed)
|
|
96
|
+
|
|
97
|
+
Example:
|
|
98
|
+
To set custom instructions:
|
|
99
|
+
instructions_set(
|
|
100
|
+
content="# Our Team's Ticket Guidelines\\n\\n...",
|
|
101
|
+
source="inline"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
"""
|
|
105
|
+
try:
|
|
106
|
+
# Validate source parameter
|
|
107
|
+
if source not in ["inline", "file"]:
|
|
108
|
+
return {
|
|
109
|
+
"status": "error",
|
|
110
|
+
"error": f"Invalid source '{source}'. Must be 'inline' or 'file'",
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
# Use current working directory as project directory
|
|
114
|
+
manager = TicketInstructionsManager(project_dir=Path.cwd())
|
|
115
|
+
|
|
116
|
+
# Set instructions
|
|
117
|
+
manager.set_instructions(content)
|
|
118
|
+
|
|
119
|
+
# Get path where instructions were saved
|
|
120
|
+
inst_path = manager.get_instructions_path()
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
"status": "completed",
|
|
124
|
+
"message": "Custom instructions saved successfully",
|
|
125
|
+
"path": str(inst_path),
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
except InstructionsValidationError as e:
|
|
129
|
+
return {
|
|
130
|
+
"status": "error",
|
|
131
|
+
"error": f"Validation failed: {str(e)}",
|
|
132
|
+
"message": "Instructions content did not pass validation checks",
|
|
133
|
+
}
|
|
134
|
+
except InstructionsError as e:
|
|
135
|
+
return {
|
|
136
|
+
"status": "error",
|
|
137
|
+
"error": f"Failed to set instructions: {str(e)}",
|
|
138
|
+
}
|
|
139
|
+
except Exception as e:
|
|
140
|
+
return {
|
|
141
|
+
"status": "error",
|
|
142
|
+
"error": f"Unexpected error: {str(e)}",
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@mcp.tool()
|
|
147
|
+
async def instructions_reset() -> dict[str, Any]:
|
|
148
|
+
"""Reset to default instructions by deleting custom instructions.
|
|
149
|
+
|
|
150
|
+
Removes any custom project-specific instructions, causing the system
|
|
151
|
+
to revert to using the default embedded instructions.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
A dictionary containing:
|
|
155
|
+
- status: "completed" or "error"
|
|
156
|
+
- message: Description of what happened
|
|
157
|
+
- error: Error message (if failed)
|
|
158
|
+
|
|
159
|
+
Example response (when custom instructions existed):
|
|
160
|
+
{
|
|
161
|
+
"status": "completed",
|
|
162
|
+
"message": "Custom instructions deleted. Now using defaults."
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
Example response (when no custom instructions):
|
|
166
|
+
{
|
|
167
|
+
"status": "completed",
|
|
168
|
+
"message": "No custom instructions to delete. Already using defaults."
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
"""
|
|
172
|
+
try:
|
|
173
|
+
# Use current working directory as project directory
|
|
174
|
+
manager = TicketInstructionsManager(project_dir=Path.cwd())
|
|
175
|
+
|
|
176
|
+
# Check if custom instructions exist
|
|
177
|
+
if not manager.has_custom_instructions():
|
|
178
|
+
return {
|
|
179
|
+
"status": "completed",
|
|
180
|
+
"message": "No custom instructions to delete. Already using defaults.",
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
# Delete custom instructions
|
|
184
|
+
deleted = manager.delete_instructions()
|
|
185
|
+
|
|
186
|
+
if deleted:
|
|
187
|
+
return {
|
|
188
|
+
"status": "completed",
|
|
189
|
+
"message": "Custom instructions deleted. Now using defaults.",
|
|
190
|
+
}
|
|
191
|
+
else:
|
|
192
|
+
return {
|
|
193
|
+
"status": "completed",
|
|
194
|
+
"message": "No custom instructions found to delete.",
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
except InstructionsError as e:
|
|
198
|
+
return {
|
|
199
|
+
"status": "error",
|
|
200
|
+
"error": f"Failed to reset instructions: {str(e)}",
|
|
201
|
+
}
|
|
202
|
+
except Exception as e:
|
|
203
|
+
return {
|
|
204
|
+
"status": "error",
|
|
205
|
+
"error": f"Unexpected error: {str(e)}",
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@mcp.tool()
|
|
210
|
+
async def instructions_validate(content: str) -> dict[str, Any]:
|
|
211
|
+
"""Validate ticket instructions content without saving.
|
|
212
|
+
|
|
213
|
+
Checks if the provided content meets validation requirements:
|
|
214
|
+
- Not empty
|
|
215
|
+
- Minimum length (100 characters)
|
|
216
|
+
- Contains markdown headers (warning only)
|
|
217
|
+
|
|
218
|
+
This allows AI agents to validate content before attempting to save it.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
content: The instructions content to validate (markdown text)
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
A dictionary containing:
|
|
225
|
+
- status: "valid" or "invalid"
|
|
226
|
+
- warnings: List of non-critical issues (e.g., missing headers)
|
|
227
|
+
- errors: List of critical validation failures
|
|
228
|
+
- message: Summary message
|
|
229
|
+
|
|
230
|
+
Example response (valid):
|
|
231
|
+
{
|
|
232
|
+
"status": "valid",
|
|
233
|
+
"warnings": ["No markdown headers found"],
|
|
234
|
+
"errors": [],
|
|
235
|
+
"message": "Content is valid but has 1 warning"
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
Example response (invalid):
|
|
239
|
+
{
|
|
240
|
+
"status": "invalid",
|
|
241
|
+
"warnings": [],
|
|
242
|
+
"errors": ["Content too short (50 characters). Minimum 100 required."],
|
|
243
|
+
"message": "Content validation failed"
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
"""
|
|
247
|
+
warnings: list[str] = []
|
|
248
|
+
errors: list[str] = []
|
|
249
|
+
|
|
250
|
+
try:
|
|
251
|
+
# Check for empty content
|
|
252
|
+
if not content or not content.strip():
|
|
253
|
+
errors.append("Instructions content cannot be empty")
|
|
254
|
+
else:
|
|
255
|
+
# Check minimum length
|
|
256
|
+
if len(content.strip()) < 100:
|
|
257
|
+
errors.append(
|
|
258
|
+
f"Content too short ({len(content)} characters). "
|
|
259
|
+
"Minimum 100 characters required for meaningful guidelines."
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
# Check for markdown headers (warning only)
|
|
263
|
+
if not any(line.strip().startswith("#") for line in content.split("\n")):
|
|
264
|
+
warnings.append(
|
|
265
|
+
"No markdown headers found. "
|
|
266
|
+
"Consider using headers for better structure."
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
# Determine status
|
|
270
|
+
if errors:
|
|
271
|
+
status = "invalid"
|
|
272
|
+
message = "Content validation failed"
|
|
273
|
+
elif warnings:
|
|
274
|
+
status = "valid"
|
|
275
|
+
message = f"Content is valid but has {len(warnings)} warning(s)"
|
|
276
|
+
else:
|
|
277
|
+
status = "valid"
|
|
278
|
+
message = "Content is valid with no issues"
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
"status": status,
|
|
282
|
+
"warnings": warnings,
|
|
283
|
+
"errors": errors,
|
|
284
|
+
"message": message,
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
except Exception as e:
|
|
288
|
+
return {
|
|
289
|
+
"status": "error",
|
|
290
|
+
"warnings": [],
|
|
291
|
+
"errors": [f"Validation error: {str(e)}"],
|
|
292
|
+
"message": "Validation process failed",
|
|
293
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Pull request integration tools for tickets.
|
|
2
|
+
|
|
3
|
+
This module implements tools for linking tickets with pull requests and
|
|
4
|
+
creating PRs from tickets. Note that PR functionality may not be available
|
|
5
|
+
in all adapters.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from ..server_sdk import get_adapter, mcp
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@mcp.tool()
|
|
14
|
+
async def ticket_create_pr(
|
|
15
|
+
ticket_id: str,
|
|
16
|
+
title: str,
|
|
17
|
+
description: str = "",
|
|
18
|
+
source_branch: str | None = None,
|
|
19
|
+
target_branch: str = "main",
|
|
20
|
+
) -> dict[str, Any]:
|
|
21
|
+
"""Create a pull request linked to a ticket.
|
|
22
|
+
|
|
23
|
+
Creates a new pull request and automatically links it to the specified
|
|
24
|
+
ticket. This functionality may not be available in all adapters.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
ticket_id: Unique identifier of the ticket to link the PR to
|
|
28
|
+
title: Pull request title
|
|
29
|
+
description: Pull request description
|
|
30
|
+
source_branch: Source branch for the PR (if not specified, may use ticket ID)
|
|
31
|
+
target_branch: Target branch for the PR (default: main)
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Created PR details and link information, or error information
|
|
35
|
+
|
|
36
|
+
"""
|
|
37
|
+
try:
|
|
38
|
+
adapter = get_adapter()
|
|
39
|
+
|
|
40
|
+
# Check if adapter supports PR operations
|
|
41
|
+
if not hasattr(adapter, "create_pull_request"):
|
|
42
|
+
return {
|
|
43
|
+
"status": "error",
|
|
44
|
+
"error": f"Pull request creation not supported by {type(adapter).__name__} adapter",
|
|
45
|
+
"ticket_id": ticket_id,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# Read ticket to validate it exists
|
|
49
|
+
ticket = await adapter.read(ticket_id)
|
|
50
|
+
if ticket is None:
|
|
51
|
+
return {
|
|
52
|
+
"status": "error",
|
|
53
|
+
"error": f"Ticket {ticket_id} not found",
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
# Use ticket ID as source branch if not specified
|
|
57
|
+
if source_branch is None:
|
|
58
|
+
source_branch = f"feature/{ticket_id}"
|
|
59
|
+
|
|
60
|
+
# Create PR via adapter
|
|
61
|
+
pr_data = await adapter.create_pull_request( # type: ignore
|
|
62
|
+
ticket_id=ticket_id,
|
|
63
|
+
title=title,
|
|
64
|
+
description=description,
|
|
65
|
+
source_branch=source_branch,
|
|
66
|
+
target_branch=target_branch,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
"status": "completed",
|
|
71
|
+
"ticket_id": ticket_id,
|
|
72
|
+
"pull_request": pr_data,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
except AttributeError:
|
|
76
|
+
return {
|
|
77
|
+
"status": "error",
|
|
78
|
+
"error": "Pull request creation not supported by this adapter",
|
|
79
|
+
"ticket_id": ticket_id,
|
|
80
|
+
}
|
|
81
|
+
except Exception as e:
|
|
82
|
+
return {
|
|
83
|
+
"status": "error",
|
|
84
|
+
"error": f"Failed to create pull request: {str(e)}",
|
|
85
|
+
"ticket_id": ticket_id,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@mcp.tool()
|
|
90
|
+
async def ticket_link_pr(
|
|
91
|
+
ticket_id: str,
|
|
92
|
+
pr_url: str,
|
|
93
|
+
) -> dict[str, Any]:
|
|
94
|
+
"""Link an existing pull request to a ticket.
|
|
95
|
+
|
|
96
|
+
Associates an existing pull request (identified by URL) with a ticket.
|
|
97
|
+
This is typically done by adding the PR URL to the ticket's metadata
|
|
98
|
+
or as a comment.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
ticket_id: Unique identifier of the ticket
|
|
102
|
+
pr_url: URL of the pull request to link
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Link confirmation and updated ticket details, or error information
|
|
106
|
+
|
|
107
|
+
"""
|
|
108
|
+
try:
|
|
109
|
+
adapter = get_adapter()
|
|
110
|
+
|
|
111
|
+
# Read ticket to validate it exists
|
|
112
|
+
ticket = await adapter.read(ticket_id)
|
|
113
|
+
if ticket is None:
|
|
114
|
+
return {
|
|
115
|
+
"status": "error",
|
|
116
|
+
"error": f"Ticket {ticket_id} not found",
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
# Check if adapter has specialized PR linking
|
|
120
|
+
if hasattr(adapter, "link_pull_request"):
|
|
121
|
+
result = await adapter.link_pull_request( # type: ignore
|
|
122
|
+
ticket_id=ticket_id, pr_url=pr_url
|
|
123
|
+
)
|
|
124
|
+
return {
|
|
125
|
+
"status": "completed",
|
|
126
|
+
"ticket_id": ticket_id,
|
|
127
|
+
"pr_url": pr_url,
|
|
128
|
+
"result": result,
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
# Fallback: Add PR link as comment
|
|
132
|
+
from ....core.models import Comment
|
|
133
|
+
|
|
134
|
+
comment = Comment(
|
|
135
|
+
ticket_id=ticket_id,
|
|
136
|
+
content=f"Pull Request: {pr_url}",
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
created_comment = await adapter.add_comment(comment)
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
"status": "completed",
|
|
143
|
+
"ticket_id": ticket_id,
|
|
144
|
+
"pr_url": pr_url,
|
|
145
|
+
"method": "comment",
|
|
146
|
+
"comment": created_comment.model_dump(),
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
except Exception as e:
|
|
150
|
+
return {
|
|
151
|
+
"status": "error",
|
|
152
|
+
"error": f"Failed to link pull request: {str(e)}",
|
|
153
|
+
"ticket_id": ticket_id,
|
|
154
|
+
}
|
|
@@ -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
|
+
}
|