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.

Files changed (73) hide show
  1. mcp_ticketer/__version__.py +1 -1
  2. mcp_ticketer/_version_scm.py +1 -0
  3. mcp_ticketer/adapters/aitrackdown.py +122 -0
  4. mcp_ticketer/adapters/asana/adapter.py +121 -0
  5. mcp_ticketer/adapters/github/__init__.py +26 -0
  6. mcp_ticketer/adapters/{github.py → github/adapter.py} +1506 -365
  7. mcp_ticketer/adapters/github/client.py +335 -0
  8. mcp_ticketer/adapters/github/mappers.py +797 -0
  9. mcp_ticketer/adapters/github/queries.py +692 -0
  10. mcp_ticketer/adapters/github/types.py +460 -0
  11. mcp_ticketer/adapters/jira/__init__.py +35 -0
  12. mcp_ticketer/adapters/{jira.py → jira/adapter.py} +250 -678
  13. mcp_ticketer/adapters/jira/client.py +271 -0
  14. mcp_ticketer/adapters/jira/mappers.py +246 -0
  15. mcp_ticketer/adapters/jira/queries.py +216 -0
  16. mcp_ticketer/adapters/jira/types.py +304 -0
  17. mcp_ticketer/adapters/linear/adapter.py +1000 -92
  18. mcp_ticketer/adapters/linear/client.py +91 -1
  19. mcp_ticketer/adapters/linear/mappers.py +107 -0
  20. mcp_ticketer/adapters/linear/queries.py +112 -2
  21. mcp_ticketer/adapters/linear/types.py +50 -10
  22. mcp_ticketer/cli/configure.py +524 -89
  23. mcp_ticketer/cli/install_mcp_server.py +418 -0
  24. mcp_ticketer/cli/main.py +10 -0
  25. mcp_ticketer/cli/mcp_configure.py +177 -49
  26. mcp_ticketer/cli/platform_installer.py +9 -0
  27. mcp_ticketer/cli/setup_command.py +157 -1
  28. mcp_ticketer/cli/ticket_commands.py +443 -81
  29. mcp_ticketer/cli/utils.py +113 -0
  30. mcp_ticketer/core/__init__.py +28 -0
  31. mcp_ticketer/core/adapter.py +367 -1
  32. mcp_ticketer/core/milestone_manager.py +252 -0
  33. mcp_ticketer/core/models.py +345 -0
  34. mcp_ticketer/core/project_utils.py +281 -0
  35. mcp_ticketer/core/project_validator.py +376 -0
  36. mcp_ticketer/core/session_state.py +6 -1
  37. mcp_ticketer/core/state_matcher.py +36 -3
  38. mcp_ticketer/mcp/server/__main__.py +2 -1
  39. mcp_ticketer/mcp/server/routing.py +68 -0
  40. mcp_ticketer/mcp/server/tools/__init__.py +7 -4
  41. mcp_ticketer/mcp/server/tools/attachment_tools.py +3 -1
  42. mcp_ticketer/mcp/server/tools/config_tools.py +233 -35
  43. mcp_ticketer/mcp/server/tools/milestone_tools.py +338 -0
  44. mcp_ticketer/mcp/server/tools/search_tools.py +30 -1
  45. mcp_ticketer/mcp/server/tools/ticket_tools.py +37 -1
  46. mcp_ticketer/queue/queue.py +68 -0
  47. {mcp_ticketer-2.0.1.dist-info → mcp_ticketer-2.2.13.dist-info}/METADATA +33 -3
  48. {mcp_ticketer-2.0.1.dist-info → mcp_ticketer-2.2.13.dist-info}/RECORD +72 -36
  49. mcp_ticketer-2.2.13.dist-info/top_level.txt +2 -0
  50. py_mcp_installer/examples/phase3_demo.py +178 -0
  51. py_mcp_installer/scripts/manage_version.py +54 -0
  52. py_mcp_installer/setup.py +6 -0
  53. py_mcp_installer/src/py_mcp_installer/__init__.py +153 -0
  54. py_mcp_installer/src/py_mcp_installer/command_builder.py +445 -0
  55. py_mcp_installer/src/py_mcp_installer/config_manager.py +541 -0
  56. py_mcp_installer/src/py_mcp_installer/exceptions.py +243 -0
  57. py_mcp_installer/src/py_mcp_installer/installation_strategy.py +617 -0
  58. py_mcp_installer/src/py_mcp_installer/installer.py +656 -0
  59. py_mcp_installer/src/py_mcp_installer/mcp_inspector.py +750 -0
  60. py_mcp_installer/src/py_mcp_installer/platform_detector.py +451 -0
  61. py_mcp_installer/src/py_mcp_installer/platforms/__init__.py +26 -0
  62. py_mcp_installer/src/py_mcp_installer/platforms/claude_code.py +225 -0
  63. py_mcp_installer/src/py_mcp_installer/platforms/codex.py +181 -0
  64. py_mcp_installer/src/py_mcp_installer/platforms/cursor.py +191 -0
  65. py_mcp_installer/src/py_mcp_installer/types.py +222 -0
  66. py_mcp_installer/src/py_mcp_installer/utils.py +463 -0
  67. py_mcp_installer/tests/__init__.py +0 -0
  68. py_mcp_installer/tests/platforms/__init__.py +0 -0
  69. py_mcp_installer/tests/test_platform_detector.py +17 -0
  70. mcp_ticketer-2.0.1.dist-info/top_level.txt +0 -1
  71. {mcp_ticketer-2.0.1.dist-info → mcp_ticketer-2.2.13.dist-info}/WHEEL +0 -0
  72. {mcp_ticketer-2.0.1.dist-info → mcp_ticketer-2.2.13.dist-info}/entry_points.txt +0 -0
  73. {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
- return {
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",
@@ -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.0.1
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):