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.

Files changed (70) hide show
  1. mcp_ticketer/__version__.py +3 -3
  2. mcp_ticketer/adapters/__init__.py +2 -0
  3. mcp_ticketer/adapters/aitrackdown.py +9 -3
  4. mcp_ticketer/adapters/asana/__init__.py +15 -0
  5. mcp_ticketer/adapters/asana/adapter.py +1308 -0
  6. mcp_ticketer/adapters/asana/client.py +292 -0
  7. mcp_ticketer/adapters/asana/mappers.py +334 -0
  8. mcp_ticketer/adapters/asana/types.py +146 -0
  9. mcp_ticketer/adapters/github.py +313 -96
  10. mcp_ticketer/adapters/jira.py +251 -1
  11. mcp_ticketer/adapters/linear/adapter.py +524 -22
  12. mcp_ticketer/adapters/linear/client.py +61 -9
  13. mcp_ticketer/adapters/linear/mappers.py +9 -3
  14. mcp_ticketer/cache/memory.py +3 -3
  15. mcp_ticketer/cli/adapter_diagnostics.py +1 -1
  16. mcp_ticketer/cli/auggie_configure.py +1 -1
  17. mcp_ticketer/cli/codex_configure.py +80 -1
  18. mcp_ticketer/cli/configure.py +33 -43
  19. mcp_ticketer/cli/diagnostics.py +18 -16
  20. mcp_ticketer/cli/discover.py +288 -21
  21. mcp_ticketer/cli/gemini_configure.py +1 -1
  22. mcp_ticketer/cli/instruction_commands.py +429 -0
  23. mcp_ticketer/cli/linear_commands.py +99 -15
  24. mcp_ticketer/cli/main.py +1199 -227
  25. mcp_ticketer/cli/mcp_configure.py +1 -1
  26. mcp_ticketer/cli/migrate_config.py +12 -8
  27. mcp_ticketer/cli/platform_commands.py +6 -6
  28. mcp_ticketer/cli/platform_detection.py +412 -0
  29. mcp_ticketer/cli/queue_commands.py +15 -15
  30. mcp_ticketer/cli/simple_health.py +1 -1
  31. mcp_ticketer/cli/ticket_commands.py +14 -13
  32. mcp_ticketer/cli/update_checker.py +313 -0
  33. mcp_ticketer/cli/utils.py +45 -41
  34. mcp_ticketer/core/__init__.py +12 -0
  35. mcp_ticketer/core/adapter.py +4 -4
  36. mcp_ticketer/core/config.py +17 -10
  37. mcp_ticketer/core/env_discovery.py +33 -3
  38. mcp_ticketer/core/env_loader.py +7 -6
  39. mcp_ticketer/core/exceptions.py +3 -3
  40. mcp_ticketer/core/http_client.py +10 -10
  41. mcp_ticketer/core/instructions.py +405 -0
  42. mcp_ticketer/core/mappers.py +1 -1
  43. mcp_ticketer/core/models.py +1 -1
  44. mcp_ticketer/core/onepassword_secrets.py +379 -0
  45. mcp_ticketer/core/project_config.py +17 -1
  46. mcp_ticketer/core/registry.py +1 -1
  47. mcp_ticketer/defaults/ticket_instructions.md +644 -0
  48. mcp_ticketer/mcp/__init__.py +2 -2
  49. mcp_ticketer/mcp/server/__init__.py +2 -2
  50. mcp_ticketer/mcp/server/main.py +82 -69
  51. mcp_ticketer/mcp/server/tools/__init__.py +9 -0
  52. mcp_ticketer/mcp/server/tools/attachment_tools.py +63 -16
  53. mcp_ticketer/mcp/server/tools/config_tools.py +381 -0
  54. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +154 -5
  55. mcp_ticketer/mcp/server/tools/instruction_tools.py +293 -0
  56. mcp_ticketer/mcp/server/tools/ticket_tools.py +157 -4
  57. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +382 -0
  58. mcp_ticketer/queue/health_monitor.py +1 -0
  59. mcp_ticketer/queue/manager.py +4 -4
  60. mcp_ticketer/queue/queue.py +3 -3
  61. mcp_ticketer/queue/run_worker.py +1 -1
  62. mcp_ticketer/queue/ticket_registry.py +2 -2
  63. mcp_ticketer/queue/worker.py +14 -12
  64. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-0.12.0.dist-info}/METADATA +106 -52
  65. mcp_ticketer-0.12.0.dist-info/RECORD +91 -0
  66. mcp_ticketer-0.4.11.dist-info/RECORD +0 -77
  67. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-0.12.0.dist-info}/WHEEL +0 -0
  68. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-0.12.0.dist-info}/entry_points.txt +0 -0
  69. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-0.12.0.dist-info}/licenses/LICENSE +0 -0
  70. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-0.12.0.dist-info}/top_level.txt +0 -0
@@ -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
- """Main entry point for MCP server - kept for backward compatibility.
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 .env files (highest priority for MCP)
1075
- env_config = _load_env_configuration()
1076
- if env_config and env_config.get("adapter_type"):
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
- try:
1101
- with open(config_file) as f:
1102
- config = json.load(f)
1103
- adapter_type = config.get("default_adapter", "aitrackdown")
1104
- # Get adapter-specific config
1105
- adapters_config = config.get("adapters", {})
1106
- adapter_config = adapters_config.get(adapter_type, {})
1107
- # Fallback to legacy config format
1108
- if not adapter_config and "config" in config:
1109
- adapter_config = config["config"]
1110
- logger.info(
1111
- f"Loaded MCP configuration from project-local: {config_file}"
1112
- )
1113
- except (OSError, json.JSONDecodeError) as e:
1114
- logger.warning(f"Could not load project config: {e}, using defaults")
1115
- adapter_type = "aitrackdown"
1116
- adapter_config = {"base_path": DEFAULT_BASE_PATH}
1117
- else:
1118
- # Priority 3: Default to aitrackdown
1119
- logger.info("No configuration found, defaulting to aitrackdown adapter")
1120
- adapter_type = "aitrackdown"
1121
- adapter_config = {"base_path": DEFAULT_BASE_PATH}
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 adapter supports attachments
46
- if not hasattr(adapter, "add_attachment"):
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 attachments not supported by {type(adapter).__name__} adapter",
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
- # Add attachment via adapter
55
- attachment = await adapter.add_attachment( # type: ignore
56
- ticket_id=ticket_id, file_path=file_path, description=description
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
- return {
60
- "status": "completed",
61
- "ticket_id": ticket_id,
62
- "attachment": attachment,
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: