mcp-ticketer 0.4.11__py3-none-any.whl → 0.12.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of mcp-ticketer might be problematic. Click here for more details.
- mcp_ticketer/__version__.py +3 -3
- mcp_ticketer/adapters/__init__.py +2 -0
- mcp_ticketer/adapters/aitrackdown.py +9 -3
- 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 +313 -96
- mcp_ticketer/adapters/jira.py +251 -1
- mcp_ticketer/adapters/linear/adapter.py +524 -22
- mcp_ticketer/adapters/linear/client.py +61 -9
- mcp_ticketer/adapters/linear/mappers.py +9 -3
- mcp_ticketer/cache/memory.py +3 -3
- mcp_ticketer/cli/adapter_diagnostics.py +1 -1
- mcp_ticketer/cli/auggie_configure.py +1 -1
- mcp_ticketer/cli/codex_configure.py +80 -1
- mcp_ticketer/cli/configure.py +33 -43
- mcp_ticketer/cli/diagnostics.py +18 -16
- mcp_ticketer/cli/discover.py +288 -21
- mcp_ticketer/cli/gemini_configure.py +1 -1
- mcp_ticketer/cli/instruction_commands.py +429 -0
- mcp_ticketer/cli/linear_commands.py +99 -15
- mcp_ticketer/cli/main.py +1199 -227
- mcp_ticketer/cli/mcp_configure.py +1 -1
- mcp_ticketer/cli/migrate_config.py +12 -8
- mcp_ticketer/cli/platform_commands.py +6 -6
- mcp_ticketer/cli/platform_detection.py +412 -0
- mcp_ticketer/cli/queue_commands.py +15 -15
- mcp_ticketer/cli/simple_health.py +1 -1
- mcp_ticketer/cli/ticket_commands.py +14 -13
- mcp_ticketer/cli/update_checker.py +313 -0
- mcp_ticketer/cli/utils.py +45 -41
- mcp_ticketer/core/__init__.py +12 -0
- mcp_ticketer/core/adapter.py +4 -4
- mcp_ticketer/core/config.py +17 -10
- mcp_ticketer/core/env_discovery.py +33 -3
- mcp_ticketer/core/env_loader.py +7 -6
- mcp_ticketer/core/exceptions.py +3 -3
- mcp_ticketer/core/http_client.py +10 -10
- mcp_ticketer/core/instructions.py +405 -0
- mcp_ticketer/core/mappers.py +1 -1
- mcp_ticketer/core/models.py +1 -1
- mcp_ticketer/core/onepassword_secrets.py +379 -0
- mcp_ticketer/core/project_config.py +17 -1
- mcp_ticketer/core/registry.py +1 -1
- mcp_ticketer/defaults/ticket_instructions.md +644 -0
- mcp_ticketer/mcp/__init__.py +2 -2
- mcp_ticketer/mcp/server/__init__.py +2 -2
- mcp_ticketer/mcp/server/main.py +82 -69
- mcp_ticketer/mcp/server/tools/__init__.py +9 -0
- mcp_ticketer/mcp/server/tools/attachment_tools.py +63 -16
- mcp_ticketer/mcp/server/tools/config_tools.py +381 -0
- mcp_ticketer/mcp/server/tools/hierarchy_tools.py +154 -5
- mcp_ticketer/mcp/server/tools/instruction_tools.py +293 -0
- mcp_ticketer/mcp/server/tools/ticket_tools.py +157 -4
- mcp_ticketer/mcp/server/tools/user_ticket_tools.py +382 -0
- mcp_ticketer/queue/health_monitor.py +1 -0
- mcp_ticketer/queue/manager.py +4 -4
- mcp_ticketer/queue/queue.py +3 -3
- mcp_ticketer/queue/run_worker.py +1 -1
- mcp_ticketer/queue/ticket_registry.py +2 -2
- mcp_ticketer/queue/worker.py +14 -12
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-0.12.0.dist-info}/METADATA +106 -52
- mcp_ticketer-0.12.0.dist-info/RECORD +91 -0
- mcp_ticketer-0.4.11.dist-info/RECORD +0 -77
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-0.12.0.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-0.12.0.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-0.12.0.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-0.12.0.dist-info}/top_level.txt +0 -0
mcp_ticketer/mcp/server/main.py
CHANGED
|
@@ -48,23 +48,6 @@ 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."""
|
|
@@ -1050,8 +1033,8 @@ class MCPTicketServer:
|
|
|
1050
1033
|
await self.adapter.close()
|
|
1051
1034
|
|
|
1052
1035
|
|
|
1053
|
-
async def main():
|
|
1054
|
-
"""
|
|
1036
|
+
async def main() -> None:
|
|
1037
|
+
"""Run main entry point for MCP server - kept for backward compatibility.
|
|
1055
1038
|
|
|
1056
1039
|
This function is maintained in case it's being called directly,
|
|
1057
1040
|
but the preferred way is now through the CLI: `mcp-ticketer mcp`
|
|
@@ -1063,62 +1046,94 @@ async def main():
|
|
|
1063
1046
|
# Load configuration
|
|
1064
1047
|
import json
|
|
1065
1048
|
import logging
|
|
1066
|
-
from pathlib import Path
|
|
1067
1049
|
|
|
1068
1050
|
logger = logging.getLogger(__name__)
|
|
1069
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
|
+
|
|
1070
1074
|
# Initialize defaults
|
|
1071
1075
|
adapter_type = "aitrackdown"
|
|
1072
1076
|
adapter_config = {"base_path": DEFAULT_BASE_PATH}
|
|
1073
1077
|
|
|
1074
|
-
# Priority 1: Check
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
adapter_type = env_config["adapter_type"]
|
|
1078
|
-
adapter_config = env_config["adapter_config"]
|
|
1079
|
-
logger.info(f"Using adapter from .env files: {adapter_type}")
|
|
1080
|
-
logger.info(f"Built adapter config from .env: {list(adapter_config.keys())}")
|
|
1081
|
-
else:
|
|
1082
|
-
# Priority 2: Check project-local config file
|
|
1083
|
-
config_file = Path.cwd() / ".mcp-ticketer" / "config.json"
|
|
1084
|
-
if config_file.exists():
|
|
1085
|
-
# Validate config is within project
|
|
1086
|
-
try:
|
|
1087
|
-
if not config_file.resolve().is_relative_to(Path.cwd().resolve()):
|
|
1088
|
-
logger.error(
|
|
1089
|
-
f"Security violation: Config file {config_file} "
|
|
1090
|
-
"is not within project directory"
|
|
1091
|
-
)
|
|
1092
|
-
raise ValueError(
|
|
1093
|
-
f"Security violation: Config file {config_file} "
|
|
1094
|
-
"is not within project directory"
|
|
1095
|
-
)
|
|
1096
|
-
except (ValueError, RuntimeError):
|
|
1097
|
-
# is_relative_to may raise ValueError in some cases
|
|
1098
|
-
pass
|
|
1078
|
+
# Priority 1: Check project-local config file (highest priority)
|
|
1079
|
+
config_file = Path.cwd() / ".mcp-ticketer" / "config.json"
|
|
1080
|
+
config_loaded = False
|
|
1099
1081
|
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
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}
|
|
1122
1137
|
|
|
1123
1138
|
# Log final configuration for debugging
|
|
1124
1139
|
logger.info(f"Starting MCP server with adapter: {adapter_type}")
|
|
@@ -1138,8 +1153,6 @@ def _load_env_configuration() -> dict[str, Any] | None:
|
|
|
1138
1153
|
Dictionary with 'adapter_type' and 'adapter_config' keys, or None if no config found
|
|
1139
1154
|
|
|
1140
1155
|
"""
|
|
1141
|
-
from pathlib import Path
|
|
1142
|
-
|
|
1143
1156
|
# Check for .env files in order of preference
|
|
1144
1157
|
env_files = [".env.local", ".env"]
|
|
1145
1158
|
env_vars = {}
|
|
@@ -12,6 +12,9 @@ Modules:
|
|
|
12
12
|
comment_tools: Comment management
|
|
13
13
|
pr_tools: Pull request integration
|
|
14
14
|
attachment_tools: File attachment handling
|
|
15
|
+
instruction_tools: Ticket instructions management
|
|
16
|
+
config_tools: Configuration management (adapter, project, user settings)
|
|
17
|
+
user_ticket_tools: User-specific ticket operations (my tickets, transitions)
|
|
15
18
|
|
|
16
19
|
"""
|
|
17
20
|
|
|
@@ -21,10 +24,13 @@ from . import (
|
|
|
21
24
|
attachment_tools, # noqa: F401
|
|
22
25
|
bulk_tools, # noqa: F401
|
|
23
26
|
comment_tools, # noqa: F401
|
|
27
|
+
config_tools, # noqa: F401
|
|
24
28
|
hierarchy_tools, # noqa: F401
|
|
29
|
+
instruction_tools, # noqa: F401
|
|
25
30
|
pr_tools, # noqa: F401
|
|
26
31
|
search_tools, # noqa: F401
|
|
27
32
|
ticket_tools, # noqa: F401
|
|
33
|
+
user_ticket_tools, # noqa: F401
|
|
28
34
|
)
|
|
29
35
|
|
|
30
36
|
__all__ = [
|
|
@@ -35,4 +41,7 @@ __all__ = [
|
|
|
35
41
|
"comment_tools",
|
|
36
42
|
"pr_tools",
|
|
37
43
|
"attachment_tools",
|
|
44
|
+
"instruction_tools",
|
|
45
|
+
"config_tools",
|
|
46
|
+
"user_ticket_tools",
|
|
38
47
|
]
|
|
@@ -5,9 +5,11 @@ attachment information. Note that file attachment functionality may not be
|
|
|
5
5
|
available in all adapters.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
+
import mimetypes
|
|
9
|
+
from pathlib import Path
|
|
8
10
|
from typing import Any
|
|
9
11
|
|
|
10
|
-
from ....core.models import Comment
|
|
12
|
+
from ....core.models import Comment, TicketType
|
|
11
13
|
from ..server_sdk import get_adapter, mcp
|
|
12
14
|
|
|
13
15
|
|
|
@@ -34,7 +36,7 @@ async def ticket_attach(
|
|
|
34
36
|
try:
|
|
35
37
|
adapter = get_adapter()
|
|
36
38
|
|
|
37
|
-
# Read ticket to validate it exists
|
|
39
|
+
# Read ticket to validate it exists and determine type
|
|
38
40
|
ticket = await adapter.read(ticket_id)
|
|
39
41
|
if ticket is None:
|
|
40
42
|
return {
|
|
@@ -42,27 +44,72 @@ async def ticket_attach(
|
|
|
42
44
|
"error": f"Ticket {ticket_id} not found",
|
|
43
45
|
}
|
|
44
46
|
|
|
45
|
-
# Check if
|
|
46
|
-
|
|
47
|
+
# Check if file exists
|
|
48
|
+
file_path_obj = Path(file_path)
|
|
49
|
+
if not file_path_obj.exists():
|
|
47
50
|
return {
|
|
48
51
|
"status": "error",
|
|
49
|
-
"error": f"File
|
|
52
|
+
"error": f"File not found: {file_path}",
|
|
50
53
|
"ticket_id": ticket_id,
|
|
51
|
-
"note": "Consider using ticket_comment to add a reference to the file location",
|
|
52
54
|
}
|
|
53
55
|
|
|
54
|
-
#
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
# Try Linear-specific upload methods first (most advanced)
|
|
57
|
+
if hasattr(adapter, "upload_file") and hasattr(adapter, "attach_file_to_issue"):
|
|
58
|
+
try:
|
|
59
|
+
# Determine MIME type
|
|
60
|
+
mime_type = mimetypes.guess_type(file_path)[0]
|
|
61
|
+
|
|
62
|
+
# Upload file to Linear's storage
|
|
63
|
+
file_url = await adapter.upload_file(file_path, mime_type) # type: ignore
|
|
64
|
+
|
|
65
|
+
# Determine ticket type and attach accordingly
|
|
66
|
+
ticket_type = getattr(ticket, "ticket_type", None)
|
|
67
|
+
filename = file_path_obj.name
|
|
68
|
+
|
|
69
|
+
if ticket_type == TicketType.EPIC and hasattr(
|
|
70
|
+
adapter, "attach_file_to_epic"
|
|
71
|
+
):
|
|
72
|
+
# Attach to epic (project)
|
|
73
|
+
result = await adapter.attach_file_to_epic( # type: ignore
|
|
74
|
+
epic_id=ticket_id,
|
|
75
|
+
file_url=file_url,
|
|
76
|
+
title=description or filename,
|
|
77
|
+
subtitle=f"Uploaded file: {filename}",
|
|
78
|
+
)
|
|
79
|
+
else:
|
|
80
|
+
# Attach to issue/task
|
|
81
|
+
result = await adapter.attach_file_to_issue( # type: ignore
|
|
82
|
+
issue_id=ticket_id,
|
|
83
|
+
file_url=file_url,
|
|
84
|
+
title=description or filename,
|
|
85
|
+
subtitle=f"Uploaded file: {filename}",
|
|
86
|
+
comment_body=description if description else None,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
"status": "completed",
|
|
91
|
+
"ticket_id": ticket_id,
|
|
92
|
+
"method": "linear_native_upload",
|
|
93
|
+
"file_url": file_url,
|
|
94
|
+
"attachment": result,
|
|
95
|
+
}
|
|
96
|
+
except Exception:
|
|
97
|
+
# Fall through to legacy method if Linear-specific upload fails
|
|
98
|
+
pass
|
|
99
|
+
|
|
100
|
+
# Try legacy add_attachment method
|
|
101
|
+
if hasattr(adapter, "add_attachment"):
|
|
102
|
+
attachment = await adapter.add_attachment( # type: ignore
|
|
103
|
+
ticket_id=ticket_id, file_path=file_path, description=description
|
|
104
|
+
)
|
|
58
105
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
106
|
+
return {
|
|
107
|
+
"status": "completed",
|
|
108
|
+
"ticket_id": ticket_id,
|
|
109
|
+
"method": "adapter_native",
|
|
110
|
+
"attachment": attachment,
|
|
111
|
+
}
|
|
64
112
|
|
|
65
|
-
except AttributeError:
|
|
66
113
|
# Fallback: Add file reference as comment
|
|
67
114
|
comment_text = f"Attachment: {file_path}"
|
|
68
115
|
if description:
|