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.
- mcp_ticketer/__init__.py +10 -10
- mcp_ticketer/__version__.py +3 -3
- mcp_ticketer/_version_scm.py +1 -0
- mcp_ticketer/adapters/__init__.py +2 -0
- mcp_ticketer/adapters/aitrackdown.py +930 -52
- mcp_ticketer/adapters/asana/__init__.py +15 -0
- mcp_ticketer/adapters/asana/adapter.py +1537 -0
- mcp_ticketer/adapters/asana/client.py +292 -0
- mcp_ticketer/adapters/asana/mappers.py +348 -0
- mcp_ticketer/adapters/asana/types.py +146 -0
- mcp_ticketer/adapters/github/__init__.py +26 -0
- mcp_ticketer/adapters/github/adapter.py +3229 -0
- mcp_ticketer/adapters/github/client.py +335 -0
- mcp_ticketer/adapters/github/mappers.py +797 -0
- mcp_ticketer/adapters/github/queries.py +692 -0
- mcp_ticketer/adapters/github/types.py +460 -0
- mcp_ticketer/adapters/hybrid.py +58 -16
- mcp_ticketer/adapters/jira/__init__.py +35 -0
- mcp_ticketer/adapters/jira/adapter.py +1351 -0
- mcp_ticketer/adapters/jira/client.py +271 -0
- mcp_ticketer/adapters/jira/mappers.py +246 -0
- mcp_ticketer/adapters/jira/queries.py +216 -0
- mcp_ticketer/adapters/jira/types.py +304 -0
- mcp_ticketer/adapters/linear/__init__.py +1 -1
- mcp_ticketer/adapters/linear/adapter.py +3810 -462
- mcp_ticketer/adapters/linear/client.py +312 -69
- mcp_ticketer/adapters/linear/mappers.py +305 -85
- mcp_ticketer/adapters/linear/queries.py +317 -17
- mcp_ticketer/adapters/linear/types.py +187 -64
- mcp_ticketer/adapters/linear.py +2 -2
- mcp_ticketer/analysis/__init__.py +56 -0
- mcp_ticketer/analysis/dependency_graph.py +255 -0
- mcp_ticketer/analysis/health_assessment.py +304 -0
- mcp_ticketer/analysis/orphaned.py +218 -0
- mcp_ticketer/analysis/project_status.py +594 -0
- mcp_ticketer/analysis/similarity.py +224 -0
- mcp_ticketer/analysis/staleness.py +266 -0
- mcp_ticketer/automation/__init__.py +11 -0
- mcp_ticketer/automation/project_updates.py +378 -0
- mcp_ticketer/cache/memory.py +9 -8
- mcp_ticketer/cli/adapter_diagnostics.py +421 -0
- mcp_ticketer/cli/auggie_configure.py +116 -15
- mcp_ticketer/cli/codex_configure.py +274 -82
- mcp_ticketer/cli/configure.py +1323 -151
- mcp_ticketer/cli/cursor_configure.py +314 -0
- mcp_ticketer/cli/diagnostics.py +209 -114
- mcp_ticketer/cli/discover.py +297 -26
- mcp_ticketer/cli/gemini_configure.py +119 -26
- mcp_ticketer/cli/init_command.py +880 -0
- mcp_ticketer/cli/install_mcp_server.py +418 -0
- mcp_ticketer/cli/instruction_commands.py +435 -0
- mcp_ticketer/cli/linear_commands.py +256 -130
- mcp_ticketer/cli/main.py +140 -1284
- mcp_ticketer/cli/mcp_configure.py +1013 -100
- mcp_ticketer/cli/mcp_server_commands.py +415 -0
- mcp_ticketer/cli/migrate_config.py +12 -8
- mcp_ticketer/cli/platform_commands.py +123 -0
- mcp_ticketer/cli/platform_detection.py +477 -0
- mcp_ticketer/cli/platform_installer.py +545 -0
- mcp_ticketer/cli/project_update_commands.py +350 -0
- mcp_ticketer/cli/python_detection.py +126 -0
- mcp_ticketer/cli/queue_commands.py +15 -15
- mcp_ticketer/cli/setup_command.py +794 -0
- mcp_ticketer/cli/simple_health.py +84 -59
- mcp_ticketer/cli/ticket_commands.py +1375 -0
- mcp_ticketer/cli/update_checker.py +313 -0
- mcp_ticketer/cli/utils.py +195 -72
- mcp_ticketer/core/__init__.py +64 -1
- mcp_ticketer/core/adapter.py +618 -18
- mcp_ticketer/core/config.py +77 -68
- mcp_ticketer/core/env_discovery.py +75 -16
- mcp_ticketer/core/env_loader.py +121 -97
- mcp_ticketer/core/exceptions.py +32 -24
- mcp_ticketer/core/http_client.py +26 -26
- mcp_ticketer/core/instructions.py +405 -0
- mcp_ticketer/core/label_manager.py +732 -0
- mcp_ticketer/core/mappers.py +42 -30
- mcp_ticketer/core/milestone_manager.py +252 -0
- mcp_ticketer/core/models.py +566 -19
- mcp_ticketer/core/onepassword_secrets.py +379 -0
- mcp_ticketer/core/priority_matcher.py +463 -0
- mcp_ticketer/core/project_config.py +189 -49
- mcp_ticketer/core/project_utils.py +281 -0
- mcp_ticketer/core/project_validator.py +376 -0
- mcp_ticketer/core/registry.py +3 -3
- mcp_ticketer/core/session_state.py +176 -0
- mcp_ticketer/core/state_matcher.py +592 -0
- mcp_ticketer/core/url_parser.py +425 -0
- mcp_ticketer/core/validators.py +69 -0
- 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/server/constants.py +58 -0
- mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
- mcp_ticketer/mcp/server/dto.py +195 -0
- mcp_ticketer/mcp/server/main.py +1343 -0
- mcp_ticketer/mcp/server/response_builder.py +206 -0
- mcp_ticketer/mcp/server/routing.py +723 -0
- mcp_ticketer/mcp/server/server_sdk.py +151 -0
- mcp_ticketer/mcp/server/tools/__init__.py +69 -0
- mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
- mcp_ticketer/mcp/server/tools/attachment_tools.py +224 -0
- mcp_ticketer/mcp/server/tools/bulk_tools.py +330 -0
- mcp_ticketer/mcp/server/tools/comment_tools.py +152 -0
- mcp_ticketer/mcp/server/tools/config_tools.py +1564 -0
- mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
- mcp_ticketer/mcp/server/tools/hierarchy_tools.py +942 -0
- mcp_ticketer/mcp/server/tools/instruction_tools.py +295 -0
- mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
- mcp_ticketer/mcp/server/tools/milestone_tools.py +338 -0
- mcp_ticketer/mcp/server/tools/pr_tools.py +150 -0
- mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
- mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
- mcp_ticketer/mcp/server/tools/search_tools.py +318 -0
- mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
- mcp_ticketer/mcp/server/tools/ticket_tools.py +1413 -0
- mcp_ticketer/mcp/server/tools/user_ticket_tools.py +364 -0
- mcp_ticketer/queue/__init__.py +1 -0
- mcp_ticketer/queue/health_monitor.py +168 -136
- mcp_ticketer/queue/manager.py +78 -63
- mcp_ticketer/queue/queue.py +108 -21
- mcp_ticketer/queue/run_worker.py +2 -2
- mcp_ticketer/queue/ticket_registry.py +213 -155
- mcp_ticketer/queue/worker.py +96 -58
- mcp_ticketer/utils/__init__.py +5 -0
- mcp_ticketer/utils/token_utils.py +246 -0
- mcp_ticketer-2.2.9.dist-info/METADATA +1396 -0
- mcp_ticketer-2.2.9.dist-info/RECORD +158 -0
- mcp_ticketer-2.2.9.dist-info/top_level.txt +2 -0
- py_mcp_installer/examples/phase3_demo.py +178 -0
- py_mcp_installer/scripts/manage_version.py +54 -0
- py_mcp_installer/setup.py +6 -0
- py_mcp_installer/src/py_mcp_installer/__init__.py +153 -0
- py_mcp_installer/src/py_mcp_installer/command_builder.py +445 -0
- py_mcp_installer/src/py_mcp_installer/config_manager.py +541 -0
- py_mcp_installer/src/py_mcp_installer/exceptions.py +243 -0
- py_mcp_installer/src/py_mcp_installer/installation_strategy.py +617 -0
- py_mcp_installer/src/py_mcp_installer/installer.py +656 -0
- py_mcp_installer/src/py_mcp_installer/mcp_inspector.py +750 -0
- py_mcp_installer/src/py_mcp_installer/platform_detector.py +451 -0
- py_mcp_installer/src/py_mcp_installer/platforms/__init__.py +26 -0
- py_mcp_installer/src/py_mcp_installer/platforms/claude_code.py +225 -0
- py_mcp_installer/src/py_mcp_installer/platforms/codex.py +181 -0
- py_mcp_installer/src/py_mcp_installer/platforms/cursor.py +191 -0
- py_mcp_installer/src/py_mcp_installer/types.py +222 -0
- py_mcp_installer/src/py_mcp_installer/utils.py +463 -0
- py_mcp_installer/tests/__init__.py +0 -0
- py_mcp_installer/tests/platforms/__init__.py +0 -0
- py_mcp_installer/tests/test_platform_detector.py +17 -0
- mcp_ticketer/adapters/github.py +0 -1354
- mcp_ticketer/adapters/jira.py +0 -1011
- mcp_ticketer/mcp/server.py +0 -1895
- mcp_ticketer-0.2.0.dist-info/METADATA +0 -414
- mcp_ticketer-0.2.0.dist-info/RECORD +0 -58
- mcp_ticketer-0.2.0.dist-info/top_level.txt +0 -1
- {mcp_ticketer-0.2.0.dist-info → mcp_ticketer-2.2.9.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.2.0.dist-info → mcp_ticketer-2.2.9.dist-info}/entry_points.txt +0 -0
- {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")
|