mcp-ticketer 0.2.0__py3-none-any.whl → 2.2.9__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.
- mcp_ticketer/__init__.py +10 -10
- mcp_ticketer/__version__.py +3 -3
- mcp_ticketer/_version_scm.py +1 -0
- mcp_ticketer/adapters/__init__.py +2 -0
- mcp_ticketer/adapters/aitrackdown.py +930 -52
- mcp_ticketer/adapters/asana/__init__.py +15 -0
- mcp_ticketer/adapters/asana/adapter.py +1537 -0
- mcp_ticketer/adapters/asana/client.py +292 -0
- mcp_ticketer/adapters/asana/mappers.py +348 -0
- mcp_ticketer/adapters/asana/types.py +146 -0
- mcp_ticketer/adapters/github/__init__.py +26 -0
- mcp_ticketer/adapters/github/adapter.py +3229 -0
- mcp_ticketer/adapters/github/client.py +335 -0
- mcp_ticketer/adapters/github/mappers.py +797 -0
- mcp_ticketer/adapters/github/queries.py +692 -0
- mcp_ticketer/adapters/github/types.py +460 -0
- mcp_ticketer/adapters/hybrid.py +58 -16
- mcp_ticketer/adapters/jira/__init__.py +35 -0
- mcp_ticketer/adapters/jira/adapter.py +1351 -0
- mcp_ticketer/adapters/jira/client.py +271 -0
- mcp_ticketer/adapters/jira/mappers.py +246 -0
- mcp_ticketer/adapters/jira/queries.py +216 -0
- mcp_ticketer/adapters/jira/types.py +304 -0
- mcp_ticketer/adapters/linear/__init__.py +1 -1
- mcp_ticketer/adapters/linear/adapter.py +3810 -462
- mcp_ticketer/adapters/linear/client.py +312 -69
- mcp_ticketer/adapters/linear/mappers.py +305 -85
- mcp_ticketer/adapters/linear/queries.py +317 -17
- mcp_ticketer/adapters/linear/types.py +187 -64
- mcp_ticketer/adapters/linear.py +2 -2
- mcp_ticketer/analysis/__init__.py +56 -0
- mcp_ticketer/analysis/dependency_graph.py +255 -0
- mcp_ticketer/analysis/health_assessment.py +304 -0
- mcp_ticketer/analysis/orphaned.py +218 -0
- mcp_ticketer/analysis/project_status.py +594 -0
- mcp_ticketer/analysis/similarity.py +224 -0
- mcp_ticketer/analysis/staleness.py +266 -0
- mcp_ticketer/automation/__init__.py +11 -0
- mcp_ticketer/automation/project_updates.py +378 -0
- mcp_ticketer/cache/memory.py +9 -8
- mcp_ticketer/cli/adapter_diagnostics.py +421 -0
- mcp_ticketer/cli/auggie_configure.py +116 -15
- mcp_ticketer/cli/codex_configure.py +274 -82
- mcp_ticketer/cli/configure.py +1323 -151
- mcp_ticketer/cli/cursor_configure.py +314 -0
- mcp_ticketer/cli/diagnostics.py +209 -114
- mcp_ticketer/cli/discover.py +297 -26
- mcp_ticketer/cli/gemini_configure.py +119 -26
- mcp_ticketer/cli/init_command.py +880 -0
- mcp_ticketer/cli/install_mcp_server.py +418 -0
- mcp_ticketer/cli/instruction_commands.py +435 -0
- mcp_ticketer/cli/linear_commands.py +256 -130
- mcp_ticketer/cli/main.py +140 -1284
- mcp_ticketer/cli/mcp_configure.py +1013 -100
- mcp_ticketer/cli/mcp_server_commands.py +415 -0
- mcp_ticketer/cli/migrate_config.py +12 -8
- mcp_ticketer/cli/platform_commands.py +123 -0
- mcp_ticketer/cli/platform_detection.py +477 -0
- mcp_ticketer/cli/platform_installer.py +545 -0
- mcp_ticketer/cli/project_update_commands.py +350 -0
- mcp_ticketer/cli/python_detection.py +126 -0
- mcp_ticketer/cli/queue_commands.py +15 -15
- mcp_ticketer/cli/setup_command.py +794 -0
- mcp_ticketer/cli/simple_health.py +84 -59
- mcp_ticketer/cli/ticket_commands.py +1375 -0
- mcp_ticketer/cli/update_checker.py +313 -0
- mcp_ticketer/cli/utils.py +195 -72
- mcp_ticketer/core/__init__.py +64 -1
- mcp_ticketer/core/adapter.py +618 -18
- mcp_ticketer/core/config.py +77 -68
- mcp_ticketer/core/env_discovery.py +75 -16
- mcp_ticketer/core/env_loader.py +121 -97
- mcp_ticketer/core/exceptions.py +32 -24
- mcp_ticketer/core/http_client.py +26 -26
- mcp_ticketer/core/instructions.py +405 -0
- mcp_ticketer/core/label_manager.py +732 -0
- mcp_ticketer/core/mappers.py +42 -30
- mcp_ticketer/core/milestone_manager.py +252 -0
- mcp_ticketer/core/models.py +566 -19
- mcp_ticketer/core/onepassword_secrets.py +379 -0
- mcp_ticketer/core/priority_matcher.py +463 -0
- mcp_ticketer/core/project_config.py +189 -49
- mcp_ticketer/core/project_utils.py +281 -0
- mcp_ticketer/core/project_validator.py +376 -0
- mcp_ticketer/core/registry.py +3 -3
- mcp_ticketer/core/session_state.py +176 -0
- mcp_ticketer/core/state_matcher.py +592 -0
- mcp_ticketer/core/url_parser.py +425 -0
- mcp_ticketer/core/validators.py +69 -0
- 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/server/constants.py +58 -0
- mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
- mcp_ticketer/mcp/server/dto.py +195 -0
- mcp_ticketer/mcp/server/main.py +1343 -0
- mcp_ticketer/mcp/server/response_builder.py +206 -0
- mcp_ticketer/mcp/server/routing.py +723 -0
- mcp_ticketer/mcp/server/server_sdk.py +151 -0
- mcp_ticketer/mcp/server/tools/__init__.py +69 -0
- mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
- mcp_ticketer/mcp/server/tools/attachment_tools.py +224 -0
- mcp_ticketer/mcp/server/tools/bulk_tools.py +330 -0
- mcp_ticketer/mcp/server/tools/comment_tools.py +152 -0
- mcp_ticketer/mcp/server/tools/config_tools.py +1564 -0
- mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
- mcp_ticketer/mcp/server/tools/hierarchy_tools.py +942 -0
- mcp_ticketer/mcp/server/tools/instruction_tools.py +295 -0
- mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
- mcp_ticketer/mcp/server/tools/milestone_tools.py +338 -0
- mcp_ticketer/mcp/server/tools/pr_tools.py +150 -0
- mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
- mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
- mcp_ticketer/mcp/server/tools/search_tools.py +318 -0
- mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
- mcp_ticketer/mcp/server/tools/ticket_tools.py +1413 -0
- mcp_ticketer/mcp/server/tools/user_ticket_tools.py +364 -0
- mcp_ticketer/queue/__init__.py +1 -0
- mcp_ticketer/queue/health_monitor.py +168 -136
- mcp_ticketer/queue/manager.py +78 -63
- mcp_ticketer/queue/queue.py +108 -21
- mcp_ticketer/queue/run_worker.py +2 -2
- mcp_ticketer/queue/ticket_registry.py +213 -155
- mcp_ticketer/queue/worker.py +96 -58
- mcp_ticketer/utils/__init__.py +5 -0
- mcp_ticketer/utils/token_utils.py +246 -0
- mcp_ticketer-2.2.9.dist-info/METADATA +1396 -0
- mcp_ticketer-2.2.9.dist-info/RECORD +158 -0
- mcp_ticketer-2.2.9.dist-info/top_level.txt +2 -0
- py_mcp_installer/examples/phase3_demo.py +178 -0
- py_mcp_installer/scripts/manage_version.py +54 -0
- py_mcp_installer/setup.py +6 -0
- py_mcp_installer/src/py_mcp_installer/__init__.py +153 -0
- py_mcp_installer/src/py_mcp_installer/command_builder.py +445 -0
- py_mcp_installer/src/py_mcp_installer/config_manager.py +541 -0
- py_mcp_installer/src/py_mcp_installer/exceptions.py +243 -0
- py_mcp_installer/src/py_mcp_installer/installation_strategy.py +617 -0
- py_mcp_installer/src/py_mcp_installer/installer.py +656 -0
- py_mcp_installer/src/py_mcp_installer/mcp_inspector.py +750 -0
- py_mcp_installer/src/py_mcp_installer/platform_detector.py +451 -0
- py_mcp_installer/src/py_mcp_installer/platforms/__init__.py +26 -0
- py_mcp_installer/src/py_mcp_installer/platforms/claude_code.py +225 -0
- py_mcp_installer/src/py_mcp_installer/platforms/codex.py +181 -0
- py_mcp_installer/src/py_mcp_installer/platforms/cursor.py +191 -0
- py_mcp_installer/src/py_mcp_installer/types.py +222 -0
- py_mcp_installer/src/py_mcp_installer/utils.py +463 -0
- py_mcp_installer/tests/__init__.py +0 -0
- py_mcp_installer/tests/platforms/__init__.py +0 -0
- py_mcp_installer/tests/test_platform_detector.py +17 -0
- mcp_ticketer/adapters/github.py +0 -1354
- mcp_ticketer/adapters/jira.py +0 -1011
- mcp_ticketer/mcp/server.py +0 -1895
- mcp_ticketer-0.2.0.dist-info/METADATA +0 -414
- mcp_ticketer-0.2.0.dist-info/RECORD +0 -58
- mcp_ticketer-0.2.0.dist-info/top_level.txt +0 -1
- {mcp_ticketer-0.2.0.dist-info → mcp_ticketer-2.2.9.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.2.0.dist-info → mcp_ticketer-2.2.9.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.2.0.dist-info → mcp_ticketer-2.2.9.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""Attachment management tools for tickets.
|
|
2
|
+
|
|
3
|
+
This module implements tools for attaching files to tickets and retrieving
|
|
4
|
+
attachment information. Note that file attachment functionality may not be
|
|
5
|
+
available in all adapters.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import mimetypes
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from ....core.models import Comment, TicketType
|
|
13
|
+
from ..server_sdk import get_adapter
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def ticket_attach(
|
|
17
|
+
ticket_id: str,
|
|
18
|
+
file_path: str,
|
|
19
|
+
description: str = "",
|
|
20
|
+
) -> dict[str, Any]: # Keep as dict for MCP compatibility
|
|
21
|
+
"""Attach a file to a ticket.
|
|
22
|
+
|
|
23
|
+
Uploads a file and associates it with the specified ticket. This
|
|
24
|
+
functionality may not be available in all adapters.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
ticket_id: Unique identifier of the ticket
|
|
28
|
+
file_path: Path to the file to attach
|
|
29
|
+
description: Optional description of the attachment
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Attachment details including URL or ID, or error information
|
|
33
|
+
|
|
34
|
+
"""
|
|
35
|
+
try:
|
|
36
|
+
adapter = get_adapter()
|
|
37
|
+
|
|
38
|
+
# Read ticket to validate it exists and determine type
|
|
39
|
+
ticket = await adapter.read(ticket_id)
|
|
40
|
+
if ticket is None:
|
|
41
|
+
return {
|
|
42
|
+
"status": "error",
|
|
43
|
+
"error": f"Ticket {ticket_id} not found",
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# Check if file exists
|
|
47
|
+
file_path_obj = Path(file_path)
|
|
48
|
+
if not file_path_obj.exists():
|
|
49
|
+
return {
|
|
50
|
+
"status": "error",
|
|
51
|
+
"error": f"File not found: {file_path}",
|
|
52
|
+
"ticket_id": ticket_id,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# Try Linear-specific upload methods first (most advanced)
|
|
56
|
+
if hasattr(adapter, "upload_file") and hasattr(adapter, "attach_file_to_issue"):
|
|
57
|
+
try:
|
|
58
|
+
# Determine MIME type
|
|
59
|
+
mime_type = mimetypes.guess_type(file_path)[0]
|
|
60
|
+
|
|
61
|
+
# Upload file to Linear's storage
|
|
62
|
+
file_url = await adapter.upload_file(file_path, mime_type)
|
|
63
|
+
|
|
64
|
+
# Determine ticket type and attach accordingly
|
|
65
|
+
ticket_type = getattr(ticket, "ticket_type", None)
|
|
66
|
+
filename = file_path_obj.name
|
|
67
|
+
|
|
68
|
+
if ticket_type == TicketType.EPIC and hasattr(
|
|
69
|
+
adapter, "attach_file_to_epic"
|
|
70
|
+
):
|
|
71
|
+
# Attach to epic (project)
|
|
72
|
+
result = await adapter.attach_file_to_epic(
|
|
73
|
+
epic_id=ticket_id,
|
|
74
|
+
file_url=file_url,
|
|
75
|
+
title=description or filename,
|
|
76
|
+
subtitle=f"Uploaded file: {filename}",
|
|
77
|
+
)
|
|
78
|
+
else:
|
|
79
|
+
# Attach to issue/task
|
|
80
|
+
result = await adapter.attach_file_to_issue(
|
|
81
|
+
issue_id=ticket_id,
|
|
82
|
+
file_url=file_url,
|
|
83
|
+
title=description or filename,
|
|
84
|
+
subtitle=f"Uploaded file: {filename}",
|
|
85
|
+
comment_body=description if description else None,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
"status": "completed",
|
|
90
|
+
"ticket_id": ticket_id,
|
|
91
|
+
"method": "linear_native_upload",
|
|
92
|
+
"file_url": file_url,
|
|
93
|
+
"attachment": result,
|
|
94
|
+
}
|
|
95
|
+
except Exception:
|
|
96
|
+
# Fall through to legacy method if Linear-specific upload fails
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
# Try legacy add_attachment method
|
|
100
|
+
if hasattr(adapter, "add_attachment"):
|
|
101
|
+
attachment = await adapter.add_attachment(
|
|
102
|
+
ticket_id=ticket_id, file_path=file_path, description=description
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
"status": "completed",
|
|
107
|
+
"ticket_id": ticket_id,
|
|
108
|
+
"method": "adapter_native",
|
|
109
|
+
"attachment": attachment,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
# Fallback: Add file reference as comment
|
|
113
|
+
comment_text = f"Attachment: {file_path}"
|
|
114
|
+
if description:
|
|
115
|
+
comment_text += f"\nDescription: {description}"
|
|
116
|
+
|
|
117
|
+
comment = Comment(
|
|
118
|
+
ticket_id=ticket_id,
|
|
119
|
+
content=comment_text,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
created_comment = await adapter.add_comment(comment)
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
"status": "completed",
|
|
126
|
+
"ticket_id": ticket_id,
|
|
127
|
+
"method": "comment_reference",
|
|
128
|
+
"file_path": file_path,
|
|
129
|
+
"comment": created_comment.model_dump(),
|
|
130
|
+
"note": "Adapter does not support direct file uploads. File reference added as comment.",
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
except FileNotFoundError:
|
|
134
|
+
return {
|
|
135
|
+
"status": "error",
|
|
136
|
+
"error": f"File not found: {file_path}",
|
|
137
|
+
"ticket_id": ticket_id,
|
|
138
|
+
}
|
|
139
|
+
except Exception as e:
|
|
140
|
+
return {
|
|
141
|
+
"status": "error",
|
|
142
|
+
"error": f"Failed to attach file: {str(e)}",
|
|
143
|
+
"ticket_id": ticket_id,
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
async def ticket_attachments(
|
|
148
|
+
ticket_id: str,
|
|
149
|
+
) -> dict[str, Any]: # Keep as dict for MCP compatibility
|
|
150
|
+
"""Get all attachments for a ticket.
|
|
151
|
+
|
|
152
|
+
Retrieves a list of all files attached to the specified ticket.
|
|
153
|
+
This functionality may not be available in all adapters.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
ticket_id: Unique identifier of the ticket
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
List of attachments with metadata, or error information
|
|
160
|
+
|
|
161
|
+
"""
|
|
162
|
+
try:
|
|
163
|
+
adapter = get_adapter()
|
|
164
|
+
|
|
165
|
+
# Read ticket to validate it exists
|
|
166
|
+
ticket = await adapter.read(ticket_id)
|
|
167
|
+
if ticket is None:
|
|
168
|
+
return {
|
|
169
|
+
"status": "error",
|
|
170
|
+
"error": f"Ticket {ticket_id} not found",
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
# Check if adapter supports attachments
|
|
174
|
+
if not hasattr(adapter, "get_attachments"):
|
|
175
|
+
return {
|
|
176
|
+
"status": "error",
|
|
177
|
+
"error": f"Attachment retrieval not supported by {type(adapter).__name__} adapter",
|
|
178
|
+
"ticket_id": ticket_id,
|
|
179
|
+
"note": "Check ticket comments for file references",
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
# Get attachments via adapter
|
|
183
|
+
attachments = await adapter.get_attachments(ticket_id)
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
"status": "completed",
|
|
187
|
+
"ticket_id": ticket_id,
|
|
188
|
+
"attachments": attachments,
|
|
189
|
+
"count": len(attachments) if isinstance(attachments, list) else 0,
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
except AttributeError:
|
|
193
|
+
# Fallback: Check comments for attachment references
|
|
194
|
+
comments = await adapter.get_comments(ticket_id=ticket_id, limit=100)
|
|
195
|
+
|
|
196
|
+
# Look for comments that reference files
|
|
197
|
+
attachment_refs = []
|
|
198
|
+
for comment in comments:
|
|
199
|
+
content = comment.content or ""
|
|
200
|
+
if content.startswith("Attachment:") or "file://" in content:
|
|
201
|
+
attachment_refs.append(
|
|
202
|
+
{
|
|
203
|
+
"type": "comment_reference",
|
|
204
|
+
"comment_id": comment.id,
|
|
205
|
+
"content": content,
|
|
206
|
+
"created_at": comment.created_at,
|
|
207
|
+
}
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
"status": "completed",
|
|
212
|
+
"ticket_id": ticket_id,
|
|
213
|
+
"method": "comment_references",
|
|
214
|
+
"attachments": attachment_refs,
|
|
215
|
+
"count": len(attachment_refs),
|
|
216
|
+
"note": "Adapter does not support direct attachments. Showing file references from comments.",
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
except Exception as e:
|
|
220
|
+
return {
|
|
221
|
+
"status": "error",
|
|
222
|
+
"error": f"Failed to get attachments: {str(e)}",
|
|
223
|
+
"ticket_id": ticket_id,
|
|
224
|
+
}
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
"""Bulk operations for creating and updating multiple tickets.
|
|
2
|
+
|
|
3
|
+
This module implements tools for batch operations on tickets to improve
|
|
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
|
+
}
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from ....core.models import Priority, Task, TicketState, TicketType
|
|
20
|
+
from ..server_sdk import get_adapter, mcp
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@mcp.tool()
|
|
24
|
+
async def ticket_bulk(
|
|
25
|
+
action: str,
|
|
26
|
+
tickets: list[dict[str, Any]] | None = None,
|
|
27
|
+
updates: list[dict[str, Any]] | None = None,
|
|
28
|
+
) -> dict[str, Any]:
|
|
29
|
+
"""Unified bulk ticket operations tool.
|
|
30
|
+
|
|
31
|
+
Performs bulk create or update operations on tickets through a single
|
|
32
|
+
interface.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
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
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
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
|
|
74
|
+
"""
|
|
75
|
+
action_lower = action.lower()
|
|
76
|
+
|
|
77
|
+
# Route to appropriate handler based on action
|
|
78
|
+
if action_lower == "create":
|
|
79
|
+
if tickets is None:
|
|
80
|
+
return {
|
|
81
|
+
"status": "error",
|
|
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": [],
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
for i, ticket_data in enumerate(tickets):
|
|
101
|
+
try:
|
|
102
|
+
# Validate required fields
|
|
103
|
+
if "title" not in ticket_data:
|
|
104
|
+
results["failed"].append(
|
|
105
|
+
{
|
|
106
|
+
"index": i,
|
|
107
|
+
"error": "Missing required field: title",
|
|
108
|
+
"data": ticket_data,
|
|
109
|
+
}
|
|
110
|
+
)
|
|
111
|
+
continue
|
|
112
|
+
|
|
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
|
+
)
|
|
154
|
+
|
|
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
|
+
)
|
|
163
|
+
|
|
164
|
+
except Exception as e:
|
|
165
|
+
results["failed"].append(
|
|
166
|
+
{
|
|
167
|
+
"index": i,
|
|
168
|
+
"error": str(e),
|
|
169
|
+
"data": ticket_data,
|
|
170
|
+
}
|
|
171
|
+
)
|
|
172
|
+
|
|
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
|
+
}
|
|
182
|
+
|
|
183
|
+
except Exception as e:
|
|
184
|
+
return {
|
|
185
|
+
"status": "error",
|
|
186
|
+
"error": f"Bulk creation failed: {str(e)}",
|
|
187
|
+
}
|
|
188
|
+
|
|
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
|
+
}
|
|
210
|
+
|
|
211
|
+
for i, update_data in enumerate(updates):
|
|
212
|
+
try:
|
|
213
|
+
# Validate required fields
|
|
214
|
+
if "ticket_id" not in update_data:
|
|
215
|
+
results["failed"].append(
|
|
216
|
+
{
|
|
217
|
+
"index": i,
|
|
218
|
+
"error": "Missing required field: ticket_id",
|
|
219
|
+
"data": update_data,
|
|
220
|
+
}
|
|
221
|
+
)
|
|
222
|
+
continue
|
|
223
|
+
|
|
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:
|
|
271
|
+
results["failed"].append(
|
|
272
|
+
{
|
|
273
|
+
"index": i,
|
|
274
|
+
"error": "No valid update fields provided",
|
|
275
|
+
"data": update_data,
|
|
276
|
+
}
|
|
277
|
+
)
|
|
278
|
+
continue
|
|
279
|
+
|
|
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
|
+
)
|
|
297
|
+
|
|
298
|
+
except Exception as e:
|
|
299
|
+
results["failed"].append(
|
|
300
|
+
{
|
|
301
|
+
"index": i,
|
|
302
|
+
"error": str(e),
|
|
303
|
+
"data": update_data,
|
|
304
|
+
}
|
|
305
|
+
)
|
|
306
|
+
|
|
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
|
+
}
|
|
316
|
+
|
|
317
|
+
except Exception as e:
|
|
318
|
+
return {
|
|
319
|
+
"status": "error",
|
|
320
|
+
"error": f"Bulk update failed: {str(e)}",
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
else:
|
|
324
|
+
valid_actions = ["create", "update"]
|
|
325
|
+
return {
|
|
326
|
+
"status": "error",
|
|
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', ...)",
|
|
330
|
+
}
|