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.
- mcp_ticketer/__init__.py +3 -12
- mcp_ticketer/__version__.py +1 -1
- mcp_ticketer/adapters/aitrackdown.py +243 -11
- mcp_ticketer/adapters/github.py +15 -14
- mcp_ticketer/adapters/hybrid.py +11 -11
- mcp_ticketer/adapters/jira.py +22 -25
- mcp_ticketer/adapters/linear/adapter.py +9 -21
- mcp_ticketer/adapters/linear/client.py +2 -1
- mcp_ticketer/adapters/linear/mappers.py +2 -1
- mcp_ticketer/cache/memory.py +6 -5
- mcp_ticketer/cli/adapter_diagnostics.py +4 -2
- mcp_ticketer/cli/auggie_configure.py +66 -0
- mcp_ticketer/cli/codex_configure.py +70 -2
- mcp_ticketer/cli/configure.py +7 -14
- mcp_ticketer/cli/diagnostics.py +2 -2
- mcp_ticketer/cli/discover.py +6 -11
- mcp_ticketer/cli/gemini_configure.py +68 -2
- mcp_ticketer/cli/linear_commands.py +6 -7
- mcp_ticketer/cli/main.py +341 -203
- mcp_ticketer/cli/mcp_configure.py +61 -2
- mcp_ticketer/cli/ticket_commands.py +27 -30
- mcp_ticketer/cli/utils.py +23 -22
- mcp_ticketer/core/__init__.py +3 -1
- mcp_ticketer/core/adapter.py +82 -13
- mcp_ticketer/core/config.py +27 -29
- mcp_ticketer/core/env_discovery.py +10 -10
- mcp_ticketer/core/env_loader.py +8 -8
- mcp_ticketer/core/http_client.py +16 -16
- mcp_ticketer/core/mappers.py +10 -10
- mcp_ticketer/core/models.py +50 -20
- mcp_ticketer/core/project_config.py +40 -34
- mcp_ticketer/core/registry.py +2 -2
- mcp_ticketer/mcp/dto.py +32 -32
- mcp_ticketer/mcp/response_builder.py +2 -2
- mcp_ticketer/mcp/server.py +17 -37
- mcp_ticketer/mcp/server_sdk.py +93 -0
- mcp_ticketer/mcp/tools/__init__.py +36 -0
- mcp_ticketer/mcp/tools/attachment_tools.py +179 -0
- mcp_ticketer/mcp/tools/bulk_tools.py +273 -0
- mcp_ticketer/mcp/tools/comment_tools.py +90 -0
- mcp_ticketer/mcp/tools/hierarchy_tools.py +383 -0
- mcp_ticketer/mcp/tools/pr_tools.py +154 -0
- mcp_ticketer/mcp/tools/search_tools.py +206 -0
- mcp_ticketer/mcp/tools/ticket_tools.py +277 -0
- mcp_ticketer/queue/health_monitor.py +4 -4
- mcp_ticketer/queue/manager.py +2 -2
- mcp_ticketer/queue/queue.py +16 -16
- mcp_ticketer/queue/ticket_registry.py +7 -7
- mcp_ticketer/queue/worker.py +2 -2
- {mcp_ticketer-0.4.1.dist-info → mcp_ticketer-0.4.3.dist-info}/METADATA +90 -17
- mcp_ticketer-0.4.3.dist-info/RECORD +73 -0
- mcp_ticketer-0.4.1.dist-info/RECORD +0 -64
- {mcp_ticketer-0.4.1.dist-info → mcp_ticketer-0.4.3.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.4.1.dist-info → mcp_ticketer-0.4.3.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.4.1.dist-info → mcp_ticketer-0.4.3.dist-info}/licenses/LICENSE +0 -0
- {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:
|
|
51
|
-
token:
|
|
50
|
+
api_key: str | None = None
|
|
51
|
+
token: str | None = None
|
|
52
52
|
|
|
53
53
|
# Linear-specific
|
|
54
|
-
team_id:
|
|
55
|
-
team_key:
|
|
56
|
-
workspace:
|
|
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:
|
|
60
|
-
email:
|
|
61
|
-
api_token:
|
|
62
|
-
project_key:
|
|
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:
|
|
66
|
-
repo:
|
|
65
|
+
owner: str | None = None
|
|
66
|
+
repo: str | None = None
|
|
67
67
|
|
|
68
68
|
# AITrackdown-specific
|
|
69
|
-
base_path:
|
|
69
|
+
base_path: str | None = None
|
|
70
70
|
|
|
71
71
|
# Project ID (can be used by any adapter for scoping)
|
|
72
|
-
project_id:
|
|
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:
|
|
130
|
-
project_id:
|
|
131
|
-
team_id:
|
|
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:
|
|
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:
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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:
|
|
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:
|
|
365
|
-
self._discovered_config:
|
|
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:
|
|
386
|
-
) ->
|
|
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:
|
|
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:
|
|
465
|
-
cli_overrides:
|
|
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) ->
|
|
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:
|
|
664
|
+
_default_resolver: ConfigResolver | None = None
|
|
659
665
|
|
|
660
666
|
|
|
661
|
-
def get_config_resolver(project_path:
|
|
667
|
+
def get_config_resolver(project_path: Path | None = None) -> ConfigResolver:
|
|
662
668
|
"""Get the global config resolver instance.
|
|
663
669
|
|
|
664
670
|
Args:
|
mcp_ticketer/core/registry.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Adapter registry for dynamic adapter management."""
|
|
2
2
|
|
|
3
|
-
from typing import Any
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
26
|
-
lead_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:
|
|
34
|
-
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:
|
|
36
|
+
assignee: str | None = Field(None, description="Issue assignee")
|
|
37
37
|
tags: list[str] = Field(default_factory=list, description="Issue tags")
|
|
38
|
-
estimated_hours:
|
|
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:
|
|
48
|
+
description: str | None = Field(None, description="Task description")
|
|
49
49
|
priority: str = Field("medium", description="Task priority")
|
|
50
|
-
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:
|
|
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:
|
|
81
|
-
state:
|
|
82
|
-
priority:
|
|
83
|
-
assignee:
|
|
84
|
-
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:
|
|
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:
|
|
108
|
-
author:
|
|
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:
|
|
119
|
-
title:
|
|
120
|
-
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:
|
|
123
|
-
github_repo:
|
|
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:
|
|
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:
|
|
177
|
-
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:
|
|
188
|
-
file_content:
|
|
189
|
-
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
|
|
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:
|
|
39
|
+
data: dict[str, Any] | None = None,
|
|
40
40
|
) -> dict[str, Any]:
|
|
41
41
|
"""Build error response.
|
|
42
42
|
|
mcp_ticketer/mcp/server.py
CHANGED
|
@@ -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
|
|
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,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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:
|
|
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() ->
|
|
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
|
+
]
|