mcp-ticketer 0.2.0__py3-none-any.whl → 2.2.9__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.
Files changed (160) hide show
  1. mcp_ticketer/__init__.py +10 -10
  2. mcp_ticketer/__version__.py +3 -3
  3. mcp_ticketer/_version_scm.py +1 -0
  4. mcp_ticketer/adapters/__init__.py +2 -0
  5. mcp_ticketer/adapters/aitrackdown.py +930 -52
  6. mcp_ticketer/adapters/asana/__init__.py +15 -0
  7. mcp_ticketer/adapters/asana/adapter.py +1537 -0
  8. mcp_ticketer/adapters/asana/client.py +292 -0
  9. mcp_ticketer/adapters/asana/mappers.py +348 -0
  10. mcp_ticketer/adapters/asana/types.py +146 -0
  11. mcp_ticketer/adapters/github/__init__.py +26 -0
  12. mcp_ticketer/adapters/github/adapter.py +3229 -0
  13. mcp_ticketer/adapters/github/client.py +335 -0
  14. mcp_ticketer/adapters/github/mappers.py +797 -0
  15. mcp_ticketer/adapters/github/queries.py +692 -0
  16. mcp_ticketer/adapters/github/types.py +460 -0
  17. mcp_ticketer/adapters/hybrid.py +58 -16
  18. mcp_ticketer/adapters/jira/__init__.py +35 -0
  19. mcp_ticketer/adapters/jira/adapter.py +1351 -0
  20. mcp_ticketer/adapters/jira/client.py +271 -0
  21. mcp_ticketer/adapters/jira/mappers.py +246 -0
  22. mcp_ticketer/adapters/jira/queries.py +216 -0
  23. mcp_ticketer/adapters/jira/types.py +304 -0
  24. mcp_ticketer/adapters/linear/__init__.py +1 -1
  25. mcp_ticketer/adapters/linear/adapter.py +3810 -462
  26. mcp_ticketer/adapters/linear/client.py +312 -69
  27. mcp_ticketer/adapters/linear/mappers.py +305 -85
  28. mcp_ticketer/adapters/linear/queries.py +317 -17
  29. mcp_ticketer/adapters/linear/types.py +187 -64
  30. mcp_ticketer/adapters/linear.py +2 -2
  31. mcp_ticketer/analysis/__init__.py +56 -0
  32. mcp_ticketer/analysis/dependency_graph.py +255 -0
  33. mcp_ticketer/analysis/health_assessment.py +304 -0
  34. mcp_ticketer/analysis/orphaned.py +218 -0
  35. mcp_ticketer/analysis/project_status.py +594 -0
  36. mcp_ticketer/analysis/similarity.py +224 -0
  37. mcp_ticketer/analysis/staleness.py +266 -0
  38. mcp_ticketer/automation/__init__.py +11 -0
  39. mcp_ticketer/automation/project_updates.py +378 -0
  40. mcp_ticketer/cache/memory.py +9 -8
  41. mcp_ticketer/cli/adapter_diagnostics.py +421 -0
  42. mcp_ticketer/cli/auggie_configure.py +116 -15
  43. mcp_ticketer/cli/codex_configure.py +274 -82
  44. mcp_ticketer/cli/configure.py +1323 -151
  45. mcp_ticketer/cli/cursor_configure.py +314 -0
  46. mcp_ticketer/cli/diagnostics.py +209 -114
  47. mcp_ticketer/cli/discover.py +297 -26
  48. mcp_ticketer/cli/gemini_configure.py +119 -26
  49. mcp_ticketer/cli/init_command.py +880 -0
  50. mcp_ticketer/cli/install_mcp_server.py +418 -0
  51. mcp_ticketer/cli/instruction_commands.py +435 -0
  52. mcp_ticketer/cli/linear_commands.py +256 -130
  53. mcp_ticketer/cli/main.py +140 -1284
  54. mcp_ticketer/cli/mcp_configure.py +1013 -100
  55. mcp_ticketer/cli/mcp_server_commands.py +415 -0
  56. mcp_ticketer/cli/migrate_config.py +12 -8
  57. mcp_ticketer/cli/platform_commands.py +123 -0
  58. mcp_ticketer/cli/platform_detection.py +477 -0
  59. mcp_ticketer/cli/platform_installer.py +545 -0
  60. mcp_ticketer/cli/project_update_commands.py +350 -0
  61. mcp_ticketer/cli/python_detection.py +126 -0
  62. mcp_ticketer/cli/queue_commands.py +15 -15
  63. mcp_ticketer/cli/setup_command.py +794 -0
  64. mcp_ticketer/cli/simple_health.py +84 -59
  65. mcp_ticketer/cli/ticket_commands.py +1375 -0
  66. mcp_ticketer/cli/update_checker.py +313 -0
  67. mcp_ticketer/cli/utils.py +195 -72
  68. mcp_ticketer/core/__init__.py +64 -1
  69. mcp_ticketer/core/adapter.py +618 -18
  70. mcp_ticketer/core/config.py +77 -68
  71. mcp_ticketer/core/env_discovery.py +75 -16
  72. mcp_ticketer/core/env_loader.py +121 -97
  73. mcp_ticketer/core/exceptions.py +32 -24
  74. mcp_ticketer/core/http_client.py +26 -26
  75. mcp_ticketer/core/instructions.py +405 -0
  76. mcp_ticketer/core/label_manager.py +732 -0
  77. mcp_ticketer/core/mappers.py +42 -30
  78. mcp_ticketer/core/milestone_manager.py +252 -0
  79. mcp_ticketer/core/models.py +566 -19
  80. mcp_ticketer/core/onepassword_secrets.py +379 -0
  81. mcp_ticketer/core/priority_matcher.py +463 -0
  82. mcp_ticketer/core/project_config.py +189 -49
  83. mcp_ticketer/core/project_utils.py +281 -0
  84. mcp_ticketer/core/project_validator.py +376 -0
  85. mcp_ticketer/core/registry.py +3 -3
  86. mcp_ticketer/core/session_state.py +176 -0
  87. mcp_ticketer/core/state_matcher.py +592 -0
  88. mcp_ticketer/core/url_parser.py +425 -0
  89. mcp_ticketer/core/validators.py +69 -0
  90. mcp_ticketer/defaults/ticket_instructions.md +644 -0
  91. mcp_ticketer/mcp/__init__.py +29 -1
  92. mcp_ticketer/mcp/__main__.py +60 -0
  93. mcp_ticketer/mcp/server/__init__.py +25 -0
  94. mcp_ticketer/mcp/server/__main__.py +60 -0
  95. mcp_ticketer/mcp/server/constants.py +58 -0
  96. mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
  97. mcp_ticketer/mcp/server/dto.py +195 -0
  98. mcp_ticketer/mcp/server/main.py +1343 -0
  99. mcp_ticketer/mcp/server/response_builder.py +206 -0
  100. mcp_ticketer/mcp/server/routing.py +723 -0
  101. mcp_ticketer/mcp/server/server_sdk.py +151 -0
  102. mcp_ticketer/mcp/server/tools/__init__.py +69 -0
  103. mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
  104. mcp_ticketer/mcp/server/tools/attachment_tools.py +224 -0
  105. mcp_ticketer/mcp/server/tools/bulk_tools.py +330 -0
  106. mcp_ticketer/mcp/server/tools/comment_tools.py +152 -0
  107. mcp_ticketer/mcp/server/tools/config_tools.py +1564 -0
  108. mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
  109. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +942 -0
  110. mcp_ticketer/mcp/server/tools/instruction_tools.py +295 -0
  111. mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
  112. mcp_ticketer/mcp/server/tools/milestone_tools.py +338 -0
  113. mcp_ticketer/mcp/server/tools/pr_tools.py +150 -0
  114. mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
  115. mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
  116. mcp_ticketer/mcp/server/tools/search_tools.py +318 -0
  117. mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
  118. mcp_ticketer/mcp/server/tools/ticket_tools.py +1413 -0
  119. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +364 -0
  120. mcp_ticketer/queue/__init__.py +1 -0
  121. mcp_ticketer/queue/health_monitor.py +168 -136
  122. mcp_ticketer/queue/manager.py +78 -63
  123. mcp_ticketer/queue/queue.py +108 -21
  124. mcp_ticketer/queue/run_worker.py +2 -2
  125. mcp_ticketer/queue/ticket_registry.py +213 -155
  126. mcp_ticketer/queue/worker.py +96 -58
  127. mcp_ticketer/utils/__init__.py +5 -0
  128. mcp_ticketer/utils/token_utils.py +246 -0
  129. mcp_ticketer-2.2.9.dist-info/METADATA +1396 -0
  130. mcp_ticketer-2.2.9.dist-info/RECORD +158 -0
  131. mcp_ticketer-2.2.9.dist-info/top_level.txt +2 -0
  132. py_mcp_installer/examples/phase3_demo.py +178 -0
  133. py_mcp_installer/scripts/manage_version.py +54 -0
  134. py_mcp_installer/setup.py +6 -0
  135. py_mcp_installer/src/py_mcp_installer/__init__.py +153 -0
  136. py_mcp_installer/src/py_mcp_installer/command_builder.py +445 -0
  137. py_mcp_installer/src/py_mcp_installer/config_manager.py +541 -0
  138. py_mcp_installer/src/py_mcp_installer/exceptions.py +243 -0
  139. py_mcp_installer/src/py_mcp_installer/installation_strategy.py +617 -0
  140. py_mcp_installer/src/py_mcp_installer/installer.py +656 -0
  141. py_mcp_installer/src/py_mcp_installer/mcp_inspector.py +750 -0
  142. py_mcp_installer/src/py_mcp_installer/platform_detector.py +451 -0
  143. py_mcp_installer/src/py_mcp_installer/platforms/__init__.py +26 -0
  144. py_mcp_installer/src/py_mcp_installer/platforms/claude_code.py +225 -0
  145. py_mcp_installer/src/py_mcp_installer/platforms/codex.py +181 -0
  146. py_mcp_installer/src/py_mcp_installer/platforms/cursor.py +191 -0
  147. py_mcp_installer/src/py_mcp_installer/types.py +222 -0
  148. py_mcp_installer/src/py_mcp_installer/utils.py +463 -0
  149. py_mcp_installer/tests/__init__.py +0 -0
  150. py_mcp_installer/tests/platforms/__init__.py +0 -0
  151. py_mcp_installer/tests/test_platform_detector.py +17 -0
  152. mcp_ticketer/adapters/github.py +0 -1354
  153. mcp_ticketer/adapters/jira.py +0 -1011
  154. mcp_ticketer/mcp/server.py +0 -1895
  155. mcp_ticketer-0.2.0.dist-info/METADATA +0 -414
  156. mcp_ticketer-0.2.0.dist-info/RECORD +0 -58
  157. mcp_ticketer-0.2.0.dist-info/top_level.txt +0 -1
  158. {mcp_ticketer-0.2.0.dist-info → mcp_ticketer-2.2.9.dist-info}/WHEEL +0 -0
  159. {mcp_ticketer-0.2.0.dist-info → mcp_ticketer-2.2.9.dist-info}/entry_points.txt +0 -0
  160. {mcp_ticketer-0.2.0.dist-info → mcp_ticketer-2.2.9.dist-info}/licenses/LICENSE +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()
@@ -0,0 +1,58 @@
1
+ """MCP server constants and configuration."""
2
+
3
+ # JSON-RPC Protocol
4
+ JSONRPC_VERSION = "2.0"
5
+ MCP_PROTOCOL_VERSION = "2024-11-05"
6
+
7
+ # Server Info
8
+ SERVER_NAME = "mcp-ticketer"
9
+ SERVER_VERSION = "0.3.2"
10
+
11
+ # Status Values
12
+ STATUS_COMPLETED = "completed"
13
+ STATUS_ERROR = "error"
14
+ STATUS_NOT_IMPLEMENTED = "not_implemented"
15
+
16
+ # Error Codes
17
+ ERROR_PARSE = -32700
18
+ ERROR_INVALID_REQUEST = -32600
19
+ ERROR_METHOD_NOT_FOUND = -32601
20
+ ERROR_INVALID_PARAMS = -32602
21
+ ERROR_INTERNAL = -32603
22
+
23
+ # Default Values
24
+ DEFAULT_LIMIT = 10
25
+ DEFAULT_OFFSET = 0
26
+ DEFAULT_PRIORITY = "medium"
27
+ DEFAULT_MAX_DEPTH = 3
28
+ DEFAULT_BASE_PATH = ".aitrackdown"
29
+
30
+ # Response Messages
31
+ MSG_TICKET_NOT_FOUND = "Ticket {ticket_id} not found"
32
+ MSG_UPDATE_FAILED = "Ticket {ticket_id} not found or update failed"
33
+ MSG_TRANSITION_FAILED = "Ticket {ticket_id} not found or transition failed"
34
+ MSG_EPIC_NOT_FOUND = "Epic {epic_id} not found"
35
+ MSG_MISSING_PARENT_ID = "Tasks must have a parent_id (issue identifier)"
36
+ MSG_UNKNOWN_OPERATION = "Unknown comment operation: {operation}"
37
+ MSG_UNKNOWN_METHOD = "Method not found: {method}"
38
+ MSG_INTERNAL_ERROR = "Internal error: {error}"
39
+ MSG_NO_TICKETS_PROVIDED = "No tickets provided for bulk creation"
40
+ MSG_NO_UPDATES_PROVIDED = "No updates provided for bulk operation"
41
+ MSG_MISSING_TITLE = "Ticket {index} missing required 'title' field"
42
+ MSG_MISSING_TICKET_ID = "Update {index} missing required 'ticket_id' field"
43
+ MSG_TICKET_ID_REQUIRED = "ticket_id is required"
44
+ MSG_PR_URL_REQUIRED = "pr_url is required"
45
+ MSG_ATTACHMENT_NOT_IMPLEMENTED = "Attachment functionality not yet implemented"
46
+ MSG_PR_NOT_SUPPORTED = "PR creation not supported for adapter: {adapter}"
47
+ MSG_PR_LINK_NOT_SUPPORTED = "PR linking not supported for adapter: {adapter}"
48
+ MSG_UNKNOWN_TOOL = "Unknown tool: {tool}"
49
+ MSG_GITHUB_CONFIG_REQUIRED = "GitHub owner and repo are required for Linear PR creation"
50
+
51
+ # Attachment Alternative Messages
52
+ ATTACHMENT_ALTERNATIVES = [
53
+ "Add file URLs in comments",
54
+ "Use external file storage",
55
+ ]
56
+ ATTACHMENT_NOT_IMPLEMENTED_REASON = (
57
+ "File attachments require adapter-specific implementation"
58
+ )
@@ -0,0 +1,175 @@
1
+ """Diagnostic helper for MCP error handling.
2
+
3
+ Provides quick diagnostic checks and error classification to help users
4
+ troubleshoot system configuration issues when MCP tools encounter errors.
5
+ """
6
+
7
+ import logging
8
+ from enum import Enum
9
+ from typing import Any
10
+
11
+ from ...core.exceptions import (
12
+ AuthenticationError,
13
+ ConfigurationError,
14
+ NetworkError,
15
+ NotFoundError,
16
+ PermissionError,
17
+ RateLimitError,
18
+ StateTransitionError,
19
+ TimeoutError,
20
+ ValidationError,
21
+ )
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ class ErrorSeverity(Enum):
27
+ """Classification of error severity for diagnostic suggestions."""
28
+
29
+ CRITICAL = "critical" # Always suggest diagnostics
30
+ MEDIUM = "medium" # Suggest if pattern detected
31
+ LOW = "low" # Never suggest diagnostics
32
+
33
+
34
+ # Map exception types to severity levels
35
+ ERROR_SEVERITY_MAP = {
36
+ AuthenticationError: ErrorSeverity.CRITICAL,
37
+ ConfigurationError: ErrorSeverity.CRITICAL,
38
+ NetworkError: ErrorSeverity.CRITICAL,
39
+ TimeoutError: ErrorSeverity.CRITICAL,
40
+ NotFoundError: ErrorSeverity.MEDIUM,
41
+ PermissionError: ErrorSeverity.MEDIUM,
42
+ RateLimitError: ErrorSeverity.MEDIUM,
43
+ ValidationError: ErrorSeverity.LOW,
44
+ StateTransitionError: ErrorSeverity.LOW,
45
+ }
46
+
47
+
48
+ def should_suggest_diagnostics(exception: Exception) -> bool:
49
+ """Determine if error response should include diagnostic suggestion.
50
+
51
+ Args:
52
+ exception: The exception that was raised
53
+
54
+ Returns:
55
+ True if diagnostics should be suggested
56
+
57
+ """
58
+ severity = get_error_severity(exception)
59
+ return severity in (ErrorSeverity.CRITICAL, ErrorSeverity.MEDIUM)
60
+
61
+
62
+ def get_error_severity(exception: Exception) -> ErrorSeverity:
63
+ """Get severity level for an exception.
64
+
65
+ Args:
66
+ exception: The exception to classify
67
+
68
+ Returns:
69
+ Error severity level
70
+
71
+ """
72
+ exception_type = type(exception)
73
+ return ERROR_SEVERITY_MAP.get(exception_type, ErrorSeverity.MEDIUM)
74
+
75
+
76
+ async def get_quick_diagnostic_info() -> dict[str, Any]:
77
+ """Get lightweight diagnostic info without running full test suite.
78
+
79
+ Performs fast checks (< 100ms) to provide immediate troubleshooting hints:
80
+ - Adapter configuration status
81
+ - Credential presence
82
+ - Queue system status
83
+
84
+ Returns:
85
+ Dictionary with quick diagnostic results
86
+
87
+ """
88
+ info: dict[str, Any] = {}
89
+
90
+ try:
91
+ # Check adapter configuration (fast file read)
92
+ from ...cli.utils import CommonPatterns
93
+
94
+ config = CommonPatterns.load_config()
95
+ adapters = config.get("adapters", {})
96
+
97
+ info["adapter_configured"] = len(adapters) > 0
98
+ info["configured_adapters"] = list(adapters.keys())
99
+ info["default_adapter"] = config.get("default_adapter")
100
+
101
+ except Exception as e:
102
+ logger.debug(f"Quick diagnostic config check failed: {e}")
103
+ info["adapter_configured"] = False
104
+ info["config_error"] = str(e)
105
+
106
+ try:
107
+ # Check queue system status (fast status check, no operations)
108
+ from ...queue.worker import Worker
109
+
110
+ worker = Worker()
111
+ info["queue_running"] = worker.running
112
+
113
+ except Exception as e:
114
+ logger.debug(f"Quick diagnostic queue check failed: {e}")
115
+ info["queue_running"] = False
116
+
117
+ try:
118
+ # Get version information
119
+ from ...__version__ import __version__
120
+
121
+ info["mcp_ticketer_version"] = __version__
122
+
123
+ except Exception as e:
124
+ logger.debug(f"Quick diagnostic version check failed: {e}")
125
+ info["mcp_ticketer_version"] = "unknown"
126
+
127
+ return info
128
+
129
+
130
+ def build_diagnostic_suggestion(
131
+ exception: Exception, quick_info: dict[str, Any] | None = None
132
+ ) -> dict[str, Any]:
133
+ """Build diagnostic suggestion dict for error response.
134
+
135
+ Args:
136
+ exception: The exception that occurred
137
+ quick_info: Optional quick diagnostic info (from get_quick_diagnostic_info())
138
+
139
+ Returns:
140
+ Diagnostic suggestion dictionary for inclusion in error response
141
+
142
+ """
143
+ severity = get_error_severity(exception)
144
+
145
+ suggestion: dict[str, Any] = {
146
+ "severity": severity.value,
147
+ "message": _get_severity_message(severity),
148
+ "recommendation": _get_recommendation(severity),
149
+ "command": "Use the 'system_diagnostics' MCP tool or CLI: mcp-ticketer doctor",
150
+ }
151
+
152
+ if quick_info:
153
+ suggestion["quick_checks"] = quick_info
154
+
155
+ return suggestion
156
+
157
+
158
+ def _get_severity_message(severity: ErrorSeverity) -> str:
159
+ """Get human-readable message for severity level."""
160
+ messages = {
161
+ ErrorSeverity.CRITICAL: "This appears to be a system configuration issue",
162
+ ErrorSeverity.MEDIUM: "This may indicate a configuration or permission issue",
163
+ ErrorSeverity.LOW: "This is a validation or input error",
164
+ }
165
+ return messages.get(severity, "An error occurred")
166
+
167
+
168
+ def _get_recommendation(severity: ErrorSeverity) -> str:
169
+ """Get recommendation text for severity level."""
170
+ recommendations = {
171
+ ErrorSeverity.CRITICAL: "Run system diagnostics to identify the problem",
172
+ ErrorSeverity.MEDIUM: "Consider running diagnostics if the issue persists",
173
+ ErrorSeverity.LOW: "Check your input and try again",
174
+ }
175
+ return recommendations.get(severity, "Review the error message")
@@ -0,0 +1,195 @@
1
+ """Data Transfer Objects for MCP requests and responses."""
2
+
3
+ from typing import Any
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ # Request DTOs
9
+ class CreateTicketRequest(BaseModel):
10
+ """Request to create a ticket."""
11
+
12
+ title: str = Field(..., min_length=1, description="Ticket title")
13
+ description: str | None = Field(None, description="Ticket description")
14
+ priority: str = Field("medium", description="Ticket priority")
15
+ tags: list[str] = Field(default_factory=list, description="Ticket tags")
16
+ assignee: str | None = Field(None, description="Ticket assignee")
17
+
18
+
19
+ class CreateEpicRequest(BaseModel):
20
+ """Request to create an epic."""
21
+
22
+ title: str = Field(..., min_length=1, description="Epic title")
23
+ description: str | None = Field(None, description="Epic description")
24
+ child_issues: list[str] = Field(default_factory=list, description="Child issue IDs")
25
+ target_date: str | None = Field(None, description="Target completion date")
26
+ lead_id: str | None = Field(None, description="Epic lead/owner ID")
27
+
28
+
29
+ class CreateIssueRequest(BaseModel):
30
+ """Request to create an issue."""
31
+
32
+ title: str = Field(..., min_length=1, description="Issue title")
33
+ description: str | None = Field(None, description="Issue description")
34
+ epic_id: str | None = Field(None, description="Parent epic ID")
35
+ priority: str = Field("medium", description="Issue priority")
36
+ assignee: str | None = Field(None, description="Issue assignee")
37
+ tags: list[str] = Field(default_factory=list, description="Issue tags")
38
+ estimated_hours: float | None = Field(
39
+ None, description="Estimated hours to complete"
40
+ )
41
+
42
+
43
+ class CreateTaskRequest(BaseModel):
44
+ """Request to create a task."""
45
+
46
+ title: str = Field(..., min_length=1, description="Task title")
47
+ parent_id: str = Field(..., description="Parent issue ID")
48
+ description: str | None = Field(None, description="Task description")
49
+ priority: str = Field("medium", description="Task priority")
50
+ assignee: str | None = Field(None, description="Task assignee")
51
+ tags: list[str] = Field(default_factory=list, description="Task tags")
52
+ estimated_hours: float | None = Field(
53
+ None, description="Estimated hours to complete"
54
+ )
55
+
56
+
57
+ class ReadTicketRequest(BaseModel):
58
+ """Request to read a ticket."""
59
+
60
+ ticket_id: str = Field(..., description="Ticket ID to read")
61
+
62
+
63
+ class UpdateTicketRequest(BaseModel):
64
+ """Request to update a ticket."""
65
+
66
+ ticket_id: str = Field(..., description="Ticket ID to update")
67
+ updates: dict[str, Any] = Field(..., description="Fields to update")
68
+
69
+
70
+ class TransitionRequest(BaseModel):
71
+ """Request to transition ticket state."""
72
+
73
+ ticket_id: str = Field(..., description="Ticket ID")
74
+ target_state: str = Field(..., description="Target state")
75
+
76
+
77
+ class SearchRequest(BaseModel):
78
+ """Request to search tickets."""
79
+
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
+ limit: int = Field(10, description="Maximum number of results")
86
+
87
+
88
+ class ListRequest(BaseModel):
89
+ """Request to list tickets."""
90
+
91
+ limit: int = Field(10, description="Maximum number of tickets to return")
92
+ offset: int = Field(0, description="Number of tickets to skip")
93
+ filters: dict[str, Any] | None = Field(None, description="Additional filters")
94
+
95
+
96
+ class DeleteTicketRequest(BaseModel):
97
+ """Request to delete a ticket."""
98
+
99
+ ticket_id: str = Field(..., description="Ticket ID to delete")
100
+
101
+
102
+ class CommentRequest(BaseModel):
103
+ """Request for comment operations."""
104
+
105
+ operation: str = Field("add", description="Operation: 'add' or 'list'")
106
+ ticket_id: str = Field(..., description="Ticket ID")
107
+ content: str | None = Field(None, description="Comment content (for add)")
108
+ author: str | None = Field(None, description="Comment author (for add)")
109
+ limit: int = Field(10, description="Max comments to return (for list)")
110
+ offset: int = Field(0, description="Number of comments to skip (for list)")
111
+
112
+
113
+ class CreatePRRequest(BaseModel):
114
+ """Request to create a pull request."""
115
+
116
+ ticket_id: str = Field(..., description="Ticket ID")
117
+ base_branch: str = Field("main", description="Base branch")
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
+ draft: bool = Field(False, description="Create as draft PR")
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
+
125
+
126
+ class LinkPRRequest(BaseModel):
127
+ """Request to link an existing PR to a ticket."""
128
+
129
+ ticket_id: str = Field(..., description="Ticket ID")
130
+ pr_url: str = Field(..., description="Pull request URL")
131
+
132
+
133
+ class EpicListRequest(BaseModel):
134
+ """Request to list epics."""
135
+
136
+ limit: int = Field(10, description="Maximum number of epics to return")
137
+ offset: int = Field(0, description="Number of epics to skip")
138
+
139
+
140
+ class EpicIssuesRequest(BaseModel):
141
+ """Request to list issues in an epic."""
142
+
143
+ epic_id: str = Field(..., description="Epic ID")
144
+
145
+
146
+ class IssueTasksRequest(BaseModel):
147
+ """Request to list tasks in an issue."""
148
+
149
+ issue_id: str = Field(..., description="Issue ID")
150
+
151
+
152
+ class HierarchyTreeRequest(BaseModel):
153
+ """Request to get hierarchy tree."""
154
+
155
+ epic_id: str | None = Field(None, description="Specific epic ID (optional)")
156
+ max_depth: int = Field(3, description="Maximum depth of tree")
157
+ limit: int = Field(10, description="Max epics to return (if no epic_id)")
158
+
159
+
160
+ class BulkCreateRequest(BaseModel):
161
+ """Request to bulk create tickets."""
162
+
163
+ tickets: list[dict[str, Any]] = Field(..., description="List of ticket data")
164
+
165
+
166
+ class BulkUpdateRequest(BaseModel):
167
+ """Request to bulk update tickets."""
168
+
169
+ updates: list[dict[str, Any]] = Field(..., description="List of update data")
170
+
171
+
172
+ class SearchHierarchyRequest(BaseModel):
173
+ """Request to search with hierarchy context."""
174
+
175
+ query: str = Field("", description="Search query")
176
+ state: str | None = Field(None, description="Filter by state")
177
+ priority: str | None = Field(None, description="Filter by priority")
178
+ include_children: bool = Field(True, description="Include child tickets")
179
+ include_parents: bool = Field(True, description="Include parent tickets")
180
+ limit: int = Field(50, description="Maximum number of results")
181
+
182
+
183
+ class AttachRequest(BaseModel):
184
+ """Request to attach file to ticket."""
185
+
186
+ ticket_id: str = Field(..., description="Ticket ID")
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
+
191
+
192
+ class ListAttachmentsRequest(BaseModel):
193
+ """Request to list ticket attachments."""
194
+
195
+ ticket_id: str = Field(..., description="Ticket ID")