mcp-ticketer 0.4.2__py3-none-any.whl → 0.4.3__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 (54) hide show
  1. mcp_ticketer/__init__.py +3 -12
  2. mcp_ticketer/__version__.py +1 -1
  3. mcp_ticketer/adapters/aitrackdown.py +243 -11
  4. mcp_ticketer/adapters/github.py +15 -14
  5. mcp_ticketer/adapters/hybrid.py +11 -11
  6. mcp_ticketer/adapters/jira.py +22 -25
  7. mcp_ticketer/adapters/linear/adapter.py +9 -21
  8. mcp_ticketer/adapters/linear/client.py +2 -1
  9. mcp_ticketer/adapters/linear/mappers.py +2 -1
  10. mcp_ticketer/cache/memory.py +6 -5
  11. mcp_ticketer/cli/adapter_diagnostics.py +4 -2
  12. mcp_ticketer/cli/codex_configure.py +2 -2
  13. mcp_ticketer/cli/configure.py +7 -14
  14. mcp_ticketer/cli/diagnostics.py +2 -2
  15. mcp_ticketer/cli/discover.py +6 -11
  16. mcp_ticketer/cli/gemini_configure.py +2 -2
  17. mcp_ticketer/cli/linear_commands.py +6 -7
  18. mcp_ticketer/cli/main.py +218 -242
  19. mcp_ticketer/cli/mcp_configure.py +1 -2
  20. mcp_ticketer/cli/ticket_commands.py +27 -30
  21. mcp_ticketer/cli/utils.py +23 -22
  22. mcp_ticketer/core/__init__.py +3 -1
  23. mcp_ticketer/core/adapter.py +82 -13
  24. mcp_ticketer/core/config.py +27 -29
  25. mcp_ticketer/core/env_discovery.py +10 -10
  26. mcp_ticketer/core/env_loader.py +8 -8
  27. mcp_ticketer/core/http_client.py +16 -16
  28. mcp_ticketer/core/mappers.py +10 -10
  29. mcp_ticketer/core/models.py +50 -20
  30. mcp_ticketer/core/project_config.py +40 -34
  31. mcp_ticketer/core/registry.py +2 -2
  32. mcp_ticketer/mcp/dto.py +32 -32
  33. mcp_ticketer/mcp/response_builder.py +2 -2
  34. mcp_ticketer/mcp/server.py +17 -37
  35. mcp_ticketer/mcp/server_sdk.py +2 -2
  36. mcp_ticketer/mcp/tools/__init__.py +7 -9
  37. mcp_ticketer/mcp/tools/attachment_tools.py +3 -4
  38. mcp_ticketer/mcp/tools/comment_tools.py +2 -2
  39. mcp_ticketer/mcp/tools/hierarchy_tools.py +8 -8
  40. mcp_ticketer/mcp/tools/pr_tools.py +2 -2
  41. mcp_ticketer/mcp/tools/search_tools.py +6 -6
  42. mcp_ticketer/mcp/tools/ticket_tools.py +12 -12
  43. mcp_ticketer/queue/health_monitor.py +4 -4
  44. mcp_ticketer/queue/manager.py +2 -2
  45. mcp_ticketer/queue/queue.py +16 -16
  46. mcp_ticketer/queue/ticket_registry.py +7 -7
  47. mcp_ticketer/queue/worker.py +2 -2
  48. {mcp_ticketer-0.4.2.dist-info → mcp_ticketer-0.4.3.dist-info}/METADATA +61 -2
  49. mcp_ticketer-0.4.3.dist-info/RECORD +73 -0
  50. mcp_ticketer-0.4.2.dist-info/RECORD +0 -73
  51. {mcp_ticketer-0.4.2.dist-info → mcp_ticketer-0.4.3.dist-info}/WHEEL +0 -0
  52. {mcp_ticketer-0.4.2.dist-info → mcp_ticketer-0.4.3.dist-info}/entry_points.txt +0 -0
  53. {mcp_ticketer-0.4.2.dist-info → mcp_ticketer-0.4.3.dist-info}/licenses/LICENSE +0 -0
  54. {mcp_ticketer-0.4.2.dist-info → mcp_ticketer-0.4.3.dist-info}/top_level.txt +0 -0
@@ -11,7 +11,7 @@ environment files, including:
11
11
  import logging
12
12
  from dataclasses import dataclass, field
13
13
  from pathlib import Path
14
- from typing import Any, Optional
14
+ from typing import Any
15
15
 
16
16
  from dotenv import dotenv_values
17
17
 
@@ -125,7 +125,7 @@ class DiscoveryResult:
125
125
  warnings: list[str] = field(default_factory=list)
126
126
  env_files_found: list[str] = field(default_factory=list)
127
127
 
128
- def get_primary_adapter(self) -> Optional[DiscoveredAdapter]:
128
+ def get_primary_adapter(self) -> DiscoveredAdapter | None:
129
129
  """Get the adapter with highest confidence and completeness."""
130
130
  if not self.adapters:
131
131
  return None
@@ -136,7 +136,7 @@ class DiscoveryResult:
136
136
  )
137
137
  return sorted_adapters[0]
138
138
 
139
- def get_adapter_by_type(self, adapter_type: str) -> Optional[DiscoveredAdapter]:
139
+ def get_adapter_by_type(self, adapter_type: str) -> DiscoveredAdapter | None:
140
140
  """Get discovered adapter by type."""
141
141
  for adapter in self.adapters:
142
142
  if adapter.adapter_type == adapter_type:
@@ -155,7 +155,7 @@ class EnvDiscovery:
155
155
  ".env.development",
156
156
  ]
157
157
 
158
- def __init__(self, project_path: Optional[Path] = None):
158
+ def __init__(self, project_path: Path | None = None):
159
159
  """Initialize discovery.
160
160
 
161
161
  Args:
@@ -255,7 +255,7 @@ class EnvDiscovery:
255
255
 
256
256
  def _find_key_value(
257
257
  self, env_vars: dict[str, str], patterns: list[str]
258
- ) -> Optional[str]:
258
+ ) -> str | None:
259
259
  """Find first matching key value from patterns.
260
260
 
261
261
  Args:
@@ -273,7 +273,7 @@ class EnvDiscovery:
273
273
 
274
274
  def _detect_linear(
275
275
  self, env_vars: dict[str, str], found_in: str
276
- ) -> Optional[DiscoveredAdapter]:
276
+ ) -> DiscoveredAdapter | None:
277
277
  """Detect Linear adapter configuration.
278
278
 
279
279
  Args:
@@ -327,7 +327,7 @@ class EnvDiscovery:
327
327
 
328
328
  def _detect_github(
329
329
  self, env_vars: dict[str, str], found_in: str
330
- ) -> Optional[DiscoveredAdapter]:
330
+ ) -> DiscoveredAdapter | None:
331
331
  """Detect GitHub adapter configuration.
332
332
 
333
333
  Args:
@@ -386,7 +386,7 @@ class EnvDiscovery:
386
386
 
387
387
  def _detect_jira(
388
388
  self, env_vars: dict[str, str], found_in: str
389
- ) -> Optional[DiscoveredAdapter]:
389
+ ) -> DiscoveredAdapter | None:
390
390
  """Detect JIRA adapter configuration.
391
391
 
392
392
  Args:
@@ -442,7 +442,7 @@ class EnvDiscovery:
442
442
 
443
443
  def _detect_aitrackdown(
444
444
  self, env_vars: dict[str, str], found_in: str
445
- ) -> Optional[DiscoveredAdapter]:
445
+ ) -> DiscoveredAdapter | None:
446
446
  """Detect AITrackdown adapter configuration.
447
447
 
448
448
  Args:
@@ -622,7 +622,7 @@ class EnvDiscovery:
622
622
  return warnings
623
623
 
624
624
 
625
- def discover_config(project_path: Optional[Path] = None) -> DiscoveryResult:
625
+ def discover_config(project_path: Path | None = None) -> DiscoveryResult:
626
626
  """Convenience function to discover configuration.
627
627
 
628
628
  Args:
@@ -12,7 +12,7 @@ import logging
12
12
  import os
13
13
  from dataclasses import dataclass
14
14
  from pathlib import Path
15
- from typing import Any, Optional
15
+ from typing import Any
16
16
 
17
17
  logger = logging.getLogger(__name__)
18
18
 
@@ -25,7 +25,7 @@ class EnvKeyConfig:
25
25
  aliases: list[str]
26
26
  description: str
27
27
  required: bool = False
28
- default: Optional[str] = None
28
+ default: str | None = None
29
29
 
30
30
 
31
31
  class UnifiedEnvLoader:
@@ -105,7 +105,7 @@ class UnifiedEnvLoader:
105
105
  ),
106
106
  }
107
107
 
108
- def __init__(self, project_root: Optional[Path] = None):
108
+ def __init__(self, project_root: Path | None = None):
109
109
  """Initialize the environment loader.
110
110
 
111
111
  Args:
@@ -177,8 +177,8 @@ class UnifiedEnvLoader:
177
177
  logger.warning(f"Failed to load {env_file}: {e}")
178
178
 
179
179
  def get_value(
180
- self, config_key: str, config: Optional[dict[str, Any]] = None
181
- ) -> Optional[str]:
180
+ self, config_key: str, config: dict[str, Any] | None = None
181
+ ) -> str | None:
182
182
  """Get a configuration value using the key alias system.
183
183
 
184
184
  Args:
@@ -230,7 +230,7 @@ class UnifiedEnvLoader:
230
230
  return None
231
231
 
232
232
  def get_adapter_config(
233
- self, adapter_name: str, base_config: Optional[dict[str, Any]] = None
233
+ self, adapter_name: str, base_config: dict[str, Any] | None = None
234
234
  ) -> dict[str, Any]:
235
235
  """Get complete configuration for an adapter with environment variable resolution.
236
236
 
@@ -307,7 +307,7 @@ class UnifiedEnvLoader:
307
307
 
308
308
 
309
309
  # Global instance
310
- _env_loader: Optional[UnifiedEnvLoader] = None
310
+ _env_loader: UnifiedEnvLoader | None = None
311
311
 
312
312
 
313
313
  def get_env_loader() -> UnifiedEnvLoader:
@@ -319,7 +319,7 @@ def get_env_loader() -> UnifiedEnvLoader:
319
319
 
320
320
 
321
321
  def load_adapter_config(
322
- adapter_name: str, base_config: Optional[dict[str, Any]] = None
322
+ adapter_name: str, base_config: dict[str, Any] | None = None
323
323
  ) -> dict[str, Any]:
324
324
  """Convenience function to load adapter configuration with environment variables.
325
325
 
@@ -4,7 +4,7 @@ import asyncio
4
4
  import logging
5
5
  import time
6
6
  from enum import Enum
7
- from typing import Any, Optional, Union
7
+ from typing import Any
8
8
 
9
9
  import httpx
10
10
  from httpx import AsyncClient, TimeoutException
@@ -32,8 +32,8 @@ class RetryConfig:
32
32
  max_delay: float = 60.0,
33
33
  exponential_base: float = 2.0,
34
34
  jitter: bool = True,
35
- retry_on_status: Optional[list[int]] = None,
36
- retry_on_exceptions: Optional[list[type]] = None,
35
+ retry_on_status: list[int] | None = None,
36
+ retry_on_exceptions: list[type] | None = None,
37
37
  ):
38
38
  self.max_retries = max_retries
39
39
  self.initial_delay = initial_delay
@@ -94,11 +94,11 @@ class BaseHTTPClient:
94
94
  def __init__(
95
95
  self,
96
96
  base_url: str,
97
- headers: Optional[dict[str, str]] = None,
98
- auth: Optional[Union[httpx.Auth, tuple]] = None,
97
+ headers: dict[str, str] | None = None,
98
+ auth: httpx.Auth | tuple | None = None,
99
99
  timeout: float = 30.0,
100
- retry_config: Optional[RetryConfig] = None,
101
- rate_limiter: Optional[RateLimiter] = None,
100
+ retry_config: RetryConfig | None = None,
101
+ rate_limiter: RateLimiter | None = None,
102
102
  verify_ssl: bool = True,
103
103
  follow_redirects: bool = True,
104
104
  ):
@@ -132,7 +132,7 @@ class BaseHTTPClient:
132
132
  "errors": 0,
133
133
  }
134
134
 
135
- self._client: Optional[AsyncClient] = None
135
+ self._client: AsyncClient | None = None
136
136
 
137
137
  async def _get_client(self) -> AsyncClient:
138
138
  """Get or create HTTP client instance."""
@@ -148,7 +148,7 @@ class BaseHTTPClient:
148
148
  return self._client
149
149
 
150
150
  async def _calculate_delay(
151
- self, attempt: int, response: Optional[httpx.Response] = None
151
+ self, attempt: int, response: httpx.Response | None = None
152
152
  ) -> float:
153
153
  """Calculate delay for retry attempt."""
154
154
  if response and response.status_code == 429:
@@ -178,7 +178,7 @@ class BaseHTTPClient:
178
178
  def _should_retry(
179
179
  self,
180
180
  exception: Exception,
181
- response: Optional[httpx.Response] = None,
181
+ response: httpx.Response | None = None,
182
182
  attempt: int = 1,
183
183
  ) -> bool:
184
184
  """Determine if request should be retried."""
@@ -198,13 +198,13 @@ class BaseHTTPClient:
198
198
 
199
199
  async def request(
200
200
  self,
201
- method: Union[HTTPMethod, str],
201
+ method: HTTPMethod | str,
202
202
  endpoint: str,
203
- data: Optional[dict[str, Any]] = None,
204
- json: Optional[dict[str, Any]] = None,
205
- params: Optional[dict[str, Any]] = None,
206
- headers: Optional[dict[str, str]] = None,
207
- timeout: Optional[float] = None,
203
+ data: dict[str, Any] | None = None,
204
+ json: dict[str, Any] | None = None,
205
+ params: dict[str, Any] | None = None,
206
+ headers: dict[str, str] | None = None,
207
+ timeout: float | None = None,
208
208
  retry_count: int = 0,
209
209
  **kwargs,
210
210
  ) -> httpx.Response:
@@ -3,7 +3,7 @@
3
3
  import logging
4
4
  from abc import ABC, abstractmethod
5
5
  from functools import lru_cache
6
- from typing import Any, Generic, Optional, TypeVar
6
+ from typing import Any, Generic, TypeVar
7
7
 
8
8
  from .models import Priority, TicketState
9
9
 
@@ -27,11 +27,11 @@ class BiDirectionalDict(Generic[T, U]):
27
27
  self._reverse: dict[U, T] = {v: k for k, v in mapping.items()}
28
28
  self._cache: dict[str, Any] = {}
29
29
 
30
- def get_forward(self, key: T, default: Optional[U] = None) -> Optional[U]:
30
+ def get_forward(self, key: T, default: U | None = None) -> U | None:
31
31
  """Get value by forward key."""
32
32
  return self._forward.get(key, default)
33
33
 
34
- def get_reverse(self, key: U, default: Optional[T] = None) -> Optional[T]:
34
+ def get_reverse(self, key: U, default: T | None = None) -> T | None:
35
35
  """Get value by reverse key."""
36
36
  return self._reverse.get(key, default)
37
37
 
@@ -83,7 +83,7 @@ class StateMapper(BaseMapper):
83
83
  """Universal state mapping utility."""
84
84
 
85
85
  def __init__(
86
- self, adapter_type: str, custom_mappings: Optional[dict[str, Any]] = None
86
+ self, adapter_type: str, custom_mappings: dict[str, Any] | None = None
87
87
  ):
88
88
  """Initialize state mapper.
89
89
 
@@ -95,7 +95,7 @@ class StateMapper(BaseMapper):
95
95
  super().__init__()
96
96
  self.adapter_type = adapter_type
97
97
  self.custom_mappings = custom_mappings or {}
98
- self._mapping: Optional[BiDirectionalDict] = None
98
+ self._mapping: BiDirectionalDict | None = None
99
99
 
100
100
  @lru_cache(maxsize=1)
101
101
  def get_mapping(self) -> BiDirectionalDict:
@@ -229,7 +229,7 @@ class StateMapper(BaseMapper):
229
229
  """Check if adapter uses labels for extended states."""
230
230
  return self.adapter_type in ["github", "linear"]
231
231
 
232
- def get_state_label(self, state: TicketState) -> Optional[str]:
232
+ def get_state_label(self, state: TicketState) -> str | None:
233
233
  """Get label name for extended states that require labels.
234
234
 
235
235
  Args:
@@ -258,7 +258,7 @@ class PriorityMapper(BaseMapper):
258
258
  """Universal priority mapping utility."""
259
259
 
260
260
  def __init__(
261
- self, adapter_type: str, custom_mappings: Optional[dict[str, Any]] = None
261
+ self, adapter_type: str, custom_mappings: dict[str, Any] | None = None
262
262
  ):
263
263
  """Initialize priority mapper.
264
264
 
@@ -270,7 +270,7 @@ class PriorityMapper(BaseMapper):
270
270
  super().__init__()
271
271
  self.adapter_type = adapter_type
272
272
  self.custom_mappings = custom_mappings or {}
273
- self._mapping: Optional[BiDirectionalDict] = None
273
+ self._mapping: BiDirectionalDict | None = None
274
274
 
275
275
  @lru_cache(maxsize=1)
276
276
  def get_mapping(self) -> BiDirectionalDict:
@@ -483,7 +483,7 @@ class MapperRegistry:
483
483
 
484
484
  @classmethod
485
485
  def get_state_mapper(
486
- cls, adapter_type: str, custom_mappings: Optional[dict[str, Any]] = None
486
+ cls, adapter_type: str, custom_mappings: dict[str, Any] | None = None
487
487
  ) -> StateMapper:
488
488
  """Get or create state mapper for adapter type.
489
489
 
@@ -502,7 +502,7 @@ class MapperRegistry:
502
502
 
503
503
  @classmethod
504
504
  def get_priority_mapper(
505
- cls, adapter_type: str, custom_mappings: Optional[dict[str, Any]] = None
505
+ cls, adapter_type: str, custom_mappings: dict[str, Any] | None = None
506
506
  ) -> PriorityMapper:
507
507
  """Get or create priority mapper for adapter type.
508
508
 
@@ -25,7 +25,7 @@ Example:
25
25
 
26
26
  from datetime import datetime
27
27
  from enum import Enum
28
- from typing import Any, Optional
28
+ from typing import Any
29
29
 
30
30
  from pydantic import BaseModel, ConfigDict, Field
31
31
 
@@ -194,14 +194,14 @@ class BaseTicket(BaseModel):
194
194
 
195
195
  model_config = ConfigDict(use_enum_values=True)
196
196
 
197
- id: Optional[str] = Field(None, description="Unique identifier")
197
+ id: str | None = Field(None, description="Unique identifier")
198
198
  title: str = Field(..., min_length=1, description="Ticket title")
199
- description: Optional[str] = Field(None, description="Detailed description")
199
+ description: str | None = Field(None, description="Detailed description")
200
200
  state: TicketState = Field(TicketState.OPEN, description="Current state")
201
201
  priority: Priority = Field(Priority.MEDIUM, description="Priority level")
202
202
  tags: list[str] = Field(default_factory=list, description="Tags/labels")
203
- created_at: Optional[datetime] = Field(None, description="Creation timestamp")
204
- updated_at: Optional[datetime] = Field(None, description="Last update timestamp")
203
+ created_at: datetime | None = Field(None, description="Creation timestamp")
204
+ updated_at: datetime | None = Field(None, description="Last update timestamp")
205
205
 
206
206
  # Metadata for field mapping to different systems
207
207
  metadata: dict[str, Any] = Field(
@@ -270,20 +270,20 @@ class Task(BaseTicket):
270
270
  ticket_type: TicketType = Field(
271
271
  default=TicketType.ISSUE, description="Ticket type in hierarchy"
272
272
  )
273
- parent_issue: Optional[str] = Field(None, description="Parent issue ID (for tasks)")
274
- parent_epic: Optional[str] = Field(
273
+ parent_issue: str | None = Field(None, description="Parent issue ID (for tasks)")
274
+ parent_epic: str | None = Field(
275
275
  None,
276
276
  description="Parent epic/project ID (for issues). Synonym: 'project'",
277
277
  )
278
- assignee: Optional[str] = Field(None, description="Assigned user")
278
+ assignee: str | None = Field(None, description="Assigned user")
279
279
  children: list[str] = Field(default_factory=list, description="Child task IDs")
280
280
 
281
281
  # Additional fields common across systems
282
- estimated_hours: Optional[float] = Field(None, description="Time estimate")
283
- actual_hours: Optional[float] = Field(None, description="Actual time spent")
282
+ estimated_hours: float | None = Field(None, description="Time estimate")
283
+ actual_hours: float | None = Field(None, description="Actual time spent")
284
284
 
285
285
  @property
286
- def project(self) -> Optional[str]:
286
+ def project(self) -> str | None:
287
287
  """Synonym for parent_epic.
288
288
 
289
289
  Returns:
@@ -293,7 +293,7 @@ class Task(BaseTicket):
293
293
  return self.parent_epic
294
294
 
295
295
  @project.setter
296
- def project(self, value: Optional[str]) -> None:
296
+ def project(self, value: str | None) -> None:
297
297
  """Set parent_epic via project synonym.
298
298
 
299
299
  Args:
@@ -345,23 +345,53 @@ class Comment(BaseModel):
345
345
 
346
346
  model_config = ConfigDict(use_enum_values=True)
347
347
 
348
- id: Optional[str] = Field(None, description="Comment ID")
348
+ id: str | None = Field(None, description="Comment ID")
349
349
  ticket_id: str = Field(..., description="Parent ticket ID")
350
- author: Optional[str] = Field(None, description="Comment author")
350
+ author: str | None = Field(None, description="Comment author")
351
351
  content: str = Field(..., min_length=1, description="Comment text")
352
- created_at: Optional[datetime] = Field(None, description="Creation timestamp")
352
+ created_at: datetime | None = Field(None, description="Creation timestamp")
353
353
  metadata: dict[str, Any] = Field(
354
354
  default_factory=dict, description="System-specific metadata"
355
355
  )
356
356
 
357
357
 
358
+ class Attachment(BaseModel):
359
+ """File attachment metadata for tickets.
360
+
361
+ Represents a file attached to a ticket across all adapters.
362
+ Each adapter maps its native attachment format to this model.
363
+ """
364
+
365
+ model_config = ConfigDict(use_enum_values=True)
366
+
367
+ id: str | None = Field(None, description="Attachment unique identifier")
368
+ ticket_id: str = Field(..., description="Parent ticket identifier")
369
+ filename: str = Field(..., description="Original filename")
370
+ url: str | None = Field(None, description="Download URL or file path")
371
+ content_type: str | None = Field(
372
+ None, description="MIME type (e.g., 'application/pdf', 'image/png')"
373
+ )
374
+ size_bytes: int | None = Field(None, description="File size in bytes")
375
+ created_at: datetime | None = Field(None, description="Upload timestamp")
376
+ created_by: str | None = Field(None, description="User who uploaded the attachment")
377
+ description: str | None = Field(None, description="Attachment description or notes")
378
+ metadata: dict[str, Any] = Field(
379
+ default_factory=dict, description="Adapter-specific attachment metadata"
380
+ )
381
+
382
+ def __str__(self) -> str:
383
+ """String representation showing filename and size."""
384
+ size_str = f" ({self.size_bytes} bytes)" if self.size_bytes else ""
385
+ return f"Attachment({self.filename}{size_str})"
386
+
387
+
358
388
  class SearchQuery(BaseModel):
359
389
  """Search query parameters."""
360
390
 
361
- query: Optional[str] = Field(None, description="Text search query")
362
- state: Optional[TicketState] = Field(None, description="Filter by state")
363
- priority: Optional[Priority] = Field(None, description="Filter by priority")
364
- tags: Optional[list[str]] = Field(None, description="Filter by tags")
365
- assignee: Optional[str] = Field(None, description="Filter by assignee")
391
+ query: str | None = Field(None, description="Text search query")
392
+ state: TicketState | None = Field(None, description="Filter by state")
393
+ priority: Priority | None = Field(None, description="Filter by priority")
394
+ tags: list[str] | None = Field(None, description="Filter by tags")
395
+ assignee: str | None = Field(None, description="Filter by assignee")
366
396
  limit: int = Field(10, gt=0, le=100, description="Maximum results")
367
397
  offset: int = Field(0, ge=0, description="Result offset for pagination")
@@ -47,29 +47,29 @@ class AdapterConfig:
47
47
  enabled: bool = True
48
48
 
49
49
  # Common fields (not all adapters use all fields)
50
- api_key: Optional[str] = None
51
- token: Optional[str] = None
50
+ api_key: str | None = None
51
+ token: str | None = None
52
52
 
53
53
  # Linear-specific
54
- team_id: Optional[str] = None
55
- team_key: Optional[str] = None
56
- workspace: Optional[str] = None
54
+ team_id: str | None = None
55
+ team_key: str | None = None
56
+ workspace: str | None = None
57
57
 
58
58
  # JIRA-specific
59
- server: Optional[str] = None
60
- email: Optional[str] = None
61
- api_token: Optional[str] = None
62
- project_key: Optional[str] = None
59
+ server: str | None = None
60
+ email: str | None = None
61
+ api_token: str | None = None
62
+ project_key: str | None = None
63
63
 
64
64
  # GitHub-specific
65
- owner: Optional[str] = None
66
- repo: Optional[str] = None
65
+ owner: str | None = None
66
+ repo: str | None = None
67
67
 
68
68
  # AITrackdown-specific
69
- base_path: Optional[str] = None
69
+ base_path: str | None = None
70
70
 
71
71
  # Project ID (can be used by any adapter for scoping)
72
- project_id: Optional[str] = None
72
+ project_id: str | None = None
73
73
 
74
74
  # Additional adapter-specific configuration
75
75
  additional_config: dict[str, Any] = field(default_factory=dict)
@@ -126,9 +126,9 @@ class ProjectConfig:
126
126
  """Configuration for a specific project."""
127
127
 
128
128
  adapter: str
129
- api_key: Optional[str] = None
130
- project_id: Optional[str] = None
131
- team_id: Optional[str] = None
129
+ api_key: str | None = None
130
+ project_id: str | None = None
131
+ team_id: str | None = None
132
132
  additional_config: dict[str, Any] = field(default_factory=dict)
133
133
 
134
134
  def to_dict(self) -> dict[str, Any]:
@@ -147,7 +147,7 @@ class HybridConfig:
147
147
 
148
148
  enabled: bool = False
149
149
  adapters: list[str] = field(default_factory=list)
150
- primary_adapter: Optional[str] = None
150
+ primary_adapter: str | None = None
151
151
  sync_strategy: SyncStrategy = SyncStrategy.PRIMARY_SOURCE
152
152
 
153
153
  def to_dict(self) -> dict[str, Any]:
@@ -172,7 +172,7 @@ class TicketerConfig:
172
172
  default_adapter: str = "aitrackdown"
173
173
  project_configs: dict[str, ProjectConfig] = field(default_factory=dict)
174
174
  adapters: dict[str, AdapterConfig] = field(default_factory=dict)
175
- hybrid_mode: Optional[HybridConfig] = None
175
+ hybrid_mode: HybridConfig | None = None
176
176
 
177
177
  def to_dict(self) -> dict[str, Any]:
178
178
  """Convert to dictionary for JSON serialization."""
@@ -219,7 +219,7 @@ class ConfigValidator:
219
219
  """Validate adapter configurations."""
220
220
 
221
221
  @staticmethod
222
- def validate_linear_config(config: dict[str, Any]) -> tuple[bool, Optional[str]]:
222
+ def validate_linear_config(config: dict[str, Any]) -> tuple[bool, str | None]:
223
223
  """Validate Linear adapter configuration.
224
224
 
225
225
  Returns:
@@ -241,7 +241,7 @@ class ConfigValidator:
241
241
  return True, None
242
242
 
243
243
  @staticmethod
244
- def validate_github_config(config: dict[str, Any]) -> tuple[bool, Optional[str]]:
244
+ def validate_github_config(config: dict[str, Any]) -> tuple[bool, str | None]:
245
245
  """Validate GitHub adapter configuration.
246
246
 
247
247
  Returns:
@@ -270,7 +270,7 @@ class ConfigValidator:
270
270
  return True, None
271
271
 
272
272
  @staticmethod
273
- def validate_jira_config(config: dict[str, Any]) -> tuple[bool, Optional[str]]:
273
+ def validate_jira_config(config: dict[str, Any]) -> tuple[bool, str | None]:
274
274
  """Validate JIRA adapter configuration.
275
275
 
276
276
  Returns:
@@ -292,7 +292,7 @@ class ConfigValidator:
292
292
  @staticmethod
293
293
  def validate_aitrackdown_config(
294
294
  config: dict[str, Any],
295
- ) -> tuple[bool, Optional[str]]:
295
+ ) -> tuple[bool, str | None]:
296
296
  """Validate AITrackdown adapter configuration.
297
297
 
298
298
  Returns:
@@ -306,7 +306,7 @@ class ConfigValidator:
306
306
  @classmethod
307
307
  def validate(
308
308
  cls, adapter_type: str, config: dict[str, Any]
309
- ) -> tuple[bool, Optional[str]]:
309
+ ) -> tuple[bool, str | None]:
310
310
  """Validate configuration for any adapter type.
311
311
 
312
312
  Args:
@@ -350,7 +350,7 @@ class ConfigResolver:
350
350
  PROJECT_CONFIG_SUBPATH = ".mcp-ticketer" / Path("config.json")
351
351
 
352
352
  def __init__(
353
- self, project_path: Optional[Path] = None, enable_env_discovery: bool = True
353
+ self, project_path: Path | None = None, enable_env_discovery: bool = True
354
354
  ):
355
355
  """Initialize config resolver.
356
356
 
@@ -361,8 +361,8 @@ class ConfigResolver:
361
361
  """
362
362
  self.project_path = project_path or Path.cwd()
363
363
  self.enable_env_discovery = enable_env_discovery
364
- self._project_config: Optional[TicketerConfig] = None
365
- self._discovered_config: Optional[DiscoveryResult] = None
364
+ self._project_config: TicketerConfig | None = None
365
+ self._discovered_config: DiscoveryResult | None = None
366
366
 
367
367
  def load_global_config(self) -> TicketerConfig:
368
368
  """Load default configuration (global config loading removed for security).
@@ -382,8 +382,8 @@ class ConfigResolver:
382
382
  return default_config
383
383
 
384
384
  def load_project_config(
385
- self, project_path: Optional[Path] = None
386
- ) -> Optional[TicketerConfig]:
385
+ self, project_path: Path | None = None
386
+ ) -> TicketerConfig | None:
387
387
  """Load project-specific configuration.
388
388
 
389
389
  Args:
@@ -424,7 +424,7 @@ class ConfigResolver:
424
424
  self.save_project_config(config)
425
425
 
426
426
  def save_project_config(
427
- self, config: TicketerConfig, project_path: Optional[Path] = None
427
+ self, config: TicketerConfig, project_path: Path | None = None
428
428
  ) -> None:
429
429
  """Save project-specific configuration.
430
430
 
@@ -461,8 +461,8 @@ class ConfigResolver:
461
461
 
462
462
  def resolve_adapter_config(
463
463
  self,
464
- adapter_name: Optional[str] = None,
465
- cli_overrides: Optional[dict[str, Any]] = None,
464
+ adapter_name: str | None = None,
465
+ cli_overrides: dict[str, Any] | None = None,
466
466
  ) -> dict[str, Any]:
467
467
  """Resolve adapter configuration with hierarchical precedence.
468
468
 
@@ -582,6 +582,12 @@ class ConfigResolver:
582
582
  overrides["team_id"] = os.getenv("MCP_TICKETER_LINEAR_TEAM_ID")
583
583
  if os.getenv("LINEAR_API_KEY"):
584
584
  overrides["api_key"] = os.getenv("LINEAR_API_KEY")
585
+ if os.getenv("LINEAR_TEAM_ID"):
586
+ overrides["team_id"] = os.getenv("LINEAR_TEAM_ID")
587
+ if os.getenv("LINEAR_TEAM_KEY"):
588
+ overrides["team_key"] = os.getenv("LINEAR_TEAM_KEY")
589
+ if os.getenv("MCP_TICKETER_LINEAR_TEAM_KEY"):
590
+ overrides["team_key"] = os.getenv("MCP_TICKETER_LINEAR_TEAM_KEY")
585
591
 
586
592
  elif adapter_type == AdapterType.GITHUB.value:
587
593
  if os.getenv("MCP_TICKETER_GITHUB_TOKEN"):
@@ -623,7 +629,7 @@ class ConfigResolver:
623
629
 
624
630
  return overrides
625
631
 
626
- def get_hybrid_config(self) -> Optional[HybridConfig]:
632
+ def get_hybrid_config(self) -> HybridConfig | None:
627
633
  """Get hybrid mode configuration if enabled.
628
634
 
629
635
  Returns:
@@ -655,10 +661,10 @@ class ConfigResolver:
655
661
 
656
662
 
657
663
  # Singleton instance for global access
658
- _default_resolver: Optional[ConfigResolver] = None
664
+ _default_resolver: ConfigResolver | None = None
659
665
 
660
666
 
661
- def get_config_resolver(project_path: Optional[Path] = None) -> ConfigResolver:
667
+ def get_config_resolver(project_path: Path | None = None) -> ConfigResolver:
662
668
  """Get the global config resolver instance.
663
669
 
664
670
  Args:
@@ -1,6 +1,6 @@
1
1
  """Adapter registry for dynamic adapter management."""
2
2
 
3
- from typing import Any, Optional
3
+ from typing import Any
4
4
 
5
5
  from .adapter import BaseAdapter
6
6
 
@@ -37,7 +37,7 @@ class AdapterRegistry:
37
37
 
38
38
  @classmethod
39
39
  def get_adapter(
40
- cls, name: str, config: Optional[dict[str, Any]] = None, force_new: bool = False
40
+ cls, name: str, config: dict[str, Any] | None = None, force_new: bool = False
41
41
  ) -> BaseAdapter:
42
42
  """Get or create an adapter instance.
43
43