mcp-ticketer 2.0.1__py3-none-any.whl → 2.2.13__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 +1 -1
- mcp_ticketer/_version_scm.py +1 -0
- mcp_ticketer/adapters/aitrackdown.py +122 -0
- mcp_ticketer/adapters/asana/adapter.py +121 -0
- mcp_ticketer/adapters/github/__init__.py +26 -0
- mcp_ticketer/adapters/{github.py → github/adapter.py} +1506 -365
- 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/jira/__init__.py +35 -0
- mcp_ticketer/adapters/{jira.py → jira/adapter.py} +250 -678
- 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/adapter.py +1000 -92
- mcp_ticketer/adapters/linear/client.py +91 -1
- mcp_ticketer/adapters/linear/mappers.py +107 -0
- mcp_ticketer/adapters/linear/queries.py +112 -2
- mcp_ticketer/adapters/linear/types.py +50 -10
- mcp_ticketer/cli/configure.py +524 -89
- mcp_ticketer/cli/install_mcp_server.py +418 -0
- mcp_ticketer/cli/main.py +10 -0
- mcp_ticketer/cli/mcp_configure.py +177 -49
- mcp_ticketer/cli/platform_installer.py +9 -0
- mcp_ticketer/cli/setup_command.py +157 -1
- mcp_ticketer/cli/ticket_commands.py +443 -81
- mcp_ticketer/cli/utils.py +113 -0
- mcp_ticketer/core/__init__.py +28 -0
- mcp_ticketer/core/adapter.py +367 -1
- mcp_ticketer/core/milestone_manager.py +252 -0
- mcp_ticketer/core/models.py +345 -0
- mcp_ticketer/core/project_utils.py +281 -0
- mcp_ticketer/core/project_validator.py +376 -0
- mcp_ticketer/core/session_state.py +6 -1
- mcp_ticketer/core/state_matcher.py +36 -3
- mcp_ticketer/mcp/server/__main__.py +2 -1
- mcp_ticketer/mcp/server/routing.py +68 -0
- mcp_ticketer/mcp/server/tools/__init__.py +7 -4
- mcp_ticketer/mcp/server/tools/attachment_tools.py +3 -1
- mcp_ticketer/mcp/server/tools/config_tools.py +233 -35
- mcp_ticketer/mcp/server/tools/milestone_tools.py +338 -0
- mcp_ticketer/mcp/server/tools/search_tools.py +30 -1
- mcp_ticketer/mcp/server/tools/ticket_tools.py +37 -1
- mcp_ticketer/queue/queue.py +68 -0
- {mcp_ticketer-2.0.1.dist-info → mcp_ticketer-2.2.13.dist-info}/METADATA +33 -3
- {mcp_ticketer-2.0.1.dist-info → mcp_ticketer-2.2.13.dist-info}/RECORD +72 -36
- mcp_ticketer-2.2.13.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-2.0.1.dist-info/top_level.txt +0 -1
- {mcp_ticketer-2.0.1.dist-info → mcp_ticketer-2.2.13.dist-info}/WHEEL +0 -0
- {mcp_ticketer-2.0.1.dist-info → mcp_ticketer-2.2.13.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-2.0.1.dist-info → mcp_ticketer-2.2.13.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
|
+
}
|
|
@@ -10,6 +10,8 @@ from typing import Any
|
|
|
10
10
|
from ....core.models import Priority, SearchQuery, TicketState
|
|
11
11
|
from ..server_sdk import get_adapter, mcp
|
|
12
12
|
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
13
15
|
|
|
14
16
|
@mcp.tool()
|
|
15
17
|
async def ticket_search(
|
|
@@ -19,12 +21,13 @@ async def ticket_search(
|
|
|
19
21
|
tags: list[str] | None = None,
|
|
20
22
|
assignee: str | None = None,
|
|
21
23
|
project_id: str | None = None,
|
|
24
|
+
milestone_id: str | None = None,
|
|
22
25
|
limit: int = 10,
|
|
23
26
|
include_hierarchy: bool = False,
|
|
24
27
|
include_children: bool = True,
|
|
25
28
|
max_depth: int = 3,
|
|
26
29
|
) -> dict[str, Any]:
|
|
27
|
-
"""Search tickets with optional hierarchy information.
|
|
30
|
+
"""Search tickets with optional hierarchy information and milestone filtering.
|
|
28
31
|
|
|
29
32
|
**Consolidates:**
|
|
30
33
|
- ticket_search() → Default behavior (include_hierarchy=False)
|
|
@@ -44,6 +47,7 @@ async def ticket_search(
|
|
|
44
47
|
- tags: Filter by tags (AND logic)
|
|
45
48
|
- assignee: Filter by assigned user
|
|
46
49
|
- project_id: Scope to specific project
|
|
50
|
+
- milestone_id: Filter by milestone (NEW in 1M-607)
|
|
47
51
|
|
|
48
52
|
**Hierarchy Options:**
|
|
49
53
|
- include_hierarchy: Include parent/child relationships (default: False)
|
|
@@ -57,6 +61,7 @@ async def ticket_search(
|
|
|
57
61
|
tags: Filter by tags - tickets must have all specified tags
|
|
58
62
|
assignee: Filter by assigned user ID or email
|
|
59
63
|
project_id: Project/epic ID (required unless default_project configured)
|
|
64
|
+
milestone_id: Filter by milestone ID (NEW in 1M-607)
|
|
60
65
|
limit: Maximum number of results to return (default: 10, max: 100)
|
|
61
66
|
include_hierarchy: Include parent/child relationships (default: False)
|
|
62
67
|
include_children: Include child tickets in hierarchy (default: True)
|
|
@@ -77,6 +82,13 @@ async def ticket_search(
|
|
|
77
82
|
max_depth=2
|
|
78
83
|
)
|
|
79
84
|
|
|
85
|
+
# Search within milestone
|
|
86
|
+
await ticket_search(
|
|
87
|
+
milestone_id="milestone-123",
|
|
88
|
+
state="open",
|
|
89
|
+
limit=20
|
|
90
|
+
)
|
|
91
|
+
|
|
80
92
|
"""
|
|
81
93
|
try:
|
|
82
94
|
# Validate project context (NEW: Required for search operations)
|
|
@@ -141,6 +153,23 @@ async def ticket_search(
|
|
|
141
153
|
# Execute search via adapter
|
|
142
154
|
results = await adapter.search(search_query)
|
|
143
155
|
|
|
156
|
+
# Filter by milestone if requested (NEW in 1M-607)
|
|
157
|
+
if milestone_id:
|
|
158
|
+
try:
|
|
159
|
+
# Get issues in milestone
|
|
160
|
+
milestone_issues = await adapter.milestone_get_issues(
|
|
161
|
+
milestone_id, state=state
|
|
162
|
+
)
|
|
163
|
+
milestone_issue_ids = {issue.id for issue in milestone_issues}
|
|
164
|
+
|
|
165
|
+
# Filter search results to only include milestone issues
|
|
166
|
+
results = [
|
|
167
|
+
ticket for ticket in results if ticket.id in milestone_issue_ids
|
|
168
|
+
]
|
|
169
|
+
except Exception as e:
|
|
170
|
+
logger.warning(f"Failed to filter by milestone {milestone_id}: {e}")
|
|
171
|
+
# Continue with unfiltered results if milestone filtering fails
|
|
172
|
+
|
|
144
173
|
# Add hierarchy if requested
|
|
145
174
|
if include_hierarchy:
|
|
146
175
|
# Validate max_depth
|
|
@@ -973,7 +973,8 @@ async def ticket_list(
|
|
|
973
973
|
else:
|
|
974
974
|
ticket_data = [ticket.model_dump() for ticket in tickets]
|
|
975
975
|
|
|
976
|
-
|
|
976
|
+
# Build response
|
|
977
|
+
response_data = {
|
|
977
978
|
"status": "completed",
|
|
978
979
|
**_build_adapter_metadata(adapter),
|
|
979
980
|
"tickets": ticket_data,
|
|
@@ -982,6 +983,41 @@ async def ticket_list(
|
|
|
982
983
|
"offset": offset,
|
|
983
984
|
"compact": compact,
|
|
984
985
|
}
|
|
986
|
+
|
|
987
|
+
# Estimate and validate token count to prevent MCP limit violations
|
|
988
|
+
# MCP has a 25k token limit per response; we use 20k as safety margin
|
|
989
|
+
from ....utils.token_utils import estimate_json_tokens
|
|
990
|
+
|
|
991
|
+
estimated_tokens = estimate_json_tokens(response_data)
|
|
992
|
+
|
|
993
|
+
# If exceeds 20k tokens (safety margin below 25k MCP limit)
|
|
994
|
+
if estimated_tokens > 20_000:
|
|
995
|
+
# Calculate recommended limit based on current token-per-ticket ratio
|
|
996
|
+
if len(tickets) > 0:
|
|
997
|
+
tokens_per_ticket = estimated_tokens / len(tickets)
|
|
998
|
+
recommended_limit = int(20_000 / tokens_per_ticket)
|
|
999
|
+
else:
|
|
1000
|
+
recommended_limit = 20
|
|
1001
|
+
|
|
1002
|
+
return {
|
|
1003
|
+
"status": "error",
|
|
1004
|
+
"error": f"Response would exceed MCP token limit ({estimated_tokens:,} tokens)",
|
|
1005
|
+
"recommendation": (
|
|
1006
|
+
f"Use smaller limit (try limit={recommended_limit}), "
|
|
1007
|
+
"add filters (state=open, project_id=...), or enable compact mode"
|
|
1008
|
+
),
|
|
1009
|
+
"current_settings": {
|
|
1010
|
+
"limit": limit,
|
|
1011
|
+
"compact": compact,
|
|
1012
|
+
"estimated_tokens": estimated_tokens,
|
|
1013
|
+
"max_allowed": 25_000,
|
|
1014
|
+
},
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
# Add token estimate to successful response for monitoring
|
|
1018
|
+
response_data["estimated_tokens"] = estimated_tokens
|
|
1019
|
+
|
|
1020
|
+
return response_data
|
|
985
1021
|
except Exception as e:
|
|
986
1022
|
error_response = {
|
|
987
1023
|
"status": "error",
|
mcp_ticketer/queue/queue.py
CHANGED
|
@@ -535,3 +535,71 @@ class Queue:
|
|
|
535
535
|
stats[status] = count
|
|
536
536
|
|
|
537
537
|
return stats
|
|
538
|
+
|
|
539
|
+
def poll_until_complete(
|
|
540
|
+
self,
|
|
541
|
+
queue_id: str,
|
|
542
|
+
timeout: float = 30.0,
|
|
543
|
+
poll_interval: float = 0.5,
|
|
544
|
+
) -> QueueItem:
|
|
545
|
+
"""Poll queue item until completion or timeout.
|
|
546
|
+
|
|
547
|
+
This enables synchronous waiting for queue operations, useful for:
|
|
548
|
+
- CLI commands that need immediate results
|
|
549
|
+
- Testing that requires actual ticket IDs
|
|
550
|
+
- GitHub adapter operations requiring ticket numbers
|
|
551
|
+
|
|
552
|
+
Args:
|
|
553
|
+
queue_id: Queue ID to poll (e.g., "Q-9E7B5050")
|
|
554
|
+
timeout: Maximum seconds to wait (default: 30.0)
|
|
555
|
+
poll_interval: Seconds between polls (default: 0.5)
|
|
556
|
+
|
|
557
|
+
Returns:
|
|
558
|
+
Completed QueueItem with result data
|
|
559
|
+
|
|
560
|
+
Raises:
|
|
561
|
+
TimeoutError: If operation doesn't complete within timeout
|
|
562
|
+
RuntimeError: If operation fails or queue item not found
|
|
563
|
+
|
|
564
|
+
Example:
|
|
565
|
+
>>> queue = Queue()
|
|
566
|
+
>>> queue_id = queue.add(ticket_data={...}, adapter="github", operation="create")
|
|
567
|
+
>>> # Start worker to process the queue
|
|
568
|
+
>>> completed_item = queue.poll_until_complete(queue_id, timeout=30)
|
|
569
|
+
>>> ticket_id = completed_item.result["id"]
|
|
570
|
+
|
|
571
|
+
"""
|
|
572
|
+
import time
|
|
573
|
+
|
|
574
|
+
start_time = time.time()
|
|
575
|
+
elapsed = 0.0
|
|
576
|
+
|
|
577
|
+
while elapsed < timeout:
|
|
578
|
+
item = self.get_item(queue_id)
|
|
579
|
+
|
|
580
|
+
if item is None:
|
|
581
|
+
raise RuntimeError(f"Queue item not found: {queue_id}")
|
|
582
|
+
|
|
583
|
+
if item.status == QueueStatus.COMPLETED:
|
|
584
|
+
if item.result is None:
|
|
585
|
+
raise RuntimeError(
|
|
586
|
+
f"Queue operation completed but has no result data: {queue_id}"
|
|
587
|
+
)
|
|
588
|
+
return item
|
|
589
|
+
|
|
590
|
+
elif item.status == QueueStatus.FAILED:
|
|
591
|
+
error_msg = item.error_message or "Unknown error"
|
|
592
|
+
raise RuntimeError(
|
|
593
|
+
f"Queue operation failed: {error_msg} (queue_id: {queue_id})"
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
# Still pending or processing - wait and retry
|
|
597
|
+
time.sleep(poll_interval)
|
|
598
|
+
elapsed = time.time() - start_time
|
|
599
|
+
|
|
600
|
+
# Timeout reached
|
|
601
|
+
final_item = self.get_item(queue_id)
|
|
602
|
+
final_status = final_item.status.value if final_item else "UNKNOWN"
|
|
603
|
+
raise TimeoutError(
|
|
604
|
+
f"Queue operation timed out after {timeout}s (status: {final_status}, queue_id: {queue_id})"
|
|
605
|
+
)
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcp-ticketer
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.13
|
|
4
4
|
Summary: Universal ticket management interface for AI agents with MCP support
|
|
5
5
|
Author-email: MCP Ticketer Team <support@mcp-ticketer.io>
|
|
6
6
|
Maintainer-email: MCP Ticketer Team <support@mcp-ticketer.io>
|
|
7
|
-
License: MIT
|
|
7
|
+
License-Expression: MIT
|
|
8
8
|
Project-URL: Homepage, https://github.com/mcp-ticketer/mcp-ticketer
|
|
9
9
|
Project-URL: Documentation, https://mcp-ticketer.readthedocs.io
|
|
10
10
|
Project-URL: Repository, https://github.com/mcp-ticketer/mcp-ticketer
|
|
@@ -14,7 +14,6 @@ Keywords: mcp,tickets,jira,linear,github,issue-tracking,project-management,ai,au
|
|
|
14
14
|
Classifier: Development Status :: 4 - Beta
|
|
15
15
|
Classifier: Intended Audience :: Developers
|
|
16
16
|
Classifier: Intended Audience :: System Administrators
|
|
17
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
18
17
|
Classifier: Operating System :: OS Independent
|
|
19
18
|
Classifier: Programming Language :: Python
|
|
20
19
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -56,9 +55,12 @@ Requires-Dist: pytest-xdist>=3.0.0; extra == "dev"
|
|
|
56
55
|
Requires-Dist: black>=23.0.0; extra == "dev"
|
|
57
56
|
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
58
57
|
Requires-Dist: mypy>=1.5.0; extra == "dev"
|
|
58
|
+
Requires-Dist: types-PyYAML>=6.0.0; extra == "dev"
|
|
59
59
|
Requires-Dist: tox>=4.11.0; extra == "dev"
|
|
60
60
|
Requires-Dist: pre-commit>=3.5.0; extra == "dev"
|
|
61
61
|
Requires-Dist: bump2version>=1.0.1; extra == "dev"
|
|
62
|
+
Requires-Dist: build>=1.0.0; extra == "dev"
|
|
63
|
+
Requires-Dist: twine>=5.0.0; extra == "dev"
|
|
62
64
|
Provides-Extra: docs
|
|
63
65
|
Requires-Dist: sphinx>=7.2.0; extra == "docs"
|
|
64
66
|
Requires-Dist: sphinx-rtd-theme>=2.0.0; extra == "docs"
|
|
@@ -160,6 +162,34 @@ pip install -e .
|
|
|
160
162
|
- Python 3.9+
|
|
161
163
|
- Virtual environment (recommended)
|
|
162
164
|
|
|
165
|
+
### PATH Configuration (Optional but Recommended)
|
|
166
|
+
|
|
167
|
+
For optimal Claude Desktop MCP integration, ensure `mcp-ticketer` is in your PATH:
|
|
168
|
+
|
|
169
|
+
**pipx users**:
|
|
170
|
+
```bash
|
|
171
|
+
export PATH="$HOME/.local/bin:$PATH"
|
|
172
|
+
# Add to ~/.bashrc or ~/.zshrc to make permanent
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
**uv users**:
|
|
176
|
+
```bash
|
|
177
|
+
export PATH="$HOME/.local/bin:$PATH" # Linux/macOS
|
|
178
|
+
# Add to ~/.bashrc or ~/.zshrc to make permanent
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**Why configure PATH?**
|
|
182
|
+
- ✅ **With PATH**: Native Claude CLI integration for better UX
|
|
183
|
+
- ⚠️ **Without PATH**: mcp-ticketer still works using full paths (legacy mode)
|
|
184
|
+
|
|
185
|
+
**Verify PATH configuration**:
|
|
186
|
+
```bash
|
|
187
|
+
which mcp-ticketer
|
|
188
|
+
# Should show: /Users/username/.local/bin/mcp-ticketer (or similar)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
**Note (v2.0.2+)**: The installer automatically detects if `mcp-ticketer` is in PATH and configures Claude Desktop appropriately. See [1M-579](https://linear.app/1m-hyperdev/issue/1M-579) for technical details.
|
|
192
|
+
|
|
163
193
|
## 🤖 Supported AI Clients
|
|
164
194
|
|
|
165
195
|
MCP Ticketer integrates with multiple AI clients via the Model Context Protocol (MCP):
|