mcp-ticketer 0.3.5__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.
- mcp_ticketer/__version__.py +3 -3
- mcp_ticketer/adapters/__init__.py +2 -0
- mcp_ticketer/adapters/aitrackdown.py +263 -14
- mcp_ticketer/adapters/asana/__init__.py +15 -0
- mcp_ticketer/adapters/asana/adapter.py +1308 -0
- mcp_ticketer/adapters/asana/client.py +292 -0
- mcp_ticketer/adapters/asana/mappers.py +334 -0
- mcp_ticketer/adapters/asana/types.py +146 -0
- mcp_ticketer/adapters/github.py +326 -109
- mcp_ticketer/adapters/hybrid.py +11 -11
- mcp_ticketer/adapters/jira.py +271 -25
- mcp_ticketer/adapters/linear/adapter.py +693 -39
- mcp_ticketer/adapters/linear/client.py +61 -9
- mcp_ticketer/adapters/linear/mappers.py +9 -3
- mcp_ticketer/adapters/linear/queries.py +9 -7
- mcp_ticketer/cache/memory.py +9 -8
- mcp_ticketer/cli/adapter_diagnostics.py +1 -1
- mcp_ticketer/cli/auggie_configure.py +104 -15
- mcp_ticketer/cli/codex_configure.py +188 -32
- mcp_ticketer/cli/configure.py +37 -48
- mcp_ticketer/cli/diagnostics.py +20 -18
- mcp_ticketer/cli/discover.py +292 -26
- mcp_ticketer/cli/gemini_configure.py +107 -26
- mcp_ticketer/cli/instruction_commands.py +429 -0
- mcp_ticketer/cli/linear_commands.py +105 -22
- mcp_ticketer/cli/main.py +1830 -435
- mcp_ticketer/cli/mcp_configure.py +296 -89
- mcp_ticketer/cli/migrate_config.py +12 -8
- mcp_ticketer/cli/platform_commands.py +123 -0
- mcp_ticketer/cli/platform_detection.py +412 -0
- mcp_ticketer/cli/python_detection.py +126 -0
- mcp_ticketer/cli/queue_commands.py +15 -15
- mcp_ticketer/cli/simple_health.py +1 -1
- mcp_ticketer/cli/ticket_commands.py +773 -0
- mcp_ticketer/cli/update_checker.py +313 -0
- mcp_ticketer/cli/utils.py +67 -62
- mcp_ticketer/core/__init__.py +14 -1
- mcp_ticketer/core/adapter.py +84 -15
- mcp_ticketer/core/config.py +44 -39
- mcp_ticketer/core/env_discovery.py +42 -12
- mcp_ticketer/core/env_loader.py +15 -14
- mcp_ticketer/core/exceptions.py +3 -3
- mcp_ticketer/core/http_client.py +26 -26
- mcp_ticketer/core/instructions.py +405 -0
- mcp_ticketer/core/mappers.py +11 -11
- mcp_ticketer/core/models.py +50 -20
- mcp_ticketer/core/onepassword_secrets.py +379 -0
- mcp_ticketer/core/project_config.py +57 -35
- mcp_ticketer/core/registry.py +3 -3
- mcp_ticketer/defaults/ticket_instructions.md +644 -0
- mcp_ticketer/mcp/__init__.py +29 -1
- mcp_ticketer/mcp/__main__.py +60 -0
- mcp_ticketer/mcp/server/__init__.py +25 -0
- mcp_ticketer/mcp/server/__main__.py +60 -0
- mcp_ticketer/mcp/{dto.py → server/dto.py} +32 -32
- mcp_ticketer/mcp/{server.py → server/main.py} +127 -74
- mcp_ticketer/mcp/{response_builder.py → server/response_builder.py} +2 -2
- mcp_ticketer/mcp/server/server_sdk.py +93 -0
- mcp_ticketer/mcp/server/tools/__init__.py +47 -0
- mcp_ticketer/mcp/server/tools/attachment_tools.py +226 -0
- mcp_ticketer/mcp/server/tools/bulk_tools.py +273 -0
- mcp_ticketer/mcp/server/tools/comment_tools.py +90 -0
- mcp_ticketer/mcp/server/tools/config_tools.py +381 -0
- mcp_ticketer/mcp/server/tools/hierarchy_tools.py +532 -0
- mcp_ticketer/mcp/server/tools/instruction_tools.py +293 -0
- mcp_ticketer/mcp/server/tools/pr_tools.py +154 -0
- mcp_ticketer/mcp/server/tools/search_tools.py +206 -0
- mcp_ticketer/mcp/server/tools/ticket_tools.py +430 -0
- mcp_ticketer/mcp/server/tools/user_ticket_tools.py +382 -0
- mcp_ticketer/queue/__init__.py +1 -0
- mcp_ticketer/queue/health_monitor.py +5 -4
- mcp_ticketer/queue/manager.py +15 -51
- mcp_ticketer/queue/queue.py +19 -19
- mcp_ticketer/queue/run_worker.py +1 -1
- mcp_ticketer/queue/ticket_registry.py +14 -14
- mcp_ticketer/queue/worker.py +16 -14
- {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/METADATA +168 -32
- mcp_ticketer-0.12.0.dist-info/RECORD +91 -0
- mcp_ticketer-0.3.5.dist-info/RECORD +0 -62
- /mcp_ticketer/mcp/{constants.py → server/constants.py} +0 -0
- {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Main entry point for MCP server module invocation.
|
|
2
|
+
|
|
3
|
+
This module enables running the MCP server via:
|
|
4
|
+
python -m mcp_ticketer.mcp.server [project_path]
|
|
5
|
+
|
|
6
|
+
This is the preferred invocation method for MCP configurations as it:
|
|
7
|
+
- Works reliably across installation methods (pipx, pip, uv)
|
|
8
|
+
- Doesn't depend on binary path detection
|
|
9
|
+
- Follows the proven mcp-vector-search pattern
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from .server import main
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def run_server() -> None:
|
|
20
|
+
"""Run the MCP server with optional project path argument.
|
|
21
|
+
|
|
22
|
+
Usage:
|
|
23
|
+
python -m mcp_ticketer.mcp.server
|
|
24
|
+
python -m mcp_ticketer.mcp.server /path/to/project
|
|
25
|
+
|
|
26
|
+
Arguments:
|
|
27
|
+
project_path (optional): Path to project directory for context
|
|
28
|
+
|
|
29
|
+
"""
|
|
30
|
+
# Check for project path argument
|
|
31
|
+
if len(sys.argv) > 1:
|
|
32
|
+
project_path = Path(sys.argv[1])
|
|
33
|
+
|
|
34
|
+
# Validate project path exists
|
|
35
|
+
if not project_path.exists():
|
|
36
|
+
sys.stderr.write(f"Error: Project path does not exist: {project_path}\n")
|
|
37
|
+
sys.exit(1)
|
|
38
|
+
|
|
39
|
+
# Change to project directory for context
|
|
40
|
+
try:
|
|
41
|
+
import os
|
|
42
|
+
|
|
43
|
+
os.chdir(project_path)
|
|
44
|
+
sys.stderr.write(f"[MCP Server] Working directory: {project_path}\n")
|
|
45
|
+
except OSError as e:
|
|
46
|
+
sys.stderr.write(f"Warning: Could not change to project directory: {e}\n")
|
|
47
|
+
|
|
48
|
+
# Run the async main function
|
|
49
|
+
try:
|
|
50
|
+
asyncio.run(main())
|
|
51
|
+
except KeyboardInterrupt:
|
|
52
|
+
sys.stderr.write("\n[MCP Server] Interrupted by user\n")
|
|
53
|
+
sys.exit(0)
|
|
54
|
+
except Exception as e:
|
|
55
|
+
sys.stderr.write(f"[MCP Server] Fatal error: {e}\n")
|
|
56
|
+
sys.exit(1)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
if __name__ == "__main__":
|
|
60
|
+
run_server()
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""MCP Server package for mcp-ticketer.
|
|
2
|
+
|
|
3
|
+
This package provides the FastMCP server implementation for ticket management
|
|
4
|
+
operations via the Model Context Protocol (MCP).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from .main import MCPTicketServer, main
|
|
11
|
+
|
|
12
|
+
__all__ = ["main", "MCPTicketServer"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def __getattr__(name: str) -> Any:
|
|
16
|
+
"""Lazy import to avoid premature module loading."""
|
|
17
|
+
if name == "main":
|
|
18
|
+
from .main import main
|
|
19
|
+
|
|
20
|
+
return main
|
|
21
|
+
if name == "MCPTicketServer":
|
|
22
|
+
from .main import MCPTicketServer
|
|
23
|
+
|
|
24
|
+
return MCPTicketServer
|
|
25
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Main entry point for MCP server module invocation.
|
|
2
|
+
|
|
3
|
+
This module enables running the MCP server via:
|
|
4
|
+
python -m mcp_ticketer.mcp.server [project_path]
|
|
5
|
+
|
|
6
|
+
This is the preferred invocation method for MCP configurations as it:
|
|
7
|
+
- Works reliably across installation methods (pipx, pip, uv)
|
|
8
|
+
- Doesn't depend on binary path detection
|
|
9
|
+
- Follows the proven mcp-vector-search pattern
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from .main import main
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def run_server() -> None:
|
|
20
|
+
"""Run the MCP server with optional project path argument.
|
|
21
|
+
|
|
22
|
+
Usage:
|
|
23
|
+
python -m mcp_ticketer.mcp.server
|
|
24
|
+
python -m mcp_ticketer.mcp.server /path/to/project
|
|
25
|
+
|
|
26
|
+
Arguments:
|
|
27
|
+
project_path (optional): Path to project directory for context
|
|
28
|
+
|
|
29
|
+
"""
|
|
30
|
+
# Check for project path argument
|
|
31
|
+
if len(sys.argv) > 1:
|
|
32
|
+
project_path = Path(sys.argv[1])
|
|
33
|
+
|
|
34
|
+
# Validate project path exists
|
|
35
|
+
if not project_path.exists():
|
|
36
|
+
sys.stderr.write(f"Error: Project path does not exist: {project_path}\n")
|
|
37
|
+
sys.exit(1)
|
|
38
|
+
|
|
39
|
+
# Change to project directory for context
|
|
40
|
+
try:
|
|
41
|
+
import os
|
|
42
|
+
|
|
43
|
+
os.chdir(project_path)
|
|
44
|
+
sys.stderr.write(f"[MCP Server] Working directory: {project_path}\n")
|
|
45
|
+
except OSError as e:
|
|
46
|
+
sys.stderr.write(f"Warning: Could not change to project directory: {e}\n")
|
|
47
|
+
|
|
48
|
+
# Run the async main function
|
|
49
|
+
try:
|
|
50
|
+
asyncio.run(main())
|
|
51
|
+
except KeyboardInterrupt:
|
|
52
|
+
sys.stderr.write("\n[MCP Server] Interrupted by user\n")
|
|
53
|
+
sys.exit(0)
|
|
54
|
+
except Exception as e:
|
|
55
|
+
sys.stderr.write(f"[MCP Server] Fatal error: {e}\n")
|
|
56
|
+
sys.exit(1)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
if __name__ == "__main__":
|
|
60
|
+
run_server()
|
|
@@ -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):
|
|
@@ -4,15 +4,15 @@ 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
|
|
|
11
11
|
# Import adapters module to trigger registration
|
|
12
12
|
import mcp_ticketer.adapters # noqa: F401
|
|
13
13
|
|
|
14
|
-
from
|
|
15
|
-
from
|
|
14
|
+
from ...core import AdapterRegistry
|
|
15
|
+
from ...core.models import Comment, Epic, Priority, SearchQuery, Task, TicketState
|
|
16
16
|
from .constants import (
|
|
17
17
|
DEFAULT_BASE_PATH,
|
|
18
18
|
DEFAULT_LIMIT,
|
|
@@ -48,29 +48,12 @@ from .dto import (
|
|
|
48
48
|
)
|
|
49
49
|
from .response_builder import ResponseBuilder
|
|
50
50
|
|
|
51
|
-
# Load environment variables early (prioritize .env.local)
|
|
52
|
-
# Check for .env.local first (takes precedence)
|
|
53
|
-
env_local_file = Path.cwd() / ".env.local"
|
|
54
|
-
if env_local_file.exists():
|
|
55
|
-
load_dotenv(env_local_file, override=True)
|
|
56
|
-
sys.stderr.write(f"[MCP Server] Loaded environment from: {env_local_file}\n")
|
|
57
|
-
else:
|
|
58
|
-
# Fall back to .env
|
|
59
|
-
env_file = Path.cwd() / ".env"
|
|
60
|
-
if env_file.exists():
|
|
61
|
-
load_dotenv(env_file, override=True)
|
|
62
|
-
sys.stderr.write(f"[MCP Server] Loaded environment from: {env_file}\n")
|
|
63
|
-
else:
|
|
64
|
-
# Try default dotenv loading (searches upward)
|
|
65
|
-
load_dotenv(override=True)
|
|
66
|
-
sys.stderr.write("[MCP Server] Loaded environment from default search path\n")
|
|
67
|
-
|
|
68
51
|
|
|
69
52
|
class MCPTicketServer:
|
|
70
53
|
"""MCP server for ticket operations over stdio - synchronous implementation."""
|
|
71
54
|
|
|
72
55
|
def __init__(
|
|
73
|
-
self, adapter_type: str = "aitrackdown", config:
|
|
56
|
+
self, adapter_type: str = "aitrackdown", config: dict[str, Any] | None = None
|
|
74
57
|
):
|
|
75
58
|
"""Initialize MCP server.
|
|
76
59
|
|
|
@@ -861,6 +844,44 @@ class MCPTicketServer:
|
|
|
861
844
|
"required": ["title"],
|
|
862
845
|
},
|
|
863
846
|
},
|
|
847
|
+
{
|
|
848
|
+
"name": "ticket_comment",
|
|
849
|
+
"description": "Add or list comments on a ticket",
|
|
850
|
+
"inputSchema": {
|
|
851
|
+
"type": "object",
|
|
852
|
+
"properties": {
|
|
853
|
+
"operation": {
|
|
854
|
+
"type": "string",
|
|
855
|
+
"enum": ["add", "list"],
|
|
856
|
+
"description": "Operation to perform: 'add' to create a comment, 'list' to retrieve comments",
|
|
857
|
+
"default": "add",
|
|
858
|
+
},
|
|
859
|
+
"ticket_id": {
|
|
860
|
+
"type": "string",
|
|
861
|
+
"description": "Ticket ID to comment on",
|
|
862
|
+
},
|
|
863
|
+
"content": {
|
|
864
|
+
"type": "string",
|
|
865
|
+
"description": "Comment content (required for 'add' operation)",
|
|
866
|
+
},
|
|
867
|
+
"author": {
|
|
868
|
+
"type": "string",
|
|
869
|
+
"description": "Comment author (optional for 'add' operation)",
|
|
870
|
+
},
|
|
871
|
+
"limit": {
|
|
872
|
+
"type": "integer",
|
|
873
|
+
"default": 10,
|
|
874
|
+
"description": "Maximum number of comments to return (for 'list' operation)",
|
|
875
|
+
},
|
|
876
|
+
"offset": {
|
|
877
|
+
"type": "integer",
|
|
878
|
+
"default": 0,
|
|
879
|
+
"description": "Number of comments to skip (for 'list' operation)",
|
|
880
|
+
},
|
|
881
|
+
},
|
|
882
|
+
"required": ["ticket_id"],
|
|
883
|
+
},
|
|
884
|
+
},
|
|
864
885
|
]
|
|
865
886
|
}
|
|
866
887
|
|
|
@@ -913,6 +934,8 @@ class MCPTicketServer:
|
|
|
913
934
|
result = await self._handle_transition(arguments)
|
|
914
935
|
elif tool_name == "ticket_search":
|
|
915
936
|
result = await self._handle_search(arguments)
|
|
937
|
+
elif tool_name == "ticket_comment":
|
|
938
|
+
result = await self._handle_comment(arguments)
|
|
916
939
|
# PR integration
|
|
917
940
|
elif tool_name == "ticket_create_pr":
|
|
918
941
|
result = await self._handle_create_pr(arguments)
|
|
@@ -1010,8 +1033,8 @@ class MCPTicketServer:
|
|
|
1010
1033
|
await self.adapter.close()
|
|
1011
1034
|
|
|
1012
1035
|
|
|
1013
|
-
async def main():
|
|
1014
|
-
"""
|
|
1036
|
+
async def main() -> None:
|
|
1037
|
+
"""Run main entry point for MCP server - kept for backward compatibility.
|
|
1015
1038
|
|
|
1016
1039
|
This function is maintained in case it's being called directly,
|
|
1017
1040
|
but the preferred way is now through the CLI: `mcp-ticketer mcp`
|
|
@@ -1023,62 +1046,94 @@ async def main():
|
|
|
1023
1046
|
# Load configuration
|
|
1024
1047
|
import json
|
|
1025
1048
|
import logging
|
|
1026
|
-
from pathlib import Path
|
|
1027
1049
|
|
|
1028
1050
|
logger = logging.getLogger(__name__)
|
|
1029
1051
|
|
|
1052
|
+
# Load environment variables AFTER working directory has been set by __main__.py
|
|
1053
|
+
# This ensures we load .env files from the target project directory, not from where the command is executed
|
|
1054
|
+
env_local_file = Path.cwd() / ".env.local"
|
|
1055
|
+
if env_local_file.exists():
|
|
1056
|
+
load_dotenv(env_local_file, override=True)
|
|
1057
|
+
sys.stderr.write(f"[MCP Server] Loaded environment from: {env_local_file}\n")
|
|
1058
|
+
logger.debug(f"Loaded environment from: {env_local_file}")
|
|
1059
|
+
else:
|
|
1060
|
+
# Fall back to .env
|
|
1061
|
+
env_file = Path.cwd() / ".env"
|
|
1062
|
+
if env_file.exists():
|
|
1063
|
+
load_dotenv(env_file, override=True)
|
|
1064
|
+
sys.stderr.write(f"[MCP Server] Loaded environment from: {env_file}\n")
|
|
1065
|
+
logger.debug(f"Loaded environment from: {env_file}")
|
|
1066
|
+
else:
|
|
1067
|
+
# Try default dotenv loading (searches upward)
|
|
1068
|
+
load_dotenv(override=True)
|
|
1069
|
+
sys.stderr.write(
|
|
1070
|
+
"[MCP Server] Loaded environment from default search path\n"
|
|
1071
|
+
)
|
|
1072
|
+
logger.debug("Loaded environment from default search path")
|
|
1073
|
+
|
|
1030
1074
|
# Initialize defaults
|
|
1031
1075
|
adapter_type = "aitrackdown"
|
|
1032
1076
|
adapter_config = {"base_path": DEFAULT_BASE_PATH}
|
|
1033
1077
|
|
|
1034
|
-
# Priority 1: Check
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
adapter_type = env_config["adapter_type"]
|
|
1038
|
-
adapter_config = env_config["adapter_config"]
|
|
1039
|
-
logger.info(f"Using adapter from .env files: {adapter_type}")
|
|
1040
|
-
logger.info(f"Built adapter config from .env: {list(adapter_config.keys())}")
|
|
1041
|
-
else:
|
|
1042
|
-
# Priority 2: Check project-local config file
|
|
1043
|
-
config_file = Path.cwd() / ".mcp-ticketer" / "config.json"
|
|
1044
|
-
if config_file.exists():
|
|
1045
|
-
# Validate config is within project
|
|
1046
|
-
try:
|
|
1047
|
-
if not config_file.resolve().is_relative_to(Path.cwd().resolve()):
|
|
1048
|
-
logger.error(
|
|
1049
|
-
f"Security violation: Config file {config_file} "
|
|
1050
|
-
"is not within project directory"
|
|
1051
|
-
)
|
|
1052
|
-
raise ValueError(
|
|
1053
|
-
f"Security violation: Config file {config_file} "
|
|
1054
|
-
"is not within project directory"
|
|
1055
|
-
)
|
|
1056
|
-
except (ValueError, RuntimeError):
|
|
1057
|
-
# is_relative_to may raise ValueError in some cases
|
|
1058
|
-
pass
|
|
1078
|
+
# Priority 1: Check project-local config file (highest priority)
|
|
1079
|
+
config_file = Path.cwd() / ".mcp-ticketer" / "config.json"
|
|
1080
|
+
config_loaded = False
|
|
1059
1081
|
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
+
if config_file.exists():
|
|
1083
|
+
# Validate config is within project
|
|
1084
|
+
try:
|
|
1085
|
+
if not config_file.resolve().is_relative_to(Path.cwd().resolve()):
|
|
1086
|
+
logger.error(
|
|
1087
|
+
f"Security violation: Config file {config_file} "
|
|
1088
|
+
"is not within project directory"
|
|
1089
|
+
)
|
|
1090
|
+
raise ValueError(
|
|
1091
|
+
f"Security violation: Config file {config_file} "
|
|
1092
|
+
"is not within project directory"
|
|
1093
|
+
)
|
|
1094
|
+
except (ValueError, RuntimeError):
|
|
1095
|
+
# is_relative_to may raise ValueError in some cases
|
|
1096
|
+
pass
|
|
1097
|
+
|
|
1098
|
+
try:
|
|
1099
|
+
with open(config_file) as f:
|
|
1100
|
+
config = json.load(f)
|
|
1101
|
+
adapter_type = config.get("default_adapter", "aitrackdown")
|
|
1102
|
+
# Get adapter-specific config
|
|
1103
|
+
adapters_config = config.get("adapters", {})
|
|
1104
|
+
adapter_config = adapters_config.get(adapter_type, {})
|
|
1105
|
+
# Fallback to legacy config format
|
|
1106
|
+
if not adapter_config and "config" in config:
|
|
1107
|
+
adapter_config = config["config"]
|
|
1108
|
+
config_loaded = True
|
|
1109
|
+
logger.info(
|
|
1110
|
+
f"Loaded MCP configuration from project-local: {config_file}"
|
|
1111
|
+
)
|
|
1112
|
+
sys.stderr.write(
|
|
1113
|
+
f"[MCP Server] Using adapter from config: {adapter_type}\n"
|
|
1114
|
+
)
|
|
1115
|
+
except (OSError, json.JSONDecodeError) as e:
|
|
1116
|
+
logger.warning(f"Could not load project config: {e}, will try .env files")
|
|
1117
|
+
|
|
1118
|
+
# Priority 2: Check .env files (only if no config file found)
|
|
1119
|
+
if not config_loaded:
|
|
1120
|
+
env_config = _load_env_configuration()
|
|
1121
|
+
if env_config and env_config.get("adapter_type"):
|
|
1122
|
+
adapter_type = env_config["adapter_type"]
|
|
1123
|
+
adapter_config = env_config["adapter_config"]
|
|
1124
|
+
config_loaded = True
|
|
1125
|
+
logger.info(f"Using adapter from .env files: {adapter_type}")
|
|
1126
|
+
logger.info(
|
|
1127
|
+
f"Built adapter config from .env: {list(adapter_config.keys())}"
|
|
1128
|
+
)
|
|
1129
|
+
sys.stderr.write(f"[MCP Server] Using adapter from .env: {adapter_type}\n")
|
|
1130
|
+
|
|
1131
|
+
# Priority 3: Default to aitrackdown
|
|
1132
|
+
if not config_loaded:
|
|
1133
|
+
logger.info("No configuration found, defaulting to aitrackdown adapter")
|
|
1134
|
+
sys.stderr.write("[MCP Server] No config found, using default: aitrackdown\n")
|
|
1135
|
+
adapter_type = "aitrackdown"
|
|
1136
|
+
adapter_config = {"base_path": DEFAULT_BASE_PATH}
|
|
1082
1137
|
|
|
1083
1138
|
# Log final configuration for debugging
|
|
1084
1139
|
logger.info(f"Starting MCP server with adapter: {adapter_type}")
|
|
@@ -1089,7 +1144,7 @@ async def main():
|
|
|
1089
1144
|
await server.run()
|
|
1090
1145
|
|
|
1091
1146
|
|
|
1092
|
-
def _load_env_configuration() ->
|
|
1147
|
+
def _load_env_configuration() -> dict[str, Any] | None:
|
|
1093
1148
|
"""Load adapter configuration from .env files.
|
|
1094
1149
|
|
|
1095
1150
|
Checks .env.local first (highest priority), then .env.
|
|
@@ -1098,8 +1153,6 @@ def _load_env_configuration() -> Optional[dict[str, Any]]:
|
|
|
1098
1153
|
Dictionary with 'adapter_type' and 'adapter_config' keys, or None if no config found
|
|
1099
1154
|
|
|
1100
1155
|
"""
|
|
1101
|
-
from pathlib import Path
|
|
1102
|
-
|
|
1103
1156
|
# Check for .env files in order of preference
|
|
1104
1157
|
env_files = [".env.local", ".env"]
|
|
1105
1158
|
env_vars = {}
|
|
@@ -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
|
|