mcp-ticketer 0.4.1__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 (56) 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/auggie_configure.py +66 -0
  13. mcp_ticketer/cli/codex_configure.py +70 -2
  14. mcp_ticketer/cli/configure.py +7 -14
  15. mcp_ticketer/cli/diagnostics.py +2 -2
  16. mcp_ticketer/cli/discover.py +6 -11
  17. mcp_ticketer/cli/gemini_configure.py +68 -2
  18. mcp_ticketer/cli/linear_commands.py +6 -7
  19. mcp_ticketer/cli/main.py +341 -203
  20. mcp_ticketer/cli/mcp_configure.py +61 -2
  21. mcp_ticketer/cli/ticket_commands.py +27 -30
  22. mcp_ticketer/cli/utils.py +23 -22
  23. mcp_ticketer/core/__init__.py +3 -1
  24. mcp_ticketer/core/adapter.py +82 -13
  25. mcp_ticketer/core/config.py +27 -29
  26. mcp_ticketer/core/env_discovery.py +10 -10
  27. mcp_ticketer/core/env_loader.py +8 -8
  28. mcp_ticketer/core/http_client.py +16 -16
  29. mcp_ticketer/core/mappers.py +10 -10
  30. mcp_ticketer/core/models.py +50 -20
  31. mcp_ticketer/core/project_config.py +40 -34
  32. mcp_ticketer/core/registry.py +2 -2
  33. mcp_ticketer/mcp/dto.py +32 -32
  34. mcp_ticketer/mcp/response_builder.py +2 -2
  35. mcp_ticketer/mcp/server.py +17 -37
  36. mcp_ticketer/mcp/server_sdk.py +93 -0
  37. mcp_ticketer/mcp/tools/__init__.py +36 -0
  38. mcp_ticketer/mcp/tools/attachment_tools.py +179 -0
  39. mcp_ticketer/mcp/tools/bulk_tools.py +273 -0
  40. mcp_ticketer/mcp/tools/comment_tools.py +90 -0
  41. mcp_ticketer/mcp/tools/hierarchy_tools.py +383 -0
  42. mcp_ticketer/mcp/tools/pr_tools.py +154 -0
  43. mcp_ticketer/mcp/tools/search_tools.py +206 -0
  44. mcp_ticketer/mcp/tools/ticket_tools.py +277 -0
  45. mcp_ticketer/queue/health_monitor.py +4 -4
  46. mcp_ticketer/queue/manager.py +2 -2
  47. mcp_ticketer/queue/queue.py +16 -16
  48. mcp_ticketer/queue/ticket_registry.py +7 -7
  49. mcp_ticketer/queue/worker.py +2 -2
  50. {mcp_ticketer-0.4.1.dist-info → mcp_ticketer-0.4.3.dist-info}/METADATA +90 -17
  51. mcp_ticketer-0.4.3.dist-info/RECORD +73 -0
  52. mcp_ticketer-0.4.1.dist-info/RECORD +0 -64
  53. {mcp_ticketer-0.4.1.dist-info → mcp_ticketer-0.4.3.dist-info}/WHEEL +0 -0
  54. {mcp_ticketer-0.4.1.dist-info → mcp_ticketer-0.4.3.dist-info}/entry_points.txt +0 -0
  55. {mcp_ticketer-0.4.1.dist-info → mcp_ticketer-0.4.3.dist-info}/licenses/LICENSE +0 -0
  56. {mcp_ticketer-0.4.1.dist-info → mcp_ticketer-0.4.3.dist-info}/top_level.txt +0 -0
@@ -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
 
mcp_ticketer/mcp/dto.py CHANGED
@@ -1,6 +1,6 @@
1
1
  """Data Transfer Objects for MCP requests and responses."""
2
2
 
3
- from typing import Any, Optional
3
+ from typing import Any
4
4
 
5
5
  from pydantic import BaseModel, Field
6
6
 
@@ -10,32 +10,32 @@ class CreateTicketRequest(BaseModel):
10
10
  """Request to create a ticket."""
11
11
 
12
12
  title: str = Field(..., min_length=1, description="Ticket title")
13
- description: Optional[str] = Field(None, description="Ticket description")
13
+ description: str | None = Field(None, description="Ticket description")
14
14
  priority: str = Field("medium", description="Ticket priority")
15
15
  tags: list[str] = Field(default_factory=list, description="Ticket tags")
16
- assignee: Optional[str] = Field(None, description="Ticket assignee")
16
+ assignee: str | None = Field(None, description="Ticket assignee")
17
17
 
18
18
 
19
19
  class CreateEpicRequest(BaseModel):
20
20
  """Request to create an epic."""
21
21
 
22
22
  title: str = Field(..., min_length=1, description="Epic title")
23
- description: Optional[str] = Field(None, description="Epic description")
23
+ description: str | None = Field(None, description="Epic description")
24
24
  child_issues: list[str] = Field(default_factory=list, description="Child issue IDs")
25
- target_date: Optional[str] = Field(None, description="Target completion date")
26
- lead_id: Optional[str] = Field(None, description="Epic lead/owner ID")
25
+ target_date: str | None = Field(None, description="Target completion date")
26
+ lead_id: str | None = Field(None, description="Epic lead/owner ID")
27
27
 
28
28
 
29
29
  class CreateIssueRequest(BaseModel):
30
30
  """Request to create an issue."""
31
31
 
32
32
  title: str = Field(..., min_length=1, description="Issue title")
33
- description: Optional[str] = Field(None, description="Issue description")
34
- epic_id: Optional[str] = Field(None, description="Parent epic ID")
33
+ description: str | None = Field(None, description="Issue description")
34
+ epic_id: str | None = Field(None, description="Parent epic ID")
35
35
  priority: str = Field("medium", description="Issue priority")
36
- assignee: Optional[str] = Field(None, description="Issue assignee")
36
+ assignee: str | None = Field(None, description="Issue assignee")
37
37
  tags: list[str] = Field(default_factory=list, description="Issue tags")
38
- estimated_hours: Optional[float] = Field(
38
+ estimated_hours: float | None = Field(
39
39
  None, description="Estimated hours to complete"
40
40
  )
41
41
 
@@ -45,11 +45,11 @@ class CreateTaskRequest(BaseModel):
45
45
 
46
46
  title: str = Field(..., min_length=1, description="Task title")
47
47
  parent_id: str = Field(..., description="Parent issue ID")
48
- description: Optional[str] = Field(None, description="Task description")
48
+ description: str | None = Field(None, description="Task description")
49
49
  priority: str = Field("medium", description="Task priority")
50
- assignee: Optional[str] = Field(None, description="Task assignee")
50
+ assignee: str | None = Field(None, description="Task assignee")
51
51
  tags: list[str] = Field(default_factory=list, description="Task tags")
52
- estimated_hours: Optional[float] = Field(
52
+ estimated_hours: float | None = Field(
53
53
  None, description="Estimated hours to complete"
54
54
  )
55
55
 
@@ -77,11 +77,11 @@ class TransitionRequest(BaseModel):
77
77
  class SearchRequest(BaseModel):
78
78
  """Request to search tickets."""
79
79
 
80
- query: Optional[str] = Field(None, description="Search query text")
81
- state: Optional[str] = Field(None, description="Filter by ticket state")
82
- priority: Optional[str] = Field(None, description="Filter by priority")
83
- assignee: Optional[str] = Field(None, description="Filter by assignee")
84
- tags: Optional[list[str]] = Field(None, description="Filter by tags")
80
+ query: str | None = Field(None, description="Search query text")
81
+ state: str | None = Field(None, description="Filter by ticket state")
82
+ priority: str | None = Field(None, description="Filter by priority")
83
+ assignee: str | None = Field(None, description="Filter by assignee")
84
+ tags: list[str] | None = Field(None, description="Filter by tags")
85
85
  limit: int = Field(10, description="Maximum number of results")
86
86
 
87
87
 
@@ -90,7 +90,7 @@ class ListRequest(BaseModel):
90
90
 
91
91
  limit: int = Field(10, description="Maximum number of tickets to return")
92
92
  offset: int = Field(0, description="Number of tickets to skip")
93
- filters: Optional[dict[str, Any]] = Field(None, description="Additional filters")
93
+ filters: dict[str, Any] | None = Field(None, description="Additional filters")
94
94
 
95
95
 
96
96
  class DeleteTicketRequest(BaseModel):
@@ -104,8 +104,8 @@ class CommentRequest(BaseModel):
104
104
 
105
105
  operation: str = Field("add", description="Operation: 'add' or 'list'")
106
106
  ticket_id: str = Field(..., description="Ticket ID")
107
- content: Optional[str] = Field(None, description="Comment content (for add)")
108
- author: Optional[str] = Field(None, description="Comment author (for add)")
107
+ content: str | None = Field(None, description="Comment content (for add)")
108
+ author: str | None = Field(None, description="Comment author (for add)")
109
109
  limit: int = Field(10, description="Max comments to return (for list)")
110
110
  offset: int = Field(0, description="Number of comments to skip (for list)")
111
111
 
@@ -115,12 +115,12 @@ class CreatePRRequest(BaseModel):
115
115
 
116
116
  ticket_id: str = Field(..., description="Ticket ID")
117
117
  base_branch: str = Field("main", description="Base branch")
118
- head_branch: Optional[str] = Field(None, description="Head branch")
119
- title: Optional[str] = Field(None, description="PR title")
120
- body: Optional[str] = Field(None, description="PR body")
118
+ head_branch: str | None = Field(None, description="Head branch")
119
+ title: str | None = Field(None, description="PR title")
120
+ body: str | None = Field(None, description="PR body")
121
121
  draft: bool = Field(False, description="Create as draft PR")
122
- github_owner: Optional[str] = Field(None, description="GitHub owner (for Linear)")
123
- github_repo: Optional[str] = Field(None, description="GitHub repo (for Linear)")
122
+ github_owner: str | None = Field(None, description="GitHub owner (for Linear)")
123
+ github_repo: str | None = Field(None, description="GitHub repo (for Linear)")
124
124
 
125
125
 
126
126
  class LinkPRRequest(BaseModel):
@@ -152,7 +152,7 @@ class IssueTasksRequest(BaseModel):
152
152
  class HierarchyTreeRequest(BaseModel):
153
153
  """Request to get hierarchy tree."""
154
154
 
155
- epic_id: Optional[str] = Field(None, description="Specific epic ID (optional)")
155
+ epic_id: str | None = Field(None, description="Specific epic ID (optional)")
156
156
  max_depth: int = Field(3, description="Maximum depth of tree")
157
157
  limit: int = Field(10, description="Max epics to return (if no epic_id)")
158
158
 
@@ -173,8 +173,8 @@ class SearchHierarchyRequest(BaseModel):
173
173
  """Request to search with hierarchy context."""
174
174
 
175
175
  query: str = Field("", description="Search query")
176
- state: Optional[str] = Field(None, description="Filter by state")
177
- priority: Optional[str] = Field(None, description="Filter by priority")
176
+ state: str | None = Field(None, description="Filter by state")
177
+ priority: str | None = Field(None, description="Filter by priority")
178
178
  include_children: bool = Field(True, description="Include child tickets")
179
179
  include_parents: bool = Field(True, description="Include parent tickets")
180
180
  limit: int = Field(50, description="Maximum number of results")
@@ -184,9 +184,9 @@ class AttachRequest(BaseModel):
184
184
  """Request to attach file to ticket."""
185
185
 
186
186
  ticket_id: str = Field(..., description="Ticket ID")
187
- file_path: Optional[str] = Field(None, description="File path to attach")
188
- file_content: Optional[str] = Field(None, description="File content (base64)")
189
- file_name: Optional[str] = Field(None, description="File name")
187
+ file_path: str | None = Field(None, description="File path to attach")
188
+ file_content: str | None = Field(None, description="File content (base64)")
189
+ file_name: str | None = Field(None, description="File name")
190
190
 
191
191
 
192
192
  class ListAttachmentsRequest(BaseModel):
@@ -1,6 +1,6 @@
1
1
  """Response builder utility for consistent MCP responses."""
2
2
 
3
- from typing import Any, Optional
3
+ from typing import Any
4
4
 
5
5
  from .constants import JSONRPC_VERSION, STATUS_COMPLETED
6
6
 
@@ -36,7 +36,7 @@ class ResponseBuilder:
36
36
  request_id: Any,
37
37
  code: int,
38
38
  message: str,
39
- data: Optional[dict[str, Any]] = None,
39
+ data: dict[str, Any] | None = None,
40
40
  ) -> dict[str, Any]:
41
41
  """Build error response.
42
42
 
@@ -4,7 +4,7 @@ import asyncio
4
4
  import json
5
5
  import sys
6
6
  from pathlib import Path
7
- from typing import Any, Optional
7
+ from typing import Any
8
8
 
9
9
  from dotenv import load_dotenv
10
10
 
@@ -12,40 +12,20 @@ from dotenv import load_dotenv
12
12
  import mcp_ticketer.adapters # noqa: F401
13
13
 
14
14
  from ..core import AdapterRegistry
15
- from ..core.models import Comment, Epic, Priority, SearchQuery, Task, TicketState
16
- from .constants import (
17
- DEFAULT_BASE_PATH,
18
- DEFAULT_LIMIT,
19
- DEFAULT_MAX_DEPTH,
20
- DEFAULT_OFFSET,
21
- ERROR_INTERNAL,
22
- ERROR_METHOD_NOT_FOUND,
23
- ERROR_PARSE,
24
- JSONRPC_VERSION,
25
- MCP_PROTOCOL_VERSION,
26
- MSG_EPIC_NOT_FOUND,
27
- MSG_INTERNAL_ERROR,
28
- MSG_MISSING_TICKET_ID,
29
- MSG_MISSING_TITLE,
30
- MSG_NO_TICKETS_PROVIDED,
31
- MSG_NO_UPDATES_PROVIDED,
32
- MSG_TICKET_NOT_FOUND,
33
- MSG_TRANSITION_FAILED,
34
- MSG_UNKNOWN_METHOD,
35
- MSG_UNKNOWN_OPERATION,
36
- MSG_UPDATE_FAILED,
37
- SERVER_NAME,
38
- SERVER_VERSION,
39
- STATUS_COMPLETED,
40
- STATUS_ERROR,
41
- )
42
- from .dto import (
43
- CreateEpicRequest,
44
- CreateIssueRequest,
45
- CreateTaskRequest,
46
- CreateTicketRequest,
47
- ReadTicketRequest,
48
- )
15
+ from ..core.models import (Comment, Epic, Priority, SearchQuery, Task,
16
+ TicketState)
17
+ from .constants import (DEFAULT_BASE_PATH, DEFAULT_LIMIT, DEFAULT_MAX_DEPTH,
18
+ DEFAULT_OFFSET, ERROR_INTERNAL, ERROR_METHOD_NOT_FOUND,
19
+ ERROR_PARSE, JSONRPC_VERSION, MCP_PROTOCOL_VERSION,
20
+ MSG_EPIC_NOT_FOUND, MSG_INTERNAL_ERROR,
21
+ MSG_MISSING_TICKET_ID, MSG_MISSING_TITLE,
22
+ MSG_NO_TICKETS_PROVIDED, MSG_NO_UPDATES_PROVIDED,
23
+ MSG_TICKET_NOT_FOUND, MSG_TRANSITION_FAILED,
24
+ MSG_UNKNOWN_METHOD, MSG_UNKNOWN_OPERATION,
25
+ MSG_UPDATE_FAILED, SERVER_NAME, SERVER_VERSION,
26
+ STATUS_COMPLETED, STATUS_ERROR)
27
+ from .dto import (CreateEpicRequest, CreateIssueRequest, CreateTaskRequest,
28
+ CreateTicketRequest, ReadTicketRequest)
49
29
  from .response_builder import ResponseBuilder
50
30
 
51
31
  # Load environment variables early (prioritize .env.local)
@@ -70,7 +50,7 @@ class MCPTicketServer:
70
50
  """MCP server for ticket operations over stdio - synchronous implementation."""
71
51
 
72
52
  def __init__(
73
- self, adapter_type: str = "aitrackdown", config: Optional[dict[str, Any]] = None
53
+ self, adapter_type: str = "aitrackdown", config: dict[str, Any] | None = None
74
54
  ):
75
55
  """Initialize MCP server.
76
56
 
@@ -1129,7 +1109,7 @@ async def main():
1129
1109
  await server.run()
1130
1110
 
1131
1111
 
1132
- def _load_env_configuration() -> Optional[dict[str, Any]]:
1112
+ def _load_env_configuration() -> dict[str, Any] | None:
1133
1113
  """Load adapter configuration from .env files.
1134
1114
 
1135
1115
  Checks .env.local first (highest priority), then .env.
@@ -0,0 +1,93 @@
1
+ """FastMCP-based MCP server implementation.
2
+
3
+ This module implements the MCP server using the official FastMCP SDK,
4
+ replacing the custom JSON-RPC implementation. It provides a cleaner,
5
+ more maintainable approach with automatic schema generation and
6
+ better error handling.
7
+
8
+ The server manages a global adapter instance that is configured at
9
+ startup and used by all tool implementations.
10
+ """
11
+
12
+ import logging
13
+ from typing import Any
14
+
15
+ from mcp.server.fastmcp import FastMCP
16
+
17
+ from ..core.adapter import BaseAdapter
18
+ from ..core.registry import AdapterRegistry
19
+
20
+ # Initialize FastMCP server
21
+ mcp = FastMCP("mcp-ticketer")
22
+
23
+ # Global adapter instance
24
+ _adapter: BaseAdapter | None = None
25
+
26
+ # Configure logging
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ def configure_adapter(adapter_type: str, config: dict[str, Any]) -> None:
31
+ """Configure the global adapter instance.
32
+
33
+ This must be called before starting the server to initialize the
34
+ adapter that will handle all ticket operations.
35
+
36
+ Args:
37
+ adapter_type: Type of adapter to create (e.g., "linear", "jira", "github")
38
+ config: Configuration dictionary for the adapter
39
+
40
+ Raises:
41
+ ValueError: If adapter type is not registered
42
+ RuntimeError: If adapter configuration fails
43
+
44
+ """
45
+ global _adapter
46
+
47
+ try:
48
+ # Get adapter from registry
49
+ _adapter = AdapterRegistry.get_adapter(adapter_type, config)
50
+ logger.info(f"Configured {adapter_type} adapter for MCP server")
51
+ except Exception as e:
52
+ logger.error(f"Failed to configure adapter: {e}")
53
+ raise RuntimeError(f"Adapter configuration failed: {e}") from e
54
+
55
+
56
+ def get_adapter() -> BaseAdapter:
57
+ """Get the configured adapter instance.
58
+
59
+ Returns:
60
+ The global adapter instance
61
+
62
+ Raises:
63
+ RuntimeError: If adapter has not been configured
64
+
65
+ """
66
+ if _adapter is None:
67
+ raise RuntimeError(
68
+ "Adapter not configured. Call configure_adapter() before starting server."
69
+ )
70
+ return _adapter
71
+
72
+
73
+ # Import all tool modules to register them with FastMCP
74
+ # These imports must come after mcp is initialized but before main()
75
+ from . import tools # noqa: E402, F401
76
+
77
+
78
+ def main() -> None:
79
+ """Run the FastMCP server.
80
+
81
+ This function starts the server using stdio transport for
82
+ JSON-RPC communication with Claude Desktop/Code.
83
+
84
+ The adapter must be configured via configure_adapter() before
85
+ calling this function.
86
+
87
+ """
88
+ # Run the server with stdio transport
89
+ mcp.run(transport="stdio")
90
+
91
+
92
+ if __name__ == "__main__":
93
+ main()
@@ -0,0 +1,36 @@
1
+ """MCP tool modules for ticket operations.
2
+
3
+ This package contains all FastMCP tool implementations organized by
4
+ functional area. Tools are automatically registered with the FastMCP
5
+ server when imported.
6
+
7
+ Modules:
8
+ ticket_tools: Basic CRUD operations for tickets
9
+ hierarchy_tools: Epic/Issue/Task hierarchy management
10
+ search_tools: Search and query operations
11
+ bulk_tools: Bulk create and update operations
12
+ comment_tools: Comment management
13
+ pr_tools: Pull request integration
14
+ attachment_tools: File attachment handling
15
+
16
+ """
17
+
18
+ # Import all tool modules to register them with FastMCP
19
+ # Order matters - import core functionality first
20
+ from . import attachment_tools # noqa: F401
21
+ from . import bulk_tools # noqa: F401
22
+ from . import comment_tools # noqa: F401
23
+ from . import hierarchy_tools # noqa: F401
24
+ from . import pr_tools # noqa: F401
25
+ from . import search_tools # noqa: F401
26
+ from . import ticket_tools # noqa: F401
27
+
28
+ __all__ = [
29
+ "ticket_tools",
30
+ "hierarchy_tools",
31
+ "search_tools",
32
+ "bulk_tools",
33
+ "comment_tools",
34
+ "pr_tools",
35
+ "attachment_tools",
36
+ ]