mcp-ticketer 0.3.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 +91 -54
- 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 -1544
- 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 -2030
- mcp_ticketer-0.3.0.dist-info/METADATA +0 -414
- mcp_ticketer-0.3.0.dist-info/RECORD +0 -59
- mcp_ticketer-0.3.0.dist-info/top_level.txt +0 -1
- {mcp_ticketer-0.3.0.dist-info → mcp_ticketer-2.2.9.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.3.0.dist-info → mcp_ticketer-2.2.9.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.3.0.dist-info → mcp_ticketer-2.2.9.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
"""Unified milestone management tools (v2.0.0).
|
|
2
|
+
|
|
3
|
+
This module implements milestone management through a single unified `milestone()` interface.
|
|
4
|
+
|
|
5
|
+
Version 2.0.0 changes:
|
|
6
|
+
- Single `milestone()` function is the exposed MCP tool
|
|
7
|
+
- All operations accessible via milestone(action="create"|"get"|"list"|"update"|"delete"|"get_issues")
|
|
8
|
+
- Follows the pattern from ticket() and hierarchy() unified tools
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from typing import Any, Literal
|
|
14
|
+
|
|
15
|
+
from ....core.adapter import BaseAdapter
|
|
16
|
+
from ..server_sdk import get_adapter, mcp
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
# Sentinel value to distinguish between "parameter not provided" and "explicitly None"
|
|
21
|
+
_UNSET = object()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _build_adapter_metadata(
|
|
25
|
+
adapter: BaseAdapter,
|
|
26
|
+
milestone_id: str | None = None,
|
|
27
|
+
) -> dict[str, Any]:
|
|
28
|
+
"""Build adapter metadata for MCP responses.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
adapter: The adapter that handled the operation
|
|
32
|
+
milestone_id: Optional milestone ID to include in metadata
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Dictionary with adapter metadata fields
|
|
36
|
+
|
|
37
|
+
"""
|
|
38
|
+
metadata = {
|
|
39
|
+
"adapter": adapter.adapter_type,
|
|
40
|
+
"adapter_name": adapter.adapter_display_name,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if milestone_id:
|
|
44
|
+
metadata["milestone_id"] = milestone_id
|
|
45
|
+
|
|
46
|
+
return metadata
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@mcp.tool()
|
|
50
|
+
async def milestone(
|
|
51
|
+
action: Literal["create", "get", "list", "update", "delete", "get_issues"],
|
|
52
|
+
# Entity identification
|
|
53
|
+
milestone_id: str | None = None,
|
|
54
|
+
# Creation/Update parameters
|
|
55
|
+
name: str | None = None,
|
|
56
|
+
target_date: str | None = None,
|
|
57
|
+
labels: list[str] | None = None,
|
|
58
|
+
description: str = "",
|
|
59
|
+
state: str | None = None,
|
|
60
|
+
# List/filter parameters
|
|
61
|
+
project_id: str | None = None,
|
|
62
|
+
) -> dict[str, Any]:
|
|
63
|
+
"""Unified milestone management tool for cross-platform milestone support.
|
|
64
|
+
|
|
65
|
+
A milestone is a list of labels with target dates, into which issues can be grouped.
|
|
66
|
+
|
|
67
|
+
Consolidates all milestone operations into a single interface with progress tracking.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
action: Operation to perform:
|
|
71
|
+
- "create": Create new milestone
|
|
72
|
+
- "get": Get milestone by ID with progress
|
|
73
|
+
- "list": List milestones (optionally filtered)
|
|
74
|
+
- "update": Update milestone properties
|
|
75
|
+
- "delete": Delete milestone
|
|
76
|
+
- "get_issues": Get issues in milestone
|
|
77
|
+
milestone_id: Milestone ID (required for get, update, delete, get_issues)
|
|
78
|
+
name: Milestone name (required for create)
|
|
79
|
+
target_date: Target completion date (ISO format: YYYY-MM-DD)
|
|
80
|
+
labels: Labels that define this milestone (user's definition)
|
|
81
|
+
description: Milestone description
|
|
82
|
+
state: Milestone state (open, active, completed, closed)
|
|
83
|
+
project_id: Project/repository filter for list operations
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Operation results with status, data, and metadata
|
|
87
|
+
|
|
88
|
+
Raises:
|
|
89
|
+
ValueError: If action is invalid or required parameters missing
|
|
90
|
+
|
|
91
|
+
Examples:
|
|
92
|
+
# Create milestone
|
|
93
|
+
await milestone(
|
|
94
|
+
action="create",
|
|
95
|
+
name="v2.1.0 Release",
|
|
96
|
+
target_date="2025-12-31",
|
|
97
|
+
labels=["v2.1", "release"]
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Get milestone with progress
|
|
101
|
+
await milestone(action="get", milestone_id="milestone-123")
|
|
102
|
+
|
|
103
|
+
# List active milestones
|
|
104
|
+
await milestone(action="list", state="active")
|
|
105
|
+
|
|
106
|
+
# Update milestone
|
|
107
|
+
await milestone(
|
|
108
|
+
action="update",
|
|
109
|
+
milestone_id="milestone-123",
|
|
110
|
+
state="completed"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Get issues in milestone
|
|
114
|
+
await milestone(action="get_issues", milestone_id="milestone-123")
|
|
115
|
+
|
|
116
|
+
# Delete milestone
|
|
117
|
+
await milestone(action="delete", milestone_id="milestone-123")
|
|
118
|
+
|
|
119
|
+
"""
|
|
120
|
+
try:
|
|
121
|
+
# Get adapter from registry
|
|
122
|
+
adapter = get_adapter()
|
|
123
|
+
|
|
124
|
+
# Validate action
|
|
125
|
+
valid_actions = ["create", "get", "list", "update", "delete", "get_issues"]
|
|
126
|
+
if action not in valid_actions:
|
|
127
|
+
return {
|
|
128
|
+
"status": "error",
|
|
129
|
+
"error": f"Invalid action '{action}'. Must be one of: {', '.join(valid_actions)}",
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
# Route to appropriate handler
|
|
133
|
+
if action == "create":
|
|
134
|
+
return await _handle_create(
|
|
135
|
+
adapter, name, target_date, labels, description, project_id
|
|
136
|
+
)
|
|
137
|
+
elif action == "get":
|
|
138
|
+
return await _handle_get(adapter, milestone_id)
|
|
139
|
+
elif action == "list":
|
|
140
|
+
return await _handle_list(adapter, project_id, state)
|
|
141
|
+
elif action == "update":
|
|
142
|
+
return await _handle_update(
|
|
143
|
+
adapter, milestone_id, name, target_date, state, labels, description
|
|
144
|
+
)
|
|
145
|
+
elif action == "delete":
|
|
146
|
+
return await _handle_delete(adapter, milestone_id)
|
|
147
|
+
elif action == "get_issues":
|
|
148
|
+
return await _handle_get_issues(adapter, milestone_id, state)
|
|
149
|
+
|
|
150
|
+
except Exception as e:
|
|
151
|
+
logger.exception("Milestone operation failed")
|
|
152
|
+
return {
|
|
153
|
+
"status": "error",
|
|
154
|
+
"error": f"Milestone operation failed: {str(e)}",
|
|
155
|
+
"action": action,
|
|
156
|
+
"milestone_id": milestone_id,
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
async def _handle_create(
|
|
161
|
+
adapter: BaseAdapter,
|
|
162
|
+
name: str | None,
|
|
163
|
+
target_date: str | None,
|
|
164
|
+
labels: list[str] | None,
|
|
165
|
+
description: str,
|
|
166
|
+
project_id: str | None,
|
|
167
|
+
) -> dict[str, Any]:
|
|
168
|
+
"""Handle milestone creation."""
|
|
169
|
+
if not name:
|
|
170
|
+
return {
|
|
171
|
+
"status": "error",
|
|
172
|
+
"error": "name is required for create action",
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
# Parse target_date if provided (expect date object, not string)
|
|
176
|
+
parsed_date = None
|
|
177
|
+
if target_date:
|
|
178
|
+
try:
|
|
179
|
+
# Try parsing as ISO date string
|
|
180
|
+
parsed_date = datetime.fromisoformat(target_date)
|
|
181
|
+
except ValueError:
|
|
182
|
+
return {
|
|
183
|
+
"status": "error",
|
|
184
|
+
"error": f"Invalid date format '{target_date}'. Use ISO format: YYYY-MM-DD",
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
milestone_obj = await adapter.milestone_create(
|
|
188
|
+
name=name,
|
|
189
|
+
target_date=parsed_date,
|
|
190
|
+
labels=labels or [],
|
|
191
|
+
description=description,
|
|
192
|
+
project_id=project_id,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
"status": "completed",
|
|
197
|
+
"message": f"Milestone '{name}' created successfully",
|
|
198
|
+
"milestone": milestone_obj.model_dump(),
|
|
199
|
+
"metadata": _build_adapter_metadata(adapter, milestone_obj.id),
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
async def _handle_get(adapter: BaseAdapter, milestone_id: str | None) -> dict[str, Any]:
|
|
204
|
+
"""Handle milestone retrieval with progress calculation."""
|
|
205
|
+
if not milestone_id:
|
|
206
|
+
return {
|
|
207
|
+
"status": "error",
|
|
208
|
+
"error": "milestone_id is required for get action",
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
milestone_obj = await adapter.milestone_get(milestone_id)
|
|
212
|
+
|
|
213
|
+
if not milestone_obj:
|
|
214
|
+
return {
|
|
215
|
+
"status": "error",
|
|
216
|
+
"error": f"Milestone '{milestone_id}' not found",
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
"status": "completed",
|
|
221
|
+
"milestone": milestone_obj.model_dump(),
|
|
222
|
+
"metadata": _build_adapter_metadata(adapter, milestone_id),
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
async def _handle_list(
|
|
227
|
+
adapter: BaseAdapter,
|
|
228
|
+
project_id: str | None,
|
|
229
|
+
state: str | None,
|
|
230
|
+
) -> dict[str, Any]:
|
|
231
|
+
"""Handle milestone listing with optional filters."""
|
|
232
|
+
milestones = await adapter.milestone_list(project_id=project_id, state=state)
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
"status": "completed",
|
|
236
|
+
"message": f"Found {len(milestones)} milestone(s)",
|
|
237
|
+
"milestones": [m.model_dump() for m in milestones],
|
|
238
|
+
"count": len(milestones),
|
|
239
|
+
"metadata": _build_adapter_metadata(adapter),
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
async def _handle_update(
|
|
244
|
+
adapter: BaseAdapter,
|
|
245
|
+
milestone_id: str | None,
|
|
246
|
+
name: str | None,
|
|
247
|
+
target_date: str | None,
|
|
248
|
+
state: str | None,
|
|
249
|
+
labels: list[str] | None,
|
|
250
|
+
description: str | None,
|
|
251
|
+
) -> dict[str, Any]:
|
|
252
|
+
"""Handle milestone update."""
|
|
253
|
+
if not milestone_id:
|
|
254
|
+
return {
|
|
255
|
+
"status": "error",
|
|
256
|
+
"error": "milestone_id is required for update action",
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
# Parse target_date if provided
|
|
260
|
+
parsed_date = None
|
|
261
|
+
if target_date:
|
|
262
|
+
try:
|
|
263
|
+
parsed_date = datetime.fromisoformat(target_date)
|
|
264
|
+
except ValueError:
|
|
265
|
+
return {
|
|
266
|
+
"status": "error",
|
|
267
|
+
"error": f"Invalid date format '{target_date}'. Use ISO format: YYYY-MM-DD",
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
milestone_obj = await adapter.milestone_update(
|
|
271
|
+
milestone_id=milestone_id,
|
|
272
|
+
name=name,
|
|
273
|
+
target_date=parsed_date,
|
|
274
|
+
state=state,
|
|
275
|
+
labels=labels,
|
|
276
|
+
description=description,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
if not milestone_obj:
|
|
280
|
+
return {
|
|
281
|
+
"status": "error",
|
|
282
|
+
"error": f"Failed to update milestone '{milestone_id}'",
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
"status": "completed",
|
|
287
|
+
"message": f"Milestone '{milestone_id}' updated successfully",
|
|
288
|
+
"milestone": milestone_obj.model_dump(),
|
|
289
|
+
"metadata": _build_adapter_metadata(adapter, milestone_id),
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
async def _handle_delete(
|
|
294
|
+
adapter: BaseAdapter, milestone_id: str | None
|
|
295
|
+
) -> dict[str, Any]:
|
|
296
|
+
"""Handle milestone deletion."""
|
|
297
|
+
if not milestone_id:
|
|
298
|
+
return {
|
|
299
|
+
"status": "error",
|
|
300
|
+
"error": "milestone_id is required for delete action",
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
success = await adapter.milestone_delete(milestone_id)
|
|
304
|
+
|
|
305
|
+
if success:
|
|
306
|
+
return {
|
|
307
|
+
"status": "completed",
|
|
308
|
+
"message": f"Milestone '{milestone_id}' deleted successfully",
|
|
309
|
+
"metadata": _build_adapter_metadata(adapter, milestone_id),
|
|
310
|
+
}
|
|
311
|
+
else:
|
|
312
|
+
return {
|
|
313
|
+
"status": "error",
|
|
314
|
+
"error": f"Failed to delete milestone '{milestone_id}'",
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
async def _handle_get_issues(
|
|
319
|
+
adapter: BaseAdapter,
|
|
320
|
+
milestone_id: str | None,
|
|
321
|
+
state: str | None,
|
|
322
|
+
) -> dict[str, Any]:
|
|
323
|
+
"""Handle getting issues in milestone."""
|
|
324
|
+
if not milestone_id:
|
|
325
|
+
return {
|
|
326
|
+
"status": "error",
|
|
327
|
+
"error": "milestone_id is required for get_issues action",
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
issues = await adapter.milestone_get_issues(milestone_id, state=state)
|
|
331
|
+
|
|
332
|
+
return {
|
|
333
|
+
"status": "completed",
|
|
334
|
+
"message": f"Found {len(issues)} issue(s) in milestone",
|
|
335
|
+
"issues": [issue.model_dump() for issue in issues],
|
|
336
|
+
"count": len(issues),
|
|
337
|
+
"metadata": _build_adapter_metadata(adapter, milestone_id),
|
|
338
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
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
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
async def ticket_create_pr(
|
|
14
|
+
ticket_id: str,
|
|
15
|
+
title: str,
|
|
16
|
+
description: str = "",
|
|
17
|
+
source_branch: str | None = None,
|
|
18
|
+
target_branch: str = "main",
|
|
19
|
+
) -> dict[str, Any]:
|
|
20
|
+
"""Create a pull request linked to a ticket.
|
|
21
|
+
|
|
22
|
+
Creates a new pull request and automatically links it to the specified
|
|
23
|
+
ticket. This functionality may not be available in all adapters.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
ticket_id: Unique identifier of the ticket to link the PR to
|
|
27
|
+
title: Pull request title
|
|
28
|
+
description: Pull request description
|
|
29
|
+
source_branch: Source branch for the PR (if not specified, may use ticket ID)
|
|
30
|
+
target_branch: Target branch for the PR (default: main)
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Created PR details and link information, or error information
|
|
34
|
+
|
|
35
|
+
"""
|
|
36
|
+
try:
|
|
37
|
+
adapter = get_adapter()
|
|
38
|
+
|
|
39
|
+
# Check if adapter supports PR operations
|
|
40
|
+
if not hasattr(adapter, "create_pull_request"):
|
|
41
|
+
return {
|
|
42
|
+
"status": "error",
|
|
43
|
+
"error": f"Pull request creation not supported by {type(adapter).__name__} adapter",
|
|
44
|
+
"ticket_id": ticket_id,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# Read ticket to validate it exists
|
|
48
|
+
ticket = await adapter.read(ticket_id)
|
|
49
|
+
if ticket is None:
|
|
50
|
+
return {
|
|
51
|
+
"status": "error",
|
|
52
|
+
"error": f"Ticket {ticket_id} not found",
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# Use ticket ID as source branch if not specified
|
|
56
|
+
if source_branch is None:
|
|
57
|
+
source_branch = f"feature/{ticket_id}"
|
|
58
|
+
|
|
59
|
+
# Create PR via adapter
|
|
60
|
+
pr_data = await adapter.create_pull_request(
|
|
61
|
+
ticket_id=ticket_id,
|
|
62
|
+
title=title,
|
|
63
|
+
description=description,
|
|
64
|
+
source_branch=source_branch,
|
|
65
|
+
target_branch=target_branch,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
"status": "completed",
|
|
70
|
+
"ticket_id": ticket_id,
|
|
71
|
+
"pull_request": pr_data,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
except AttributeError:
|
|
75
|
+
return {
|
|
76
|
+
"status": "error",
|
|
77
|
+
"error": "Pull request creation not supported by this adapter",
|
|
78
|
+
"ticket_id": ticket_id,
|
|
79
|
+
}
|
|
80
|
+
except Exception as e:
|
|
81
|
+
return {
|
|
82
|
+
"status": "error",
|
|
83
|
+
"error": f"Failed to create pull request: {str(e)}",
|
|
84
|
+
"ticket_id": ticket_id,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
async def ticket_link_pr(
|
|
89
|
+
ticket_id: str,
|
|
90
|
+
pr_url: str,
|
|
91
|
+
) -> dict[str, Any]:
|
|
92
|
+
"""Link an existing pull request to a ticket.
|
|
93
|
+
|
|
94
|
+
Associates an existing pull request (identified by URL) with a ticket.
|
|
95
|
+
This is typically done by adding the PR URL to the ticket's metadata
|
|
96
|
+
or as a comment.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
ticket_id: Unique identifier of the ticket
|
|
100
|
+
pr_url: URL of the pull request to link
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Link confirmation and updated ticket details, or error information
|
|
104
|
+
|
|
105
|
+
"""
|
|
106
|
+
try:
|
|
107
|
+
adapter = get_adapter()
|
|
108
|
+
|
|
109
|
+
# Read ticket to validate it exists
|
|
110
|
+
ticket = await adapter.read(ticket_id)
|
|
111
|
+
if ticket is None:
|
|
112
|
+
return {
|
|
113
|
+
"status": "error",
|
|
114
|
+
"error": f"Ticket {ticket_id} not found",
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
# Check if adapter has specialized PR linking
|
|
118
|
+
if hasattr(adapter, "link_pull_request"):
|
|
119
|
+
result = await adapter.link_pull_request(ticket_id=ticket_id, pr_url=pr_url)
|
|
120
|
+
return {
|
|
121
|
+
"status": "completed",
|
|
122
|
+
"ticket_id": ticket_id,
|
|
123
|
+
"pr_url": pr_url,
|
|
124
|
+
"result": result,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
# Fallback: Add PR link as comment
|
|
128
|
+
from ....core.models import Comment
|
|
129
|
+
|
|
130
|
+
comment = Comment(
|
|
131
|
+
ticket_id=ticket_id,
|
|
132
|
+
content=f"Pull Request: {pr_url}",
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
created_comment = await adapter.add_comment(comment)
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
"status": "completed",
|
|
139
|
+
"ticket_id": ticket_id,
|
|
140
|
+
"pr_url": pr_url,
|
|
141
|
+
"method": "comment",
|
|
142
|
+
"comment": created_comment.model_dump(),
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
except Exception as e:
|
|
146
|
+
return {
|
|
147
|
+
"status": "error",
|
|
148
|
+
"error": f"Failed to link pull request: {str(e)}",
|
|
149
|
+
"ticket_id": ticket_id,
|
|
150
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""MCP tools for project status analysis and work planning.
|
|
2
|
+
|
|
3
|
+
This module provides PM-focused tools to analyze project health and
|
|
4
|
+
generate intelligent work plans with recommendations.
|
|
5
|
+
|
|
6
|
+
Tools:
|
|
7
|
+
- project_status: Comprehensive project/epic analysis with health assessment
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from ....analysis.project_status import StatusAnalyzer
|
|
14
|
+
from ....core.project_config import ConfigResolver
|
|
15
|
+
from ..server_sdk import get_adapter, mcp
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@mcp.tool()
|
|
21
|
+
async def project_status(project_id: str | None = None) -> dict[str, Any]:
|
|
22
|
+
"""Analyze project/epic status and generate work plan with recommendations.
|
|
23
|
+
|
|
24
|
+
Provides comprehensive project analysis including:
|
|
25
|
+
- Health assessment (on_track, at_risk, off_track)
|
|
26
|
+
- Status breakdown by state and priority
|
|
27
|
+
- Dependency analysis and critical path
|
|
28
|
+
- Top 3 recommended tickets to start next
|
|
29
|
+
- Blocker identification
|
|
30
|
+
- Work distribution by assignee
|
|
31
|
+
- Actionable recommendations for project managers
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
project_id: ID of the project/epic to analyze (optional, uses default_project if not provided)
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Complete project status analysis with recommendations, or error information
|
|
38
|
+
|
|
39
|
+
Example:
|
|
40
|
+
# Analyze specific project
|
|
41
|
+
result = await project_status(project_id="eac28953c267")
|
|
42
|
+
|
|
43
|
+
# Analyze default project
|
|
44
|
+
result = await project_status()
|
|
45
|
+
|
|
46
|
+
Example Response:
|
|
47
|
+
{
|
|
48
|
+
"status": "success",
|
|
49
|
+
"project_id": "eac28953c267",
|
|
50
|
+
"project_name": "MCP Ticketer",
|
|
51
|
+
"health": "at_risk",
|
|
52
|
+
"summary": {
|
|
53
|
+
"total": 4,
|
|
54
|
+
"open": 3,
|
|
55
|
+
"in_progress": 1,
|
|
56
|
+
"done": 0
|
|
57
|
+
},
|
|
58
|
+
"recommended_next": [
|
|
59
|
+
{
|
|
60
|
+
"ticket_id": "1M-317",
|
|
61
|
+
"title": "Fix project organization",
|
|
62
|
+
"priority": "critical",
|
|
63
|
+
"reason": "Critical priority, Unblocks 2 tickets",
|
|
64
|
+
"blocks": ["1M-315", "1M-316"]
|
|
65
|
+
}
|
|
66
|
+
],
|
|
67
|
+
"recommendations": [
|
|
68
|
+
"Resolve 1M-317 first (critical) - Unblocks 2 tickets",
|
|
69
|
+
"1 critical priority ticket needs attention"
|
|
70
|
+
]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
"""
|
|
74
|
+
try:
|
|
75
|
+
adapter = get_adapter()
|
|
76
|
+
|
|
77
|
+
# Use default project if not provided
|
|
78
|
+
if not project_id:
|
|
79
|
+
resolver = ConfigResolver()
|
|
80
|
+
config = resolver.resolve()
|
|
81
|
+
project_id = config.default_project
|
|
82
|
+
|
|
83
|
+
if not project_id:
|
|
84
|
+
return {
|
|
85
|
+
"status": "error",
|
|
86
|
+
"error": "No project_id provided and no default_project configured",
|
|
87
|
+
"message": "Use config_set_project to set a default project, or provide project_id parameter",
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# Read the epic/project to get name
|
|
91
|
+
try:
|
|
92
|
+
epic = await adapter.read(project_id)
|
|
93
|
+
if epic is None:
|
|
94
|
+
return {
|
|
95
|
+
"status": "error",
|
|
96
|
+
"error": f"Project/Epic {project_id} not found",
|
|
97
|
+
}
|
|
98
|
+
project_name = epic.title or project_id
|
|
99
|
+
except Exception as e:
|
|
100
|
+
logger.warning(
|
|
101
|
+
f"Failed to read project {project_id} for name: {e}. Using ID as name."
|
|
102
|
+
)
|
|
103
|
+
project_name = project_id
|
|
104
|
+
|
|
105
|
+
# Get all child issues
|
|
106
|
+
child_issue_ids = getattr(epic, "child_issues", [])
|
|
107
|
+
|
|
108
|
+
if not child_issue_ids:
|
|
109
|
+
return {
|
|
110
|
+
"status": "success",
|
|
111
|
+
"project_id": project_id,
|
|
112
|
+
"project_name": project_name,
|
|
113
|
+
"health": "on_track",
|
|
114
|
+
"summary": {"total": 0},
|
|
115
|
+
"message": "Project has no tickets yet",
|
|
116
|
+
"recommended_next": [],
|
|
117
|
+
"recommendations": ["Project is empty - Create tickets to get started"],
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
# Fetch each child issue
|
|
121
|
+
tickets = []
|
|
122
|
+
for issue_id in child_issue_ids:
|
|
123
|
+
try:
|
|
124
|
+
issue = await adapter.read(issue_id)
|
|
125
|
+
if issue:
|
|
126
|
+
tickets.append(issue)
|
|
127
|
+
except Exception as e:
|
|
128
|
+
logger.warning(f"Failed to read issue {issue_id}: {e}")
|
|
129
|
+
continue
|
|
130
|
+
|
|
131
|
+
if not tickets:
|
|
132
|
+
return {
|
|
133
|
+
"status": "success",
|
|
134
|
+
"project_id": project_id,
|
|
135
|
+
"project_name": project_name,
|
|
136
|
+
"health": "at_risk",
|
|
137
|
+
"summary": {"total": 0},
|
|
138
|
+
"message": f"Could not load tickets for project (found {len(child_issue_ids)} IDs but couldn't read them)",
|
|
139
|
+
"recommended_next": [],
|
|
140
|
+
"recommendations": ["Check ticket IDs and permissions"],
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
# Perform status analysis
|
|
144
|
+
analyzer = StatusAnalyzer()
|
|
145
|
+
result = analyzer.analyze(project_id, project_name, tickets)
|
|
146
|
+
|
|
147
|
+
# Convert to dict and add success status
|
|
148
|
+
result_dict = result.model_dump()
|
|
149
|
+
result_dict["status"] = "success"
|
|
150
|
+
|
|
151
|
+
return result_dict
|
|
152
|
+
|
|
153
|
+
except Exception as e:
|
|
154
|
+
logger.error(f"Error analyzing project status: {e}", exc_info=True)
|
|
155
|
+
return {
|
|
156
|
+
"status": "error",
|
|
157
|
+
"error": f"Failed to analyze project status: {str(e)}",
|
|
158
|
+
}
|