mcp-ticketer 0.4.11__py3-none-any.whl → 0.12.0__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 (70) hide show
  1. mcp_ticketer/__version__.py +3 -3
  2. mcp_ticketer/adapters/__init__.py +2 -0
  3. mcp_ticketer/adapters/aitrackdown.py +9 -3
  4. mcp_ticketer/adapters/asana/__init__.py +15 -0
  5. mcp_ticketer/adapters/asana/adapter.py +1308 -0
  6. mcp_ticketer/adapters/asana/client.py +292 -0
  7. mcp_ticketer/adapters/asana/mappers.py +334 -0
  8. mcp_ticketer/adapters/asana/types.py +146 -0
  9. mcp_ticketer/adapters/github.py +313 -96
  10. mcp_ticketer/adapters/jira.py +251 -1
  11. mcp_ticketer/adapters/linear/adapter.py +524 -22
  12. mcp_ticketer/adapters/linear/client.py +61 -9
  13. mcp_ticketer/adapters/linear/mappers.py +9 -3
  14. mcp_ticketer/cache/memory.py +3 -3
  15. mcp_ticketer/cli/adapter_diagnostics.py +1 -1
  16. mcp_ticketer/cli/auggie_configure.py +1 -1
  17. mcp_ticketer/cli/codex_configure.py +80 -1
  18. mcp_ticketer/cli/configure.py +33 -43
  19. mcp_ticketer/cli/diagnostics.py +18 -16
  20. mcp_ticketer/cli/discover.py +288 -21
  21. mcp_ticketer/cli/gemini_configure.py +1 -1
  22. mcp_ticketer/cli/instruction_commands.py +429 -0
  23. mcp_ticketer/cli/linear_commands.py +99 -15
  24. mcp_ticketer/cli/main.py +1199 -227
  25. mcp_ticketer/cli/mcp_configure.py +1 -1
  26. mcp_ticketer/cli/migrate_config.py +12 -8
  27. mcp_ticketer/cli/platform_commands.py +6 -6
  28. mcp_ticketer/cli/platform_detection.py +412 -0
  29. mcp_ticketer/cli/queue_commands.py +15 -15
  30. mcp_ticketer/cli/simple_health.py +1 -1
  31. mcp_ticketer/cli/ticket_commands.py +14 -13
  32. mcp_ticketer/cli/update_checker.py +313 -0
  33. mcp_ticketer/cli/utils.py +45 -41
  34. mcp_ticketer/core/__init__.py +12 -0
  35. mcp_ticketer/core/adapter.py +4 -4
  36. mcp_ticketer/core/config.py +17 -10
  37. mcp_ticketer/core/env_discovery.py +33 -3
  38. mcp_ticketer/core/env_loader.py +7 -6
  39. mcp_ticketer/core/exceptions.py +3 -3
  40. mcp_ticketer/core/http_client.py +10 -10
  41. mcp_ticketer/core/instructions.py +405 -0
  42. mcp_ticketer/core/mappers.py +1 -1
  43. mcp_ticketer/core/models.py +1 -1
  44. mcp_ticketer/core/onepassword_secrets.py +379 -0
  45. mcp_ticketer/core/project_config.py +17 -1
  46. mcp_ticketer/core/registry.py +1 -1
  47. mcp_ticketer/defaults/ticket_instructions.md +644 -0
  48. mcp_ticketer/mcp/__init__.py +2 -2
  49. mcp_ticketer/mcp/server/__init__.py +2 -2
  50. mcp_ticketer/mcp/server/main.py +82 -69
  51. mcp_ticketer/mcp/server/tools/__init__.py +9 -0
  52. mcp_ticketer/mcp/server/tools/attachment_tools.py +63 -16
  53. mcp_ticketer/mcp/server/tools/config_tools.py +381 -0
  54. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +154 -5
  55. mcp_ticketer/mcp/server/tools/instruction_tools.py +293 -0
  56. mcp_ticketer/mcp/server/tools/ticket_tools.py +157 -4
  57. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +382 -0
  58. mcp_ticketer/queue/health_monitor.py +1 -0
  59. mcp_ticketer/queue/manager.py +4 -4
  60. mcp_ticketer/queue/queue.py +3 -3
  61. mcp_ticketer/queue/run_worker.py +1 -1
  62. mcp_ticketer/queue/ticket_registry.py +2 -2
  63. mcp_ticketer/queue/worker.py +14 -12
  64. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-0.12.0.dist-info}/METADATA +106 -52
  65. mcp_ticketer-0.12.0.dist-info/RECORD +91 -0
  66. mcp_ticketer-0.4.11.dist-info/RECORD +0 -77
  67. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-0.12.0.dist-info}/WHEEL +0 -0
  68. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-0.12.0.dist-info}/entry_points.txt +0 -0
  69. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-0.12.0.dist-info}/licenses/LICENSE +0 -0
  70. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-0.12.0.dist-info}/top_level.txt +0 -0
@@ -230,7 +230,7 @@ class BaseAdapter(ABC, Generic[T]):
230
230
  # Epic/Issue/Task Hierarchy Methods
231
231
 
232
232
  async def create_epic(
233
- self, title: str, description: str | None = None, **kwargs
233
+ self, title: str, description: str | None = None, **kwargs: Any
234
234
  ) -> Epic | None:
235
235
  """Create epic (top-level grouping).
236
236
 
@@ -270,7 +270,7 @@ class BaseAdapter(ABC, Generic[T]):
270
270
  return result
271
271
  return None
272
272
 
273
- async def list_epics(self, **kwargs) -> builtins.list[Epic]:
273
+ async def list_epics(self, **kwargs: Any) -> builtins.list[Epic]:
274
274
  """List all epics.
275
275
 
276
276
  Args:
@@ -291,7 +291,7 @@ class BaseAdapter(ABC, Generic[T]):
291
291
  title: str,
292
292
  description: str | None = None,
293
293
  epic_id: str | None = None,
294
- **kwargs,
294
+ **kwargs: Any,
295
295
  ) -> Task | None:
296
296
  """Create issue, optionally linked to epic.
297
297
 
@@ -330,7 +330,7 @@ class BaseAdapter(ABC, Generic[T]):
330
330
  return [r for r in results if isinstance(r, Task) and r.is_issue()]
331
331
 
332
332
  async def create_task(
333
- self, title: str, parent_id: str, description: str | None = None, **kwargs
333
+ self, title: str, parent_id: str, description: str | None = None, **kwargs: Any
334
334
  ) -> Task | None:
335
335
  """Create task as sub-ticket of parent issue.
336
336
 
@@ -47,7 +47,8 @@ class GitHubConfig(BaseAdapterConfig):
47
47
 
48
48
  @field_validator("token", mode="before")
49
49
  @classmethod
50
- def validate_token(cls, v):
50
+ def validate_token(cls, v: Any) -> str:
51
+ """Validate GitHub token from config or environment."""
51
52
  if not v:
52
53
  v = os.getenv("GITHUB_TOKEN")
53
54
  if not v:
@@ -56,7 +57,8 @@ class GitHubConfig(BaseAdapterConfig):
56
57
 
57
58
  @field_validator("owner", mode="before")
58
59
  @classmethod
59
- def validate_owner(cls, v):
60
+ def validate_owner(cls, v: Any) -> str:
61
+ """Validate GitHub repository owner from config or environment."""
60
62
  if not v:
61
63
  v = os.getenv("GITHUB_OWNER")
62
64
  if not v:
@@ -65,7 +67,8 @@ class GitHubConfig(BaseAdapterConfig):
65
67
 
66
68
  @field_validator("repo", mode="before")
67
69
  @classmethod
68
- def validate_repo(cls, v):
70
+ def validate_repo(cls, v: Any) -> str:
71
+ """Validate GitHub repository name from config or environment."""
69
72
  if not v:
70
73
  v = os.getenv("GITHUB_REPO")
71
74
  if not v:
@@ -86,7 +89,8 @@ class JiraConfig(BaseAdapterConfig):
86
89
 
87
90
  @field_validator("server", mode="before")
88
91
  @classmethod
89
- def validate_server(cls, v):
92
+ def validate_server(cls, v: Any) -> str:
93
+ """Validate JIRA server URL from config or environment."""
90
94
  if not v:
91
95
  v = os.getenv("JIRA_SERVER")
92
96
  if not v:
@@ -95,7 +99,8 @@ class JiraConfig(BaseAdapterConfig):
95
99
 
96
100
  @field_validator("email", mode="before")
97
101
  @classmethod
98
- def validate_email(cls, v):
102
+ def validate_email(cls, v: Any) -> str:
103
+ """Validate JIRA user email from config or environment."""
99
104
  if not v:
100
105
  v = os.getenv("JIRA_EMAIL")
101
106
  if not v:
@@ -104,7 +109,8 @@ class JiraConfig(BaseAdapterConfig):
104
109
 
105
110
  @field_validator("api_token", mode="before")
106
111
  @classmethod
107
- def validate_api_token(cls, v):
112
+ def validate_api_token(cls, v: Any) -> str:
113
+ """Validate JIRA API token from config or environment."""
108
114
  if not v:
109
115
  v = os.getenv("JIRA_API_TOKEN")
110
116
  if not v:
@@ -123,7 +129,7 @@ class LinearConfig(BaseAdapterConfig):
123
129
  api_url: str = "https://api.linear.app/graphql"
124
130
 
125
131
  @model_validator(mode="after")
126
- def validate_team_identifier(self):
132
+ def validate_team_identifier(self) -> "LinearConfig":
127
133
  """Ensure either team_key or team_id is provided."""
128
134
  if not self.team_key and not self.team_id:
129
135
  raise ValueError("Either team_key or team_id is required")
@@ -131,7 +137,8 @@ class LinearConfig(BaseAdapterConfig):
131
137
 
132
138
  @field_validator("api_key", mode="before")
133
139
  @classmethod
134
- def validate_api_key(cls, v):
140
+ def validate_api_key(cls, v: Any) -> str:
141
+ """Validate Linear API key from config or environment."""
135
142
  if not v:
136
143
  v = os.getenv("LINEAR_API_KEY")
137
144
  if not v:
@@ -179,7 +186,7 @@ class AppConfig(BaseModel):
179
186
  default_adapter: str | None = None
180
187
 
181
188
  @model_validator(mode="after")
182
- def validate_adapters(self):
189
+ def validate_adapters(self) -> "AppConfig":
183
190
  """Validate adapter configurations."""
184
191
  adapters = self.adapters
185
192
 
@@ -220,7 +227,7 @@ class ConfigurationManager:
220
227
  cls._instance = super().__new__(cls)
221
228
  return cls._instance
222
229
 
223
- def __init__(self):
230
+ def __init__(self) -> None:
224
231
  """Initialize configuration manager."""
225
232
  if not hasattr(self, "_initialized"):
226
233
  self._initialized = True
@@ -6,6 +6,7 @@ environment files, including:
6
6
  - Support for multiple naming conventions
7
7
  - Project information extraction
8
8
  - Security validation
9
+ - 1Password CLI integration for secret references
9
10
  """
10
11
 
11
12
  import logging
@@ -15,6 +16,7 @@ from typing import Any
15
16
 
16
17
  from dotenv import dotenv_values
17
18
 
19
+ from .onepassword_secrets import OnePasswordConfig, OnePasswordSecretsLoader
18
20
  from .project_config import AdapterType
19
21
 
20
22
  logger = logging.getLogger(__name__)
@@ -155,14 +157,27 @@ class EnvDiscovery:
155
157
  ".env.development",
156
158
  ]
157
159
 
158
- def __init__(self, project_path: Path | None = None):
160
+ def __init__(
161
+ self,
162
+ project_path: Path | None = None,
163
+ enable_1password: bool = True,
164
+ onepassword_config: OnePasswordConfig | None = None,
165
+ ):
159
166
  """Initialize discovery.
160
167
 
161
168
  Args:
162
169
  project_path: Path to project root (defaults to cwd)
170
+ enable_1password: Enable 1Password CLI integration for secret resolution
171
+ onepassword_config: Configuration for 1Password integration
163
172
 
164
173
  """
165
174
  self.project_path = project_path or Path.cwd()
175
+ self.enable_1password = enable_1password
176
+ self.op_loader = (
177
+ OnePasswordSecretsLoader(onepassword_config or OnePasswordConfig())
178
+ if enable_1password
179
+ else None
180
+ )
166
181
 
167
182
  def discover(self) -> DiscoveryResult:
168
183
  """Discover adapter configurations from environment files.
@@ -241,7 +256,22 @@ class EnvDiscovery:
241
256
  file_path = self.project_path / env_file
242
257
  if file_path.exists():
243
258
  try:
244
- env_vars = dotenv_values(file_path)
259
+ # Check if file contains 1Password references and use op loader if available
260
+ if self.op_loader and self.enable_1password:
261
+ content = file_path.read_text(encoding="utf-8")
262
+ if "op://" in content:
263
+ logger.info(
264
+ f"Detected 1Password references in {env_file}, "
265
+ "attempting to resolve..."
266
+ )
267
+ env_vars = self.op_loader.load_secrets_from_env_file(
268
+ file_path
269
+ )
270
+ else:
271
+ env_vars = dotenv_values(file_path)
272
+ else:
273
+ env_vars = dotenv_values(file_path)
274
+
245
275
  # Filter out None values
246
276
  env_vars = {k: v for k, v in env_vars.items() if v is not None}
247
277
  merged_env.update(env_vars)
@@ -623,7 +653,7 @@ class EnvDiscovery:
623
653
 
624
654
 
625
655
  def discover_config(project_path: Path | None = None) -> DiscoveryResult:
626
- """Convenience function to discover configuration.
656
+ """Discover configuration from environment files.
627
657
 
628
658
  Args:
629
659
  project_path: Path to project root (defaults to cwd)
@@ -29,8 +29,9 @@ class EnvKeyConfig:
29
29
 
30
30
 
31
31
  class UnifiedEnvLoader:
32
- """Unified environment loader that handles multiple naming conventions
33
- and provides consistent environment loading across all contexts.
32
+ """Unified environment loader that handles multiple naming conventions.
33
+
34
+ Provides consistent environment loading across all contexts.
34
35
  """
35
36
 
36
37
  # Define key aliases for all adapters
@@ -131,7 +132,7 @@ class UnifiedEnvLoader:
131
132
  # Fallback to current directory
132
133
  return Path.cwd()
133
134
 
134
- def _load_env_files(self):
135
+ def _load_env_files(self) -> None:
135
136
  """Load environment variables from .env files."""
136
137
  env_files = [
137
138
  self.project_root / ".env.local",
@@ -144,7 +145,7 @@ class UnifiedEnvLoader:
144
145
  logger.debug(f"Loading environment from: {env_file}")
145
146
  self._load_env_file(env_file)
146
147
 
147
- def _load_env_file(self, env_file: Path):
148
+ def _load_env_file(self, env_file: Path) -> None:
148
149
  """Load variables from a single .env file."""
149
150
  try:
150
151
  with open(env_file) as f:
@@ -321,7 +322,7 @@ def get_env_loader() -> UnifiedEnvLoader:
321
322
  def load_adapter_config(
322
323
  adapter_name: str, base_config: dict[str, Any] | None = None
323
324
  ) -> dict[str, Any]:
324
- """Convenience function to load adapter configuration with environment variables.
325
+ """Load adapter configuration with environment variables.
325
326
 
326
327
  Args:
327
328
  adapter_name: Name of the adapter ('linear', 'jira', 'github')
@@ -335,7 +336,7 @@ def load_adapter_config(
335
336
 
336
337
 
337
338
  def validate_adapter_config(adapter_name: str, config: dict[str, Any]) -> list[str]:
338
- """Convenience function to validate adapter configuration.
339
+ """Validate adapter configuration.
339
340
 
340
341
  Args:
341
342
  adapter_name: Name of the adapter
@@ -35,7 +35,7 @@ class AdapterError(MCPTicketerError):
35
35
  self.original_error = original_error
36
36
 
37
37
  def __str__(self) -> str:
38
- """String representation of the error."""
38
+ """Return string representation of the error."""
39
39
  base_msg = f"[{self.adapter_name}] {super().__str__()}"
40
40
  if self.original_error:
41
41
  base_msg += f" (caused by: {self.original_error})"
@@ -88,7 +88,7 @@ class ValidationError(MCPTicketerError):
88
88
  self.value = value
89
89
 
90
90
  def __str__(self) -> str:
91
- """String representation of the error."""
91
+ """Return string representation of the error."""
92
92
  base_msg = super().__str__()
93
93
  if self.field:
94
94
  base_msg += f" (field: {self.field})"
@@ -126,7 +126,7 @@ class StateTransitionError(MCPTicketerError):
126
126
  self.to_state = to_state
127
127
 
128
128
  def __str__(self) -> str:
129
- """String representation of the error."""
129
+ """Return string representation of the error."""
130
130
  return f"{super().__str__()} ({self.from_state} -> {self.to_state})"
131
131
 
132
132
 
@@ -206,7 +206,7 @@ class BaseHTTPClient:
206
206
  headers: dict[str, str] | None = None,
207
207
  timeout: float | None = None,
208
208
  retry_count: int = 0,
209
- **kwargs,
209
+ **kwargs: Any,
210
210
  ) -> httpx.Response:
211
211
  """Make HTTP request with retry and rate limiting.
212
212
 
@@ -293,27 +293,27 @@ class BaseHTTPClient:
293
293
  # No more retries, re-raise the exception
294
294
  raise
295
295
 
296
- async def get(self, endpoint: str, **kwargs) -> httpx.Response:
296
+ async def get(self, endpoint: str, **kwargs: Any) -> httpx.Response:
297
297
  """Make GET request."""
298
298
  return await self.request(HTTPMethod.GET, endpoint, **kwargs)
299
299
 
300
- async def post(self, endpoint: str, **kwargs) -> httpx.Response:
300
+ async def post(self, endpoint: str, **kwargs: Any) -> httpx.Response:
301
301
  """Make POST request."""
302
302
  return await self.request(HTTPMethod.POST, endpoint, **kwargs)
303
303
 
304
- async def put(self, endpoint: str, **kwargs) -> httpx.Response:
304
+ async def put(self, endpoint: str, **kwargs: Any) -> httpx.Response:
305
305
  """Make PUT request."""
306
306
  return await self.request(HTTPMethod.PUT, endpoint, **kwargs)
307
307
 
308
- async def patch(self, endpoint: str, **kwargs) -> httpx.Response:
308
+ async def patch(self, endpoint: str, **kwargs: Any) -> httpx.Response:
309
309
  """Make PATCH request."""
310
310
  return await self.request(HTTPMethod.PATCH, endpoint, **kwargs)
311
311
 
312
- async def delete(self, endpoint: str, **kwargs) -> httpx.Response:
312
+ async def delete(self, endpoint: str, **kwargs: Any) -> httpx.Response:
313
313
  """Make DELETE request."""
314
314
  return await self.request(HTTPMethod.DELETE, endpoint, **kwargs)
315
315
 
316
- async def get_json(self, endpoint: str, **kwargs) -> dict[str, Any]:
316
+ async def get_json(self, endpoint: str, **kwargs: Any) -> dict[str, Any]:
317
317
  """Make GET request and return JSON response."""
318
318
  response = await self.get(endpoint, **kwargs)
319
319
 
@@ -323,7 +323,7 @@ class BaseHTTPClient:
323
323
 
324
324
  return response.json()
325
325
 
326
- async def post_json(self, endpoint: str, **kwargs) -> dict[str, Any]:
326
+ async def post_json(self, endpoint: str, **kwargs: Any) -> dict[str, Any]:
327
327
  """Make POST request and return JSON response."""
328
328
  response = await self.post(endpoint, **kwargs)
329
329
 
@@ -333,7 +333,7 @@ class BaseHTTPClient:
333
333
 
334
334
  return response.json()
335
335
 
336
- async def put_json(self, endpoint: str, **kwargs) -> dict[str, Any]:
336
+ async def put_json(self, endpoint: str, **kwargs: Any) -> dict[str, Any]:
337
337
  """Make PUT request and return JSON response."""
338
338
  response = await self.put(endpoint, **kwargs)
339
339
 
@@ -343,7 +343,7 @@ class BaseHTTPClient:
343
343
 
344
344
  return response.json()
345
345
 
346
- async def patch_json(self, endpoint: str, **kwargs) -> dict[str, Any]:
346
+ async def patch_json(self, endpoint: str, **kwargs: Any) -> dict[str, Any]:
347
347
  """Make PATCH request and return JSON response."""
348
348
  response = await self.patch(endpoint, **kwargs)
349
349