mcp-ticketer 0.4.11__py3-none-any.whl → 2.0.1__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 (111) hide show
  1. mcp_ticketer/__init__.py +10 -10
  2. mcp_ticketer/__version__.py +3 -3
  3. mcp_ticketer/adapters/__init__.py +2 -0
  4. mcp_ticketer/adapters/aitrackdown.py +394 -9
  5. mcp_ticketer/adapters/asana/__init__.py +15 -0
  6. mcp_ticketer/adapters/asana/adapter.py +1416 -0
  7. mcp_ticketer/adapters/asana/client.py +292 -0
  8. mcp_ticketer/adapters/asana/mappers.py +348 -0
  9. mcp_ticketer/adapters/asana/types.py +146 -0
  10. mcp_ticketer/adapters/github.py +836 -105
  11. mcp_ticketer/adapters/hybrid.py +47 -5
  12. mcp_ticketer/adapters/jira.py +772 -1
  13. mcp_ticketer/adapters/linear/adapter.py +2293 -108
  14. mcp_ticketer/adapters/linear/client.py +146 -12
  15. mcp_ticketer/adapters/linear/mappers.py +105 -11
  16. mcp_ticketer/adapters/linear/queries.py +168 -1
  17. mcp_ticketer/adapters/linear/types.py +80 -4
  18. mcp_ticketer/analysis/__init__.py +56 -0
  19. mcp_ticketer/analysis/dependency_graph.py +255 -0
  20. mcp_ticketer/analysis/health_assessment.py +304 -0
  21. mcp_ticketer/analysis/orphaned.py +218 -0
  22. mcp_ticketer/analysis/project_status.py +594 -0
  23. mcp_ticketer/analysis/similarity.py +224 -0
  24. mcp_ticketer/analysis/staleness.py +266 -0
  25. mcp_ticketer/automation/__init__.py +11 -0
  26. mcp_ticketer/automation/project_updates.py +378 -0
  27. mcp_ticketer/cache/memory.py +3 -3
  28. mcp_ticketer/cli/adapter_diagnostics.py +4 -2
  29. mcp_ticketer/cli/auggie_configure.py +18 -6
  30. mcp_ticketer/cli/codex_configure.py +175 -60
  31. mcp_ticketer/cli/configure.py +884 -146
  32. mcp_ticketer/cli/cursor_configure.py +314 -0
  33. mcp_ticketer/cli/diagnostics.py +31 -28
  34. mcp_ticketer/cli/discover.py +293 -21
  35. mcp_ticketer/cli/gemini_configure.py +18 -6
  36. mcp_ticketer/cli/init_command.py +880 -0
  37. mcp_ticketer/cli/instruction_commands.py +435 -0
  38. mcp_ticketer/cli/linear_commands.py +99 -15
  39. mcp_ticketer/cli/main.py +109 -2055
  40. mcp_ticketer/cli/mcp_configure.py +673 -99
  41. mcp_ticketer/cli/mcp_server_commands.py +415 -0
  42. mcp_ticketer/cli/migrate_config.py +12 -8
  43. mcp_ticketer/cli/platform_commands.py +6 -6
  44. mcp_ticketer/cli/platform_detection.py +477 -0
  45. mcp_ticketer/cli/platform_installer.py +536 -0
  46. mcp_ticketer/cli/project_update_commands.py +350 -0
  47. mcp_ticketer/cli/queue_commands.py +15 -15
  48. mcp_ticketer/cli/setup_command.py +639 -0
  49. mcp_ticketer/cli/simple_health.py +13 -11
  50. mcp_ticketer/cli/ticket_commands.py +277 -36
  51. mcp_ticketer/cli/update_checker.py +313 -0
  52. mcp_ticketer/cli/utils.py +45 -41
  53. mcp_ticketer/core/__init__.py +35 -1
  54. mcp_ticketer/core/adapter.py +170 -5
  55. mcp_ticketer/core/config.py +38 -31
  56. mcp_ticketer/core/env_discovery.py +33 -3
  57. mcp_ticketer/core/env_loader.py +7 -6
  58. mcp_ticketer/core/exceptions.py +10 -4
  59. mcp_ticketer/core/http_client.py +10 -10
  60. mcp_ticketer/core/instructions.py +405 -0
  61. mcp_ticketer/core/label_manager.py +732 -0
  62. mcp_ticketer/core/mappers.py +32 -20
  63. mcp_ticketer/core/models.py +136 -1
  64. mcp_ticketer/core/onepassword_secrets.py +379 -0
  65. mcp_ticketer/core/priority_matcher.py +463 -0
  66. mcp_ticketer/core/project_config.py +148 -14
  67. mcp_ticketer/core/registry.py +1 -1
  68. mcp_ticketer/core/session_state.py +171 -0
  69. mcp_ticketer/core/state_matcher.py +592 -0
  70. mcp_ticketer/core/url_parser.py +425 -0
  71. mcp_ticketer/core/validators.py +69 -0
  72. mcp_ticketer/defaults/ticket_instructions.md +644 -0
  73. mcp_ticketer/mcp/__init__.py +2 -2
  74. mcp_ticketer/mcp/server/__init__.py +2 -2
  75. mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
  76. mcp_ticketer/mcp/server/main.py +187 -93
  77. mcp_ticketer/mcp/server/routing.py +655 -0
  78. mcp_ticketer/mcp/server/server_sdk.py +58 -0
  79. mcp_ticketer/mcp/server/tools/__init__.py +37 -9
  80. mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
  81. mcp_ticketer/mcp/server/tools/attachment_tools.py +65 -20
  82. mcp_ticketer/mcp/server/tools/bulk_tools.py +259 -202
  83. mcp_ticketer/mcp/server/tools/comment_tools.py +74 -12
  84. mcp_ticketer/mcp/server/tools/config_tools.py +1429 -0
  85. mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
  86. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +878 -319
  87. mcp_ticketer/mcp/server/tools/instruction_tools.py +295 -0
  88. mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
  89. mcp_ticketer/mcp/server/tools/pr_tools.py +3 -7
  90. mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
  91. mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
  92. mcp_ticketer/mcp/server/tools/search_tools.py +180 -97
  93. mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
  94. mcp_ticketer/mcp/server/tools/ticket_tools.py +1182 -82
  95. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +364 -0
  96. mcp_ticketer/queue/health_monitor.py +1 -0
  97. mcp_ticketer/queue/manager.py +4 -4
  98. mcp_ticketer/queue/queue.py +3 -3
  99. mcp_ticketer/queue/run_worker.py +1 -1
  100. mcp_ticketer/queue/ticket_registry.py +2 -2
  101. mcp_ticketer/queue/worker.py +15 -13
  102. mcp_ticketer/utils/__init__.py +5 -0
  103. mcp_ticketer/utils/token_utils.py +246 -0
  104. mcp_ticketer-2.0.1.dist-info/METADATA +1366 -0
  105. mcp_ticketer-2.0.1.dist-info/RECORD +122 -0
  106. mcp_ticketer-0.4.11.dist-info/METADATA +0 -496
  107. mcp_ticketer-0.4.11.dist-info/RECORD +0 -77
  108. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/WHEEL +0 -0
  109. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/entry_points.txt +0 -0
  110. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/licenses/LICENSE +0 -0
  111. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,364 @@
1
+ """User-specific ticket management tools.
2
+
3
+ This module provides tools for managing tickets from a user's perspective,
4
+ including transitioning tickets through workflow states with validation.
5
+
6
+ Design Decision: Workflow State Validation
7
+ ------------------------------------------
8
+ State transitions are validated using TicketState.can_transition_to() to ensure
9
+ tickets follow the defined workflow. This prevents invalid state changes that
10
+ could break integrations or confuse team members.
11
+
12
+ Valid workflow transitions:
13
+ - OPEN → IN_PROGRESS, WAITING, BLOCKED, CLOSED
14
+ - IN_PROGRESS → READY, WAITING, BLOCKED, OPEN
15
+ - READY → TESTED, IN_PROGRESS, BLOCKED
16
+ - TESTED → DONE, IN_PROGRESS
17
+ - DONE → CLOSED
18
+ - WAITING/BLOCKED → OPEN, IN_PROGRESS, CLOSED
19
+ - CLOSED → (no transitions, terminal state)
20
+
21
+ Performance Considerations:
22
+ - State transition validation is O(1) lookup in predefined state machine
23
+ """
24
+
25
+ from typing import Any
26
+
27
+ from ....core.adapter import BaseAdapter
28
+ from ....core.models import TicketState
29
+ from ....core.state_matcher import get_state_matcher
30
+ from ..server_sdk import get_adapter, mcp
31
+
32
+
33
+ def _build_adapter_metadata(
34
+ adapter: BaseAdapter,
35
+ ticket_id: str | None = None,
36
+ ) -> dict[str, Any]:
37
+ """Build adapter metadata for MCP responses."""
38
+ metadata = {
39
+ "adapter": adapter.adapter_type,
40
+ "adapter_name": adapter.adapter_display_name,
41
+ }
42
+ if ticket_id:
43
+ metadata["ticket_id"] = ticket_id
44
+ return metadata
45
+
46
+
47
+ @mcp.tool()
48
+ async def get_available_transitions(ticket_id: str) -> dict[str, Any]:
49
+ """Get valid next states for ticket based on workflow state machine.
50
+
51
+ Args: ticket_id (required)
52
+ Returns: TransitionResponse with current_state, available_transitions, transition_descriptions, is_terminal
53
+ See: docs/ticket-workflows.md#valid-state-transitions
54
+ """
55
+ try:
56
+ # Get ticket from adapter
57
+ adapter = get_adapter()
58
+ ticket = await adapter.read(ticket_id)
59
+
60
+ if ticket is None:
61
+ return {
62
+ "status": "error",
63
+ "error": f"Ticket {ticket_id} not found",
64
+ }
65
+
66
+ # Get current state
67
+ current_state = ticket.state
68
+
69
+ # Get valid transitions from state machine
70
+ valid_transitions = TicketState.valid_transitions()
71
+ # Handle both TicketState enum and string values
72
+ if isinstance(current_state, str):
73
+ current_state = TicketState(current_state)
74
+ available = valid_transitions.get(current_state, [])
75
+
76
+ # Create human-readable descriptions
77
+ descriptions = {
78
+ TicketState.OPEN: "Move to backlog (not yet started)",
79
+ TicketState.IN_PROGRESS: "Begin active work on ticket",
80
+ TicketState.READY: "Mark as complete and ready for review/testing",
81
+ TicketState.TESTED: "Mark as tested and verified",
82
+ TicketState.DONE: "Mark as complete and accepted",
83
+ TicketState.WAITING: "Pause work while waiting for external dependency",
84
+ TicketState.BLOCKED: "Work is blocked by an impediment",
85
+ TicketState.CLOSED: "Close and archive ticket (final state)",
86
+ }
87
+
88
+ transition_descriptions = {
89
+ state.value: descriptions.get(state, "") for state in available
90
+ }
91
+
92
+ return {
93
+ "status": "completed",
94
+ **_build_adapter_metadata(adapter, ticket_id),
95
+ "current_state": current_state.value,
96
+ "available_transitions": [state.value for state in available],
97
+ "transition_descriptions": transition_descriptions,
98
+ "is_terminal": len(available) == 0,
99
+ }
100
+ except Exception as e:
101
+ return {
102
+ "status": "error",
103
+ "error": f"Failed to get available transitions: {str(e)}",
104
+ }
105
+
106
+
107
+ @mcp.tool()
108
+ async def ticket_transition(
109
+ ticket_id: str,
110
+ to_state: str,
111
+ comment: str | None = None,
112
+ auto_confirm: bool = True,
113
+ ) -> dict[str, Any]:
114
+ """Move ticket through workflow with validation and semantic matching (natural language support).
115
+
116
+ Args: ticket_id (required), to_state (supports natural language like "working on it"), comment (optional), auto_confirm (default: True)
117
+ Returns: TransitionResponse with status, ticket, previous_state, new_state, matched_state, confidence, suggestions (if ambiguous)
118
+ See: docs/ticket-workflows.md#semantic-state-matching, docs/ticket-workflows.md#valid-state-transitions
119
+ """
120
+ try:
121
+ # Get ticket from adapter
122
+ adapter = get_adapter()
123
+ ticket = await adapter.read(ticket_id)
124
+
125
+ if ticket is None:
126
+ return {
127
+ "status": "error",
128
+ "error": f"Ticket {ticket_id} not found",
129
+ }
130
+
131
+ # Store current state for response
132
+ current_state = ticket.state
133
+ # Handle both TicketState enum and string values
134
+ if isinstance(current_state, str):
135
+ current_state = TicketState(current_state)
136
+
137
+ # Use semantic matcher to resolve target state
138
+ matcher = get_state_matcher()
139
+ match_result = matcher.match_state(to_state)
140
+
141
+ # Build response with semantic match info
142
+ response: dict[str, Any] = {
143
+ "ticket_id": ticket_id,
144
+ "original_input": to_state,
145
+ "matched_state": match_result.state.value,
146
+ "confidence": match_result.confidence,
147
+ "match_type": match_result.match_type,
148
+ "current_state": current_state.value,
149
+ }
150
+
151
+ # Handle low confidence - provide suggestions
152
+ if match_result.is_low_confidence():
153
+ suggestions = matcher.suggest_states(to_state, top_n=3)
154
+ return {
155
+ **response,
156
+ "status": "ambiguous",
157
+ "message": "Input is ambiguous. Please choose from suggestions.",
158
+ "suggestions": [
159
+ {
160
+ "state": s.state.value,
161
+ "confidence": s.confidence,
162
+ "description": _get_state_description(s.state),
163
+ }
164
+ for s in suggestions
165
+ ],
166
+ }
167
+
168
+ # Handle medium confidence - needs confirmation unless auto_confirm
169
+ if match_result.is_medium_confidence() and not auto_confirm:
170
+ return {
171
+ **response,
172
+ "status": "needs_confirmation",
173
+ "message": f"Matched '{to_state}' to '{match_result.state.value}' with {match_result.confidence:.0%} confidence. Please confirm.",
174
+ "confirm_required": True,
175
+ }
176
+
177
+ target_state = match_result.state
178
+
179
+ # Validate transition using adapter (includes parent/child state constraints)
180
+ is_valid = await adapter.validate_transition(ticket_id, target_state)
181
+ if not is_valid:
182
+ # Check if it's a workflow violation or parent constraint violation
183
+ workflow_valid = current_state.can_transition_to(target_state)
184
+ valid_transitions = TicketState.valid_transitions().get(current_state, [])
185
+ valid_values = [s.value for s in valid_transitions]
186
+
187
+ if workflow_valid:
188
+ # Workflow is valid, so this must be a parent constraint violation
189
+ # Get children to determine max child state
190
+ from ....core.models import Task
191
+
192
+ if isinstance(ticket, Task) and ticket.children:
193
+ try:
194
+ children = await adapter.list_tasks_by_issue(ticket_id)
195
+ if children:
196
+ max_child_state = None
197
+ max_child_level = 0
198
+ for child in children:
199
+ child_state = child.state
200
+ if isinstance(child_state, str):
201
+ try:
202
+ child_state = TicketState(child_state)
203
+ except ValueError:
204
+ continue
205
+ child_level = child_state.completion_level()
206
+ if child_level > max_child_level:
207
+ max_child_level = child_level
208
+ max_child_state = child_state
209
+
210
+ return {
211
+ **response,
212
+ "status": "error",
213
+ "error": f"Cannot transition to '{target_state.value}': parent issue has children in higher completion states",
214
+ "reason": "parent_constraint_violation",
215
+ "max_child_state": (
216
+ max_child_state.value if max_child_state else None
217
+ ),
218
+ "message": f"Cannot transition to {target_state.value}: "
219
+ f"parent issue has children in higher completion states (max child state: {max_child_state.value if max_child_state else 'unknown'}). "
220
+ f"Please update child states first.",
221
+ "valid_transitions": valid_values,
222
+ }
223
+ except Exception:
224
+ # Fallback to generic message if we can't determine child states
225
+ pass
226
+
227
+ # Generic parent constraint violation message
228
+ return {
229
+ **response,
230
+ "status": "error",
231
+ "error": f"Cannot transition to '{target_state.value}': parent/child state constraint violation",
232
+ "reason": "parent_constraint_violation",
233
+ "message": f"Cannot transition to {target_state.value}: "
234
+ f"parent issue has children in higher completion states. Please update child states first.",
235
+ "valid_transitions": valid_values,
236
+ }
237
+ else:
238
+ # Workflow violation
239
+ return {
240
+ **response,
241
+ "status": "error",
242
+ "error": f"Invalid transition from '{current_state.value}' to '{target_state.value}'",
243
+ "reason": "workflow_violation",
244
+ "valid_transitions": valid_values,
245
+ "message": f"Cannot transition from {current_state.value} to {target_state.value}. "
246
+ f"Valid transitions: {', '.join(valid_values) if valid_values else 'none (terminal state)'}",
247
+ }
248
+
249
+ # Update ticket state
250
+ updated = await adapter.update(ticket_id, {"state": target_state})
251
+
252
+ if updated is None:
253
+ return {
254
+ **response,
255
+ "status": "error",
256
+ "error": f"Failed to update ticket {ticket_id}",
257
+ }
258
+
259
+ # Add comment if provided and adapter supports it
260
+ comment_added = False
261
+ if comment and hasattr(adapter, "add_comment"):
262
+ try:
263
+ await adapter.add_comment(ticket_id, comment)
264
+ comment_added = True
265
+ except Exception:
266
+ # Log but don't fail the transition
267
+ comment_added = False
268
+
269
+ # Auto project update hook (1M-315)
270
+ # Trigger automatic project update if enabled and ticket has parent epic
271
+ auto_update_result = None
272
+ try:
273
+ from pathlib import Path
274
+
275
+ from ....automation.project_updates import AutoProjectUpdateManager
276
+ from ....core.project_config import ConfigResolver
277
+
278
+ # Load config
279
+ resolver = ConfigResolver(project_path=Path.cwd())
280
+ config_obj = resolver.load_project_config()
281
+ config_dict = config_obj.to_dict() if config_obj else {}
282
+
283
+ # Check if auto updates enabled
284
+ auto_updates_mgr = AutoProjectUpdateManager(config_dict, adapter)
285
+ if auto_updates_mgr.is_enabled():
286
+ # Check if ticket has parent_epic
287
+ parent_epic = (
288
+ updated.parent_epic if hasattr(updated, "parent_epic") else None
289
+ )
290
+
291
+ if parent_epic:
292
+ # Only trigger on configured frequency
293
+ update_frequency = auto_updates_mgr.get_update_frequency()
294
+ should_trigger = update_frequency == "on_transition"
295
+
296
+ # For "on_completion", only trigger if transitioned to done/closed
297
+ if update_frequency == "on_completion":
298
+ from ....core.models import TicketState as TSEnum
299
+
300
+ should_trigger = target_state in (TSEnum.DONE, TSEnum.CLOSED)
301
+
302
+ if should_trigger:
303
+ auto_update_result = (
304
+ await auto_updates_mgr.create_transition_update(
305
+ ticket_id=ticket_id,
306
+ ticket_title=updated.title or "",
307
+ old_state=current_state.value,
308
+ new_state=target_state.value,
309
+ parent_epic=parent_epic,
310
+ )
311
+ )
312
+ except Exception as e:
313
+ # Log error but don't block the transition
314
+ import logging
315
+
316
+ logging.getLogger(__name__).warning(
317
+ f"Auto project update failed (non-blocking): {e}"
318
+ )
319
+
320
+ # Build final response
321
+ final_response = {
322
+ **response,
323
+ **_build_adapter_metadata(adapter, ticket_id),
324
+ "status": "completed",
325
+ "ticket": updated.model_dump(),
326
+ "previous_state": current_state.value,
327
+ "new_state": target_state.value,
328
+ "comment_added": comment_added,
329
+ "message": f"Ticket {ticket_id} transitioned from {current_state.value} to {target_state.value}",
330
+ }
331
+
332
+ # Include auto update result if applicable
333
+ if auto_update_result:
334
+ final_response["auto_project_update"] = auto_update_result
335
+
336
+ return final_response
337
+ except Exception as e:
338
+ return {
339
+ "status": "error",
340
+ "error": f"Failed to transition ticket: {str(e)}",
341
+ }
342
+
343
+
344
+ def _get_state_description(state: TicketState) -> str:
345
+ """Get human-readable description of a state.
346
+
347
+ Args:
348
+ state: TicketState to describe
349
+
350
+ Returns:
351
+ Description string
352
+
353
+ """
354
+ descriptions = {
355
+ TicketState.OPEN: "Work not yet started, in backlog",
356
+ TicketState.IN_PROGRESS: "Work is actively being done",
357
+ TicketState.READY: "Work complete, ready for review or testing",
358
+ TicketState.TESTED: "Work has been tested and verified",
359
+ TicketState.DONE: "Work is complete and accepted",
360
+ TicketState.WAITING: "Work paused, waiting for external dependency",
361
+ TicketState.BLOCKED: "Work blocked by an impediment",
362
+ TicketState.CLOSED: "Ticket closed or archived (final state)",
363
+ }
364
+ return descriptions.get(state, "")
@@ -39,6 +39,7 @@ class HealthAlert:
39
39
  self.timestamp = timestamp or datetime.now()
40
40
 
41
41
  def __str__(self) -> str:
42
+ """Return string representation of alert."""
42
43
  return f"[{self.level.upper()}] {self.message}"
43
44
 
44
45
 
@@ -19,7 +19,7 @@ logger = logging.getLogger(__name__)
19
19
  class WorkerManager:
20
20
  """Manages worker process with file-based locking."""
21
21
 
22
- def __init__(self):
22
+ def __init__(self) -> None:
23
23
  """Initialize worker manager."""
24
24
  # Lazy import to avoid circular dependency
25
25
  from .queue import Queue
@@ -54,7 +54,7 @@ class WorkerManager:
54
54
  # Lock already held
55
55
  return False
56
56
 
57
- def _release_lock(self):
57
+ def _release_lock(self) -> None:
58
58
  """Release worker lock."""
59
59
  if hasattr(self, "lock_fd"):
60
60
  fcntl.lockf(self.lock_fd, fcntl.LOCK_UN)
@@ -287,7 +287,7 @@ class WorkerManager:
287
287
  is_running = self.is_running()
288
288
  pid = self._get_pid() if is_running else None
289
289
 
290
- status = {"running": is_running, "pid": pid}
290
+ status: dict[str, Any] = {"running": is_running, "pid": pid}
291
291
 
292
292
  # Add process info if running
293
293
  if is_running and pid:
@@ -326,7 +326,7 @@ class WorkerManager:
326
326
  except (OSError, ValueError):
327
327
  return None
328
328
 
329
- def _cleanup(self):
329
+ def _cleanup(self) -> None:
330
330
  """Clean up lock and PID files."""
331
331
  self._release_lock()
332
332
  if self.pid_file.exists():
@@ -83,7 +83,7 @@ class Queue:
83
83
  self._lock = threading.Lock()
84
84
  self._init_database()
85
85
 
86
- def _init_database(self):
86
+ def _init_database(self) -> None:
87
87
  """Initialize database schema."""
88
88
  with sqlite3.connect(self.db_path) as conn:
89
89
  conn.execute(
@@ -462,7 +462,7 @@ class Queue:
462
462
 
463
463
  return cursor.fetchone()[0]
464
464
 
465
- def cleanup_old(self, days: int = 7):
465
+ def cleanup_old(self, days: int = 7) -> None:
466
466
  """Clean up old completed/failed items.
467
467
 
468
468
  Args:
@@ -487,7 +487,7 @@ class Queue:
487
487
  )
488
488
  conn.commit()
489
489
 
490
- def reset_stuck_items(self, timeout_minutes: int = 30):
490
+ def reset_stuck_items(self, timeout_minutes: int = 30) -> None:
491
491
  """Reset items stuck in processing state.
492
492
 
493
493
  Args:
@@ -13,7 +13,7 @@ logging.basicConfig(
13
13
  logger = logging.getLogger(__name__)
14
14
 
15
15
 
16
- def main():
16
+ def main() -> None:
17
17
  """Run the worker process."""
18
18
  import os
19
19
 
@@ -27,7 +27,7 @@ class TicketRegistry:
27
27
  self._lock = threading.Lock()
28
28
  self._init_database()
29
29
 
30
- def _init_database(self):
30
+ def _init_database(self) -> None:
31
31
  """Initialize database schema."""
32
32
  with sqlite3.connect(self.db_path) as conn:
33
33
  # Ticket registry table
@@ -149,7 +149,7 @@ class TicketRegistry:
149
149
  with self._lock:
150
150
  with sqlite3.connect(self.db_path) as conn:
151
151
  update_fields = ["status = ?", "updated_at = ?"]
152
- values = [status, datetime.now().isoformat()]
152
+ values: list[Any] = [status, datetime.now().isoformat()]
153
153
 
154
154
  if ticket_id is not None:
155
155
  update_fields.append("ticket_id = ?")
@@ -97,12 +97,12 @@ class Worker:
97
97
  f"Worker initialized with batch_size={batch_size}, max_concurrent={max_concurrent}"
98
98
  )
99
99
 
100
- def _signal_handler(self, signum, frame):
100
+ def _signal_handler(self, signum: int, frame: Any) -> None:
101
101
  """Handle shutdown signals."""
102
102
  logger.info(f"Received signal {signum}, shutting down...")
103
103
  self.stop()
104
104
 
105
- def start(self, daemon: bool = True):
105
+ def start(self, daemon: bool = True) -> None:
106
106
  """Start the worker.
107
107
 
108
108
  Args:
@@ -126,14 +126,14 @@ class Worker:
126
126
  # Run in main thread
127
127
  self._run_loop()
128
128
 
129
- def stop(self):
129
+ def stop(self) -> None:
130
130
  """Stop the worker."""
131
131
  logger.info("Stopping worker...")
132
132
  self.running = False
133
133
  self.stop_event.set()
134
134
 
135
- def _run_loop(self):
136
- """Main worker loop with batch processing."""
135
+ def _run_loop(self) -> None:
136
+ """Run main worker loop with batch processing."""
137
137
  logger.info("Worker loop started")
138
138
 
139
139
  # Reset any stuck items on startup
@@ -174,7 +174,7 @@ class Worker:
174
174
  break
175
175
  return batch
176
176
 
177
- async def _process_batch(self, batch: list[QueueItem]):
177
+ async def _process_batch(self, batch: list[QueueItem]) -> None:
178
178
  """Process a batch of queue items with concurrency control.
179
179
 
180
180
  Args:
@@ -184,7 +184,7 @@ class Worker:
184
184
  logger.info(f"Processing batch of {len(batch)} items")
185
185
 
186
186
  # Group items by adapter for concurrent processing
187
- adapter_groups = {}
187
+ adapter_groups: dict[str, list[Any]] = {}
188
188
  for item in batch:
189
189
  if item.adapter not in adapter_groups:
190
190
  adapter_groups[item.adapter] = []
@@ -199,7 +199,9 @@ class Worker:
199
199
  # Wait for all adapter groups to complete
200
200
  await asyncio.gather(*tasks, return_exceptions=True)
201
201
 
202
- async def _process_adapter_group(self, adapter: str, items: list[QueueItem]):
202
+ async def _process_adapter_group(
203
+ self, adapter: str, items: list[QueueItem]
204
+ ) -> None:
203
205
  """Process items for a specific adapter with concurrency control.
204
206
 
205
207
  Args:
@@ -216,7 +218,7 @@ class Worker:
216
218
  semaphore = self.adapter_semaphores[adapter]
217
219
 
218
220
  # Process items with concurrency control
219
- async def process_with_semaphore(item):
221
+ async def process_with_semaphore(item: QueueItem) -> None:
220
222
  async with semaphore:
221
223
  await self._process_item(item)
222
224
 
@@ -226,7 +228,7 @@ class Worker:
226
228
  # Process with concurrency control
227
229
  await asyncio.gather(*tasks, return_exceptions=True)
228
230
 
229
- async def _process_item(self, item: QueueItem):
231
+ async def _process_item(self, item: QueueItem) -> None:
230
232
  """Process a single queue item.
231
233
 
232
234
  Args:
@@ -333,7 +335,7 @@ class Worker:
333
335
  self.stats["items_failed"] += 1
334
336
  logger.error(f"Max retries exceeded for {item.id}, marking as failed")
335
337
 
336
- async def _check_rate_limit(self, adapter: str):
338
+ async def _check_rate_limit(self, adapter: str) -> None:
337
339
  """Check and enforce rate limits.
338
340
 
339
341
  Args:
@@ -357,7 +359,7 @@ class Worker:
357
359
 
358
360
  self.last_request_times[adapter] = datetime.now()
359
361
 
360
- def _get_adapter(self, item: QueueItem):
362
+ def _get_adapter(self, item: QueueItem) -> Any:
361
363
  """Get adapter instance for item.
362
364
 
363
365
  Args:
@@ -442,7 +444,7 @@ class Worker:
442
444
 
443
445
  return adapter
444
446
 
445
- async def _execute_operation(self, adapter, item: QueueItem) -> dict[str, Any]:
447
+ async def _execute_operation(self, adapter: Any, item: QueueItem) -> dict[str, Any]:
446
448
  """Execute the queued operation.
447
449
 
448
450
  Args:
@@ -0,0 +1,5 @@
1
+ """Utility modules for mcp-ticketer."""
2
+
3
+ from .token_utils import estimate_json_tokens, estimate_tokens, paginate_response
4
+
5
+ __all__ = ["estimate_tokens", "estimate_json_tokens", "paginate_response"]