mcp-ticketer 0.1.30__py3-none-any.whl → 1.2.11__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 (109) 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 +796 -46
  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 +879 -129
  11. mcp_ticketer/adapters/hybrid.py +11 -11
  12. mcp_ticketer/adapters/jira.py +973 -73
  13. mcp_ticketer/adapters/linear/__init__.py +24 -0
  14. mcp_ticketer/adapters/linear/adapter.py +2732 -0
  15. mcp_ticketer/adapters/linear/client.py +344 -0
  16. mcp_ticketer/adapters/linear/mappers.py +420 -0
  17. mcp_ticketer/adapters/linear/queries.py +479 -0
  18. mcp_ticketer/adapters/linear/types.py +360 -0
  19. mcp_ticketer/adapters/linear.py +10 -2315
  20. mcp_ticketer/analysis/__init__.py +23 -0
  21. mcp_ticketer/analysis/orphaned.py +218 -0
  22. mcp_ticketer/analysis/similarity.py +224 -0
  23. mcp_ticketer/analysis/staleness.py +266 -0
  24. mcp_ticketer/cache/memory.py +9 -8
  25. mcp_ticketer/cli/adapter_diagnostics.py +421 -0
  26. mcp_ticketer/cli/auggie_configure.py +116 -15
  27. mcp_ticketer/cli/codex_configure.py +274 -82
  28. mcp_ticketer/cli/configure.py +888 -151
  29. mcp_ticketer/cli/diagnostics.py +400 -157
  30. mcp_ticketer/cli/discover.py +297 -26
  31. mcp_ticketer/cli/gemini_configure.py +119 -26
  32. mcp_ticketer/cli/init_command.py +880 -0
  33. mcp_ticketer/cli/instruction_commands.py +435 -0
  34. mcp_ticketer/cli/linear_commands.py +616 -0
  35. mcp_ticketer/cli/main.py +203 -1165
  36. mcp_ticketer/cli/mcp_configure.py +474 -90
  37. mcp_ticketer/cli/mcp_server_commands.py +415 -0
  38. mcp_ticketer/cli/migrate_config.py +12 -8
  39. mcp_ticketer/cli/platform_commands.py +123 -0
  40. mcp_ticketer/cli/platform_detection.py +418 -0
  41. mcp_ticketer/cli/platform_installer.py +513 -0
  42. mcp_ticketer/cli/python_detection.py +126 -0
  43. mcp_ticketer/cli/queue_commands.py +15 -15
  44. mcp_ticketer/cli/setup_command.py +639 -0
  45. mcp_ticketer/cli/simple_health.py +90 -65
  46. mcp_ticketer/cli/ticket_commands.py +1013 -0
  47. mcp_ticketer/cli/update_checker.py +313 -0
  48. mcp_ticketer/cli/utils.py +114 -66
  49. mcp_ticketer/core/__init__.py +24 -1
  50. mcp_ticketer/core/adapter.py +250 -16
  51. mcp_ticketer/core/config.py +145 -37
  52. mcp_ticketer/core/env_discovery.py +101 -22
  53. mcp_ticketer/core/env_loader.py +349 -0
  54. mcp_ticketer/core/exceptions.py +160 -0
  55. mcp_ticketer/core/http_client.py +26 -26
  56. mcp_ticketer/core/instructions.py +405 -0
  57. mcp_ticketer/core/label_manager.py +732 -0
  58. mcp_ticketer/core/mappers.py +42 -30
  59. mcp_ticketer/core/models.py +280 -28
  60. mcp_ticketer/core/onepassword_secrets.py +379 -0
  61. mcp_ticketer/core/project_config.py +183 -49
  62. mcp_ticketer/core/registry.py +3 -3
  63. mcp_ticketer/core/session_state.py +171 -0
  64. mcp_ticketer/core/state_matcher.py +592 -0
  65. mcp_ticketer/core/url_parser.py +425 -0
  66. mcp_ticketer/core/validators.py +69 -0
  67. mcp_ticketer/defaults/ticket_instructions.md +644 -0
  68. mcp_ticketer/mcp/__init__.py +29 -1
  69. mcp_ticketer/mcp/__main__.py +60 -0
  70. mcp_ticketer/mcp/server/__init__.py +25 -0
  71. mcp_ticketer/mcp/server/__main__.py +60 -0
  72. mcp_ticketer/mcp/server/constants.py +58 -0
  73. mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
  74. mcp_ticketer/mcp/server/dto.py +195 -0
  75. mcp_ticketer/mcp/server/main.py +1343 -0
  76. mcp_ticketer/mcp/server/response_builder.py +206 -0
  77. mcp_ticketer/mcp/server/routing.py +655 -0
  78. mcp_ticketer/mcp/server/server_sdk.py +151 -0
  79. mcp_ticketer/mcp/server/tools/__init__.py +56 -0
  80. mcp_ticketer/mcp/server/tools/analysis_tools.py +495 -0
  81. mcp_ticketer/mcp/server/tools/attachment_tools.py +226 -0
  82. mcp_ticketer/mcp/server/tools/bulk_tools.py +273 -0
  83. mcp_ticketer/mcp/server/tools/comment_tools.py +152 -0
  84. mcp_ticketer/mcp/server/tools/config_tools.py +1439 -0
  85. mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
  86. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +921 -0
  87. mcp_ticketer/mcp/server/tools/instruction_tools.py +300 -0
  88. mcp_ticketer/mcp/server/tools/label_tools.py +948 -0
  89. mcp_ticketer/mcp/server/tools/pr_tools.py +152 -0
  90. mcp_ticketer/mcp/server/tools/search_tools.py +215 -0
  91. mcp_ticketer/mcp/server/tools/session_tools.py +170 -0
  92. mcp_ticketer/mcp/server/tools/ticket_tools.py +1268 -0
  93. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +547 -0
  94. mcp_ticketer/queue/__init__.py +1 -0
  95. mcp_ticketer/queue/health_monitor.py +168 -136
  96. mcp_ticketer/queue/manager.py +95 -25
  97. mcp_ticketer/queue/queue.py +40 -21
  98. mcp_ticketer/queue/run_worker.py +6 -1
  99. mcp_ticketer/queue/ticket_registry.py +213 -155
  100. mcp_ticketer/queue/worker.py +109 -49
  101. mcp_ticketer-1.2.11.dist-info/METADATA +792 -0
  102. mcp_ticketer-1.2.11.dist-info/RECORD +110 -0
  103. mcp_ticketer/mcp/server.py +0 -1895
  104. mcp_ticketer-0.1.30.dist-info/METADATA +0 -413
  105. mcp_ticketer-0.1.30.dist-info/RECORD +0 -49
  106. {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/WHEEL +0 -0
  107. {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/entry_points.txt +0 -0
  108. {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/licenses/LICENSE +0 -0
  109. {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/top_level.txt +0 -0
@@ -4,14 +4,14 @@ import fcntl
4
4
  import logging
5
5
  import os
6
6
  import subprocess
7
- import sys
8
7
  import time
9
8
  from pathlib import Path
10
- from typing import Any, Optional
9
+ from typing import TYPE_CHECKING, Any
11
10
 
12
11
  import psutil
13
12
 
14
- from .queue import Queue
13
+ if TYPE_CHECKING:
14
+ pass
15
15
 
16
16
  logger = logging.getLogger(__name__)
17
17
 
@@ -19,8 +19,11 @@ 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
+ # Lazy import to avoid circular dependency
25
+ from .queue import Queue
26
+
24
27
  self.lock_file = Path.home() / ".mcp-ticketer" / "worker.lock"
25
28
  self.pid_file = Path.home() / ".mcp-ticketer" / "worker.pid"
26
29
  self.lock_file.parent.mkdir(parents=True, exist_ok=True)
@@ -51,7 +54,7 @@ class WorkerManager:
51
54
  # Lock already held
52
55
  return False
53
56
 
54
- def _release_lock(self):
57
+ def _release_lock(self) -> None:
55
58
  """Release worker lock."""
56
59
  if hasattr(self, "lock_fd"):
57
60
  fcntl.lockf(self.lock_fd, fcntl.LOCK_UN)
@@ -81,12 +84,22 @@ class WorkerManager:
81
84
  # Try to start worker
82
85
  return self.start()
83
86
 
84
- def start(self) -> bool:
87
+ def start(self, timeout: float = 30.0) -> bool:
85
88
  """Start the worker process.
86
89
 
90
+ Args:
91
+ timeout: Maximum seconds to wait for worker to become fully operational (default: 30)
92
+
87
93
  Returns:
88
94
  True if started successfully, False otherwise
89
95
 
96
+ Raises:
97
+ TimeoutError: If worker doesn't start within timeout period
98
+
99
+ Note:
100
+ If timeout occurs, a worker may already be running in another process.
101
+ Check for stale lock files or processes using `get_status()`.
102
+
90
103
  """
91
104
  # Check if already running
92
105
  if self.is_running():
@@ -96,11 +109,44 @@ class WorkerManager:
96
109
  # Try to acquire lock
97
110
  if not self._acquire_lock():
98
111
  logger.warning("Could not acquire lock - another worker may be running")
99
- return False
112
+ # Wait briefly to see if worker becomes operational
113
+ start_time = time.time()
114
+ while time.time() - start_time < timeout:
115
+ if self.is_running():
116
+ logger.info("Worker started by another process")
117
+ return True
118
+ time.sleep(1)
119
+
120
+ raise TimeoutError(
121
+ f"Worker start timed out after {timeout}s. "
122
+ f"Lock is held but worker is not running. "
123
+ f"Check for stale lock files or zombie processes."
124
+ )
100
125
 
101
126
  try:
102
- # Start worker in subprocess
103
- cmd = [sys.executable, "-m", "mcp_ticketer.queue.run_worker"]
127
+ # Start worker in subprocess using the same Python executable as the CLI
128
+ # This ensures the worker can import mcp_ticketer modules
129
+ # Lazy import to avoid circular dependency
130
+ from ..cli.python_detection import get_mcp_ticketer_python
131
+
132
+ python_executable = get_mcp_ticketer_python()
133
+ cmd = [python_executable, "-m", "mcp_ticketer.queue.run_worker"]
134
+
135
+ # Prepare environment for subprocess
136
+ # Ensure the subprocess gets the same environment as the parent
137
+ subprocess_env = os.environ.copy()
138
+
139
+ # Explicitly load environment variables from .env.local if it exists
140
+ env_file = Path.cwd() / ".env.local"
141
+ if env_file.exists():
142
+ logger.debug(f"Loading environment from {env_file} for subprocess")
143
+ from dotenv import dotenv_values
144
+
145
+ env_vars = dotenv_values(env_file)
146
+ subprocess_env.update(env_vars)
147
+ logger.debug(
148
+ f"Added {len(env_vars)} environment variables from .env.local"
149
+ )
104
150
 
105
151
  # Start as background process
106
152
  process = subprocess.Popen(
@@ -108,25 +154,49 @@ class WorkerManager:
108
154
  stdout=subprocess.DEVNULL,
109
155
  stderr=subprocess.DEVNULL,
110
156
  start_new_session=True,
157
+ env=subprocess_env, # Pass environment explicitly
158
+ cwd=str(Path.cwd()), # Ensure correct working directory
111
159
  )
112
160
 
113
161
  # Save PID
114
162
  self.pid_file.write_text(str(process.pid))
115
163
 
116
- # Give the process a moment to start
117
- import time
118
-
119
- time.sleep(0.5)
120
-
121
- # Verify process is running
122
- if not psutil.pid_exists(process.pid):
123
- logger.error("Worker process died immediately after starting")
124
- self._cleanup()
125
- return False
126
-
127
- logger.info(f"Started worker process with PID {process.pid}")
128
- return True
164
+ # Wait for worker to become fully operational with timeout
165
+ start_time = time.time()
166
+ while time.time() - start_time < timeout:
167
+ # Check if process exists
168
+ if not psutil.pid_exists(process.pid):
169
+ logger.error("Worker process died during startup")
170
+ self._cleanup()
171
+ return False
172
+
173
+ # Check if worker is fully running (not just process exists)
174
+ if self.is_running():
175
+ elapsed = time.time() - start_time
176
+ logger.info(
177
+ f"Worker started successfully in {elapsed:.1f}s with PID {process.pid}"
178
+ )
179
+ return True
180
+
181
+ # Log progress for long startups
182
+ elapsed = time.time() - start_time
183
+ if elapsed > 5 and int(elapsed) % 5 == 0:
184
+ logger.debug(
185
+ f"Waiting for worker to start, elapsed: {elapsed:.1f}s"
186
+ )
187
+
188
+ time.sleep(0.5)
189
+
190
+ # Timeout reached
191
+ raise TimeoutError(
192
+ f"Worker start timed out after {timeout}s. "
193
+ f"Process spawned (PID {process.pid}) but worker did not become operational. "
194
+ f"Check worker logs for startup errors."
195
+ )
129
196
 
197
+ except TimeoutError:
198
+ # Re-raise timeout errors with context preserved
199
+ raise
130
200
  except Exception as e:
131
201
  logger.error(f"Failed to start worker: {e}")
132
202
  self._release_lock()
@@ -217,7 +287,7 @@ class WorkerManager:
217
287
  is_running = self.is_running()
218
288
  pid = self._get_pid() if is_running else None
219
289
 
220
- status = {"running": is_running, "pid": pid}
290
+ status: dict[str, Any] = {"running": is_running, "pid": pid}
221
291
 
222
292
  # Add process info if running
223
293
  if is_running and pid:
@@ -240,7 +310,7 @@ class WorkerManager:
240
310
 
241
311
  return status
242
312
 
243
- def _get_pid(self) -> Optional[int]:
313
+ def _get_pid(self) -> int | None:
244
314
  """Get worker PID from file.
245
315
 
246
316
  Returns:
@@ -256,7 +326,7 @@ class WorkerManager:
256
326
  except (OSError, ValueError):
257
327
  return None
258
328
 
259
- def _cleanup(self):
329
+ def _cleanup(self) -> None:
260
330
  """Clean up lock and PID files."""
261
331
  self._release_lock()
262
332
  if self.pid_file.exists():
@@ -8,7 +8,7 @@ from dataclasses import asdict, dataclass
8
8
  from datetime import datetime, timedelta
9
9
  from enum import Enum
10
10
  from pathlib import Path
11
- from typing import Any, Optional
11
+ from typing import Any
12
12
 
13
13
 
14
14
  class QueueStatus(str, Enum):
@@ -30,11 +30,12 @@ class QueueItem:
30
30
  operation: str
31
31
  status: QueueStatus
32
32
  created_at: datetime
33
- processed_at: Optional[datetime] = None
34
- error_message: Optional[str] = None
33
+ processed_at: datetime | None = None
34
+ error_message: str | None = None
35
35
  retry_count: int = 0
36
- result: Optional[dict[str, Any]] = None
37
- project_dir: Optional[str] = None
36
+ result: dict[str, Any] | None = None
37
+ project_dir: str | None = None
38
+ adapter_config: dict[str, Any] | None = None # Adapter configuration
38
39
 
39
40
  def to_dict(self) -> dict:
40
41
  """Convert to dictionary for storage."""
@@ -59,13 +60,14 @@ class QueueItem:
59
60
  retry_count=row[8],
60
61
  result=json.loads(row[9]) if row[9] else None,
61
62
  project_dir=row[10] if len(row) > 10 else None,
63
+ adapter_config=json.loads(row[11]) if len(row) > 11 and row[11] else None,
62
64
  )
63
65
 
64
66
 
65
67
  class Queue:
66
68
  """Thread-safe SQLite queue for ticket operations."""
67
69
 
68
- def __init__(self, db_path: Optional[Path] = None):
70
+ def __init__(self, db_path: Path | None = None):
69
71
  """Initialize queue with database connection.
70
72
 
71
73
  Args:
@@ -81,7 +83,7 @@ class Queue:
81
83
  self._lock = threading.Lock()
82
84
  self._init_database()
83
85
 
84
- def _init_database(self):
86
+ def _init_database(self) -> None:
85
87
  """Initialize database schema."""
86
88
  with sqlite3.connect(self.db_path) as conn:
87
89
  conn.execute(
@@ -127,6 +129,8 @@ class Queue:
127
129
  columns = [row[1] for row in cursor.fetchall()]
128
130
  if "project_dir" not in columns:
129
131
  conn.execute("ALTER TABLE queue ADD COLUMN project_dir TEXT")
132
+ if "adapter_config" not in columns:
133
+ conn.execute("ALTER TABLE queue ADD COLUMN adapter_config TEXT")
130
134
 
131
135
  conn.commit()
132
136
 
@@ -135,7 +139,8 @@ class Queue:
135
139
  ticket_data: dict[str, Any],
136
140
  adapter: str,
137
141
  operation: str,
138
- project_dir: Optional[str] = None,
142
+ project_dir: str | None = None,
143
+ adapter_config: dict[str, Any] | None = None,
139
144
  ) -> str:
140
145
  """Add item to queue.
141
146
 
@@ -144,6 +149,7 @@ class Queue:
144
149
  adapter: Name of the adapter to use
145
150
  operation: Operation to perform (create, update, delete, etc.)
146
151
  project_dir: Project directory for config resolution (defaults to current directory)
152
+ adapter_config: Adapter configuration to use (optional, for explicit config passing)
147
153
 
148
154
  Returns:
149
155
  Queue ID for tracking
@@ -161,8 +167,8 @@ class Queue:
161
167
  """
162
168
  INSERT INTO queue (
163
169
  id, ticket_data, adapter, operation,
164
- status, created_at, retry_count, project_dir
165
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
170
+ status, created_at, retry_count, project_dir, adapter_config
171
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
166
172
  """,
167
173
  (
168
174
  queue_id,
@@ -173,13 +179,14 @@ class Queue:
173
179
  datetime.now().isoformat(),
174
180
  0,
175
181
  project_dir,
182
+ json.dumps(adapter_config) if adapter_config else None,
176
183
  ),
177
184
  )
178
185
  conn.commit()
179
186
 
180
187
  return queue_id
181
188
 
182
- def get_next_pending(self) -> Optional[QueueItem]:
189
+ def get_next_pending(self) -> QueueItem | None:
183
190
  """Get next pending item from queue atomically.
184
191
 
185
192
  Returns:
@@ -211,7 +218,12 @@ class Queue:
211
218
  SET status = ?, processed_at = ?
212
219
  WHERE id = ? AND status = ?
213
220
  """,
214
- (QueueStatus.PROCESSING.value, datetime.now().isoformat(), row[0], QueueStatus.PENDING.value),
221
+ (
222
+ QueueStatus.PROCESSING.value,
223
+ datetime.now().isoformat(),
224
+ row[0],
225
+ QueueStatus.PENDING.value,
226
+ ),
215
227
  )
216
228
 
217
229
  # Check if update was successful (prevents race conditions)
@@ -239,9 +251,9 @@ class Queue:
239
251
  self,
240
252
  queue_id: str,
241
253
  status: QueueStatus,
242
- error_message: Optional[str] = None,
243
- result: Optional[dict[str, Any]] = None,
244
- expected_status: Optional[QueueStatus] = None,
254
+ error_message: str | None = None,
255
+ result: dict[str, Any] | None = None,
256
+ expected_status: QueueStatus | None = None,
245
257
  ) -> bool:
246
258
  """Update queue item status atomically.
247
259
 
@@ -254,6 +266,7 @@ class Queue:
254
266
 
255
267
  Returns:
256
268
  True if update was successful, False if item was in unexpected state
269
+
257
270
  """
258
271
  with self._lock:
259
272
  with sqlite3.connect(self.db_path) as conn:
@@ -314,7 +327,9 @@ class Queue:
314
327
  conn.rollback()
315
328
  raise
316
329
 
317
- def increment_retry(self, queue_id: str, expected_status: Optional[QueueStatus] = None) -> int:
330
+ def increment_retry(
331
+ self, queue_id: str, expected_status: QueueStatus | None = None
332
+ ) -> int:
318
333
  """Increment retry count and reset to pending atomically.
319
334
 
320
335
  Args:
@@ -340,7 +355,11 @@ class Queue:
340
355
  WHERE id = ? AND status = ?
341
356
  RETURNING retry_count
342
357
  """,
343
- (QueueStatus.PENDING.value, queue_id, expected_status.value),
358
+ (
359
+ QueueStatus.PENDING.value,
360
+ queue_id,
361
+ expected_status.value,
362
+ ),
344
363
  )
345
364
  else:
346
365
  # Regular increment
@@ -368,7 +387,7 @@ class Queue:
368
387
  conn.rollback()
369
388
  raise
370
389
 
371
- def get_item(self, queue_id: str) -> Optional[QueueItem]:
390
+ def get_item(self, queue_id: str) -> QueueItem | None:
372
391
  """Get specific queue item by ID.
373
392
 
374
393
  Args:
@@ -390,7 +409,7 @@ class Queue:
390
409
  return QueueItem.from_row(row) if row else None
391
410
 
392
411
  def list_items(
393
- self, status: Optional[QueueStatus] = None, limit: int = 50
412
+ self, status: QueueStatus | None = None, limit: int = 50
394
413
  ) -> list[QueueItem]:
395
414
  """List queue items.
396
415
 
@@ -443,7 +462,7 @@ class Queue:
443
462
 
444
463
  return cursor.fetchone()[0]
445
464
 
446
- def cleanup_old(self, days: int = 7):
465
+ def cleanup_old(self, days: int = 7) -> None:
447
466
  """Clean up old completed/failed items.
448
467
 
449
468
  Args:
@@ -468,7 +487,7 @@ class Queue:
468
487
  )
469
488
  conn.commit()
470
489
 
471
- def reset_stuck_items(self, timeout_minutes: int = 30):
490
+ def reset_stuck_items(self, timeout_minutes: int = 30) -> None:
472
491
  """Reset items stuck in processing state.
473
492
 
474
493
  Args:
@@ -13,9 +13,14 @@ 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
+ import os
19
+
18
20
  logger.info("Starting standalone worker process")
21
+ logger.info(f"Worker Python executable: {sys.executable}")
22
+ logger.info(f"Worker working directory: {os.getcwd()}")
23
+ logger.info(f"Worker Python path: {sys.path[:3]}...") # Show first 3 entries
19
24
 
20
25
  try:
21
26
  # Create queue and worker