mcp-ticketer 0.1.30__py3-none-any.whl → 1.2.11__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/__init__.py +10 -10
- mcp_ticketer/__version__.py +3 -3
- mcp_ticketer/adapters/__init__.py +2 -0
- mcp_ticketer/adapters/aitrackdown.py +796 -46
- mcp_ticketer/adapters/asana/__init__.py +15 -0
- mcp_ticketer/adapters/asana/adapter.py +1416 -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.py +879 -129
- mcp_ticketer/adapters/hybrid.py +11 -11
- mcp_ticketer/adapters/jira.py +973 -73
- mcp_ticketer/adapters/linear/__init__.py +24 -0
- mcp_ticketer/adapters/linear/adapter.py +2732 -0
- mcp_ticketer/adapters/linear/client.py +344 -0
- mcp_ticketer/adapters/linear/mappers.py +420 -0
- mcp_ticketer/adapters/linear/queries.py +479 -0
- mcp_ticketer/adapters/linear/types.py +360 -0
- mcp_ticketer/adapters/linear.py +10 -2315
- mcp_ticketer/analysis/__init__.py +23 -0
- mcp_ticketer/analysis/orphaned.py +218 -0
- mcp_ticketer/analysis/similarity.py +224 -0
- mcp_ticketer/analysis/staleness.py +266 -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 +888 -151
- mcp_ticketer/cli/diagnostics.py +400 -157
- 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/instruction_commands.py +435 -0
- mcp_ticketer/cli/linear_commands.py +616 -0
- mcp_ticketer/cli/main.py +203 -1165
- mcp_ticketer/cli/mcp_configure.py +474 -90
- 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 +418 -0
- mcp_ticketer/cli/platform_installer.py +513 -0
- mcp_ticketer/cli/python_detection.py +126 -0
- mcp_ticketer/cli/queue_commands.py +15 -15
- mcp_ticketer/cli/setup_command.py +639 -0
- mcp_ticketer/cli/simple_health.py +90 -65
- mcp_ticketer/cli/ticket_commands.py +1013 -0
- mcp_ticketer/cli/update_checker.py +313 -0
- mcp_ticketer/cli/utils.py +114 -66
- mcp_ticketer/core/__init__.py +24 -1
- mcp_ticketer/core/adapter.py +250 -16
- mcp_ticketer/core/config.py +145 -37
- mcp_ticketer/core/env_discovery.py +101 -22
- mcp_ticketer/core/env_loader.py +349 -0
- mcp_ticketer/core/exceptions.py +160 -0
- 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/models.py +280 -28
- mcp_ticketer/core/onepassword_secrets.py +379 -0
- mcp_ticketer/core/project_config.py +183 -49
- mcp_ticketer/core/registry.py +3 -3
- mcp_ticketer/core/session_state.py +171 -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 +655 -0
- mcp_ticketer/mcp/server/server_sdk.py +151 -0
- mcp_ticketer/mcp/server/tools/__init__.py +56 -0
- mcp_ticketer/mcp/server/tools/analysis_tools.py +495 -0
- mcp_ticketer/mcp/server/tools/attachment_tools.py +226 -0
- mcp_ticketer/mcp/server/tools/bulk_tools.py +273 -0
- mcp_ticketer/mcp/server/tools/comment_tools.py +152 -0
- mcp_ticketer/mcp/server/tools/config_tools.py +1439 -0
- mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
- mcp_ticketer/mcp/server/tools/hierarchy_tools.py +921 -0
- mcp_ticketer/mcp/server/tools/instruction_tools.py +300 -0
- mcp_ticketer/mcp/server/tools/label_tools.py +948 -0
- mcp_ticketer/mcp/server/tools/pr_tools.py +152 -0
- mcp_ticketer/mcp/server/tools/search_tools.py +215 -0
- mcp_ticketer/mcp/server/tools/session_tools.py +170 -0
- mcp_ticketer/mcp/server/tools/ticket_tools.py +1268 -0
- mcp_ticketer/mcp/server/tools/user_ticket_tools.py +547 -0
- mcp_ticketer/queue/__init__.py +1 -0
- mcp_ticketer/queue/health_monitor.py +168 -136
- mcp_ticketer/queue/manager.py +95 -25
- mcp_ticketer/queue/queue.py +40 -21
- mcp_ticketer/queue/run_worker.py +6 -1
- mcp_ticketer/queue/ticket_registry.py +213 -155
- mcp_ticketer/queue/worker.py +109 -49
- mcp_ticketer-1.2.11.dist-info/METADATA +792 -0
- mcp_ticketer-1.2.11.dist-info/RECORD +110 -0
- mcp_ticketer/mcp/server.py +0 -1895
- mcp_ticketer-0.1.30.dist-info/METADATA +0 -413
- mcp_ticketer-0.1.30.dist-info/RECORD +0 -49
- {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""FastMCP-based MCP server implementation.
|
|
2
|
+
|
|
3
|
+
This module implements the MCP server using the official FastMCP SDK,
|
|
4
|
+
replacing the custom JSON-RPC implementation. It provides a cleaner,
|
|
5
|
+
more maintainable approach with automatic schema generation and
|
|
6
|
+
better error handling.
|
|
7
|
+
|
|
8
|
+
The server manages a global adapter instance that is configured at
|
|
9
|
+
startup and used by all tool implementations.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from mcp.server.fastmcp import FastMCP
|
|
16
|
+
|
|
17
|
+
from ...core.adapter import BaseAdapter
|
|
18
|
+
from ...core.registry import AdapterRegistry
|
|
19
|
+
|
|
20
|
+
# Initialize FastMCP server
|
|
21
|
+
mcp = FastMCP("mcp-ticketer")
|
|
22
|
+
|
|
23
|
+
# Global adapter instance
|
|
24
|
+
_adapter: BaseAdapter | None = None
|
|
25
|
+
|
|
26
|
+
# Global router instance (optional, for multi-platform support)
|
|
27
|
+
_router: Any | None = None
|
|
28
|
+
|
|
29
|
+
# Configure logging
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def configure_adapter(adapter_type: str, config: dict[str, Any]) -> None:
|
|
34
|
+
"""Configure the global adapter instance.
|
|
35
|
+
|
|
36
|
+
This must be called before starting the server to initialize the
|
|
37
|
+
adapter that will handle all ticket operations.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
adapter_type: Type of adapter to create (e.g., "linear", "jira", "github")
|
|
41
|
+
config: Configuration dictionary for the adapter
|
|
42
|
+
|
|
43
|
+
Raises:
|
|
44
|
+
ValueError: If adapter type is not registered
|
|
45
|
+
RuntimeError: If adapter configuration fails
|
|
46
|
+
|
|
47
|
+
"""
|
|
48
|
+
global _adapter
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
# Get adapter from registry
|
|
52
|
+
_adapter = AdapterRegistry.get_adapter(adapter_type, config)
|
|
53
|
+
logger.info(f"Configured {adapter_type} adapter for MCP server")
|
|
54
|
+
except Exception as e:
|
|
55
|
+
logger.error(f"Failed to configure adapter: {e}")
|
|
56
|
+
raise RuntimeError(f"Adapter configuration failed: {e}") from e
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def get_adapter() -> BaseAdapter:
|
|
60
|
+
"""Get the configured adapter instance.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
The global adapter instance
|
|
64
|
+
|
|
65
|
+
Raises:
|
|
66
|
+
RuntimeError: If adapter has not been configured
|
|
67
|
+
|
|
68
|
+
"""
|
|
69
|
+
if _adapter is None:
|
|
70
|
+
raise RuntimeError(
|
|
71
|
+
"Adapter not configured. Call configure_adapter() before starting server."
|
|
72
|
+
)
|
|
73
|
+
return _adapter
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def configure_router(
|
|
77
|
+
default_adapter: str, adapter_configs: dict[str, dict[str, Any]]
|
|
78
|
+
) -> None:
|
|
79
|
+
"""Configure multi-platform routing support (optional).
|
|
80
|
+
|
|
81
|
+
This enables URL-based ticket access across multiple platforms in a
|
|
82
|
+
single MCP session. When configured, tools will use the router to
|
|
83
|
+
automatically detect the platform from URLs.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
default_adapter: Default adapter for plain IDs (e.g., "linear")
|
|
87
|
+
adapter_configs: Configuration for each adapter
|
|
88
|
+
Example: {
|
|
89
|
+
"linear": {"api_key": "...", "team_id": "..."},
|
|
90
|
+
"github": {"token": "...", "owner": "...", "repo": "..."}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
Raises:
|
|
94
|
+
RuntimeError: If router configuration fails
|
|
95
|
+
|
|
96
|
+
"""
|
|
97
|
+
global _router
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
from .routing import TicketRouter
|
|
101
|
+
|
|
102
|
+
_router = TicketRouter(
|
|
103
|
+
default_adapter=default_adapter, adapter_configs=adapter_configs
|
|
104
|
+
)
|
|
105
|
+
logger.info(f"Configured multi-platform router with default: {default_adapter}")
|
|
106
|
+
except Exception as e:
|
|
107
|
+
logger.error(f"Failed to configure router: {e}")
|
|
108
|
+
raise RuntimeError(f"Router configuration failed: {e}") from e
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def get_router() -> Any:
|
|
112
|
+
"""Get the configured router instance (if available).
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
The global router instance, or None if not configured
|
|
116
|
+
|
|
117
|
+
"""
|
|
118
|
+
return _router
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def has_router() -> bool:
|
|
122
|
+
"""Check if multi-platform router is configured.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
True if router is available, False otherwise
|
|
126
|
+
|
|
127
|
+
"""
|
|
128
|
+
return _router is not None
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# Import all tool modules to register them with FastMCP
|
|
132
|
+
# These imports must come after mcp is initialized but before main()
|
|
133
|
+
from . import tools # noqa: E402, F401
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def main() -> None:
|
|
137
|
+
"""Run the FastMCP server.
|
|
138
|
+
|
|
139
|
+
This function starts the server using stdio transport for
|
|
140
|
+
JSON-RPC communication with Claude Desktop/Code.
|
|
141
|
+
|
|
142
|
+
The adapter must be configured via configure_adapter() before
|
|
143
|
+
calling this function.
|
|
144
|
+
|
|
145
|
+
"""
|
|
146
|
+
# Run the server with stdio transport
|
|
147
|
+
mcp.run(transport="stdio")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
if __name__ == "__main__":
|
|
151
|
+
main()
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""MCP tool modules for ticket operations.
|
|
2
|
+
|
|
3
|
+
This package contains all FastMCP tool implementations organized by
|
|
4
|
+
functional area. Tools are automatically registered with the FastMCP
|
|
5
|
+
server when imported.
|
|
6
|
+
|
|
7
|
+
Modules:
|
|
8
|
+
ticket_tools: Basic CRUD operations for tickets
|
|
9
|
+
hierarchy_tools: Epic/Issue/Task hierarchy management
|
|
10
|
+
search_tools: Search and query operations
|
|
11
|
+
bulk_tools: Bulk create and update operations
|
|
12
|
+
comment_tools: Comment management
|
|
13
|
+
pr_tools: Pull request integration
|
|
14
|
+
attachment_tools: File attachment handling
|
|
15
|
+
instruction_tools: Ticket instructions management
|
|
16
|
+
config_tools: Configuration management (adapter, project, user settings)
|
|
17
|
+
session_tools: Session tracking and ticket association management
|
|
18
|
+
user_ticket_tools: User-specific ticket operations (my tickets, transitions)
|
|
19
|
+
analysis_tools: Ticket analysis and cleanup tools (similar, stale, orphaned)
|
|
20
|
+
label_tools: Label management, normalization, deduplication, and cleanup
|
|
21
|
+
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
# Import all tool modules to register them with FastMCP
|
|
25
|
+
# Order matters - import core functionality first
|
|
26
|
+
from . import (
|
|
27
|
+
analysis_tools, # noqa: F401
|
|
28
|
+
attachment_tools, # noqa: F401
|
|
29
|
+
bulk_tools, # noqa: F401
|
|
30
|
+
comment_tools, # noqa: F401
|
|
31
|
+
config_tools, # noqa: F401
|
|
32
|
+
hierarchy_tools, # noqa: F401
|
|
33
|
+
instruction_tools, # noqa: F401
|
|
34
|
+
label_tools, # noqa: F401
|
|
35
|
+
pr_tools, # noqa: F401
|
|
36
|
+
search_tools, # noqa: F401
|
|
37
|
+
session_tools, # noqa: F401
|
|
38
|
+
ticket_tools, # noqa: F401
|
|
39
|
+
user_ticket_tools, # noqa: F401
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
"analysis_tools",
|
|
44
|
+
"attachment_tools",
|
|
45
|
+
"bulk_tools",
|
|
46
|
+
"comment_tools",
|
|
47
|
+
"config_tools",
|
|
48
|
+
"hierarchy_tools",
|
|
49
|
+
"instruction_tools",
|
|
50
|
+
"label_tools",
|
|
51
|
+
"pr_tools",
|
|
52
|
+
"search_tools",
|
|
53
|
+
"session_tools",
|
|
54
|
+
"ticket_tools",
|
|
55
|
+
"user_ticket_tools",
|
|
56
|
+
]
|
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
"""MCP tools for ticket analysis and cleanup.
|
|
2
|
+
|
|
3
|
+
This module provides PM-focused tools to help maintain ticket health:
|
|
4
|
+
- ticket_find_similar: Find duplicate or related tickets
|
|
5
|
+
- ticket_find_stale: Identify old, inactive tickets
|
|
6
|
+
- ticket_find_orphaned: Find tickets without hierarchy
|
|
7
|
+
- ticket_cleanup_report: Generate comprehensive cleanup report
|
|
8
|
+
|
|
9
|
+
These tools help product managers maintain development practices and
|
|
10
|
+
identify tickets that need attention.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
# Try to import analysis dependencies (optional)
|
|
18
|
+
try:
|
|
19
|
+
from ....analysis.orphaned import OrphanedTicketDetector
|
|
20
|
+
from ....analysis.similarity import TicketSimilarityAnalyzer
|
|
21
|
+
from ....analysis.staleness import StaleTicketDetector
|
|
22
|
+
|
|
23
|
+
ANALYSIS_AVAILABLE = True
|
|
24
|
+
except ImportError:
|
|
25
|
+
ANALYSIS_AVAILABLE = False
|
|
26
|
+
# Define placeholder classes for type hints
|
|
27
|
+
OrphanedTicketDetector = None # type: ignore
|
|
28
|
+
TicketSimilarityAnalyzer = None # type: ignore
|
|
29
|
+
StaleTicketDetector = None # type: ignore
|
|
30
|
+
|
|
31
|
+
from ....core.models import SearchQuery, TicketState
|
|
32
|
+
from ..server_sdk import get_adapter, mcp
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@mcp.tool()
|
|
38
|
+
async def ticket_find_similar(
|
|
39
|
+
ticket_id: str | None = None,
|
|
40
|
+
threshold: float = 0.75,
|
|
41
|
+
limit: int = 10,
|
|
42
|
+
) -> dict[str, Any]:
|
|
43
|
+
"""Find similar tickets to detect duplicates.
|
|
44
|
+
|
|
45
|
+
Uses TF-IDF and cosine similarity to find tickets with similar
|
|
46
|
+
titles and descriptions. Useful for identifying duplicate tickets
|
|
47
|
+
or related work that should be linked.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
ticket_id: Find similar tickets to this one (if None, find all similar pairs)
|
|
51
|
+
threshold: Similarity threshold 0.0-1.0 (default: 0.75)
|
|
52
|
+
limit: Maximum number of results (default: 10)
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
List of similar ticket pairs with similarity scores and recommended actions
|
|
56
|
+
|
|
57
|
+
Example:
|
|
58
|
+
# Find tickets similar to a specific ticket
|
|
59
|
+
result = await ticket_find_similar(ticket_id="TICKET-123", threshold=0.8)
|
|
60
|
+
|
|
61
|
+
# Find all similar pairs in the system
|
|
62
|
+
result = await ticket_find_similar(limit=20)
|
|
63
|
+
|
|
64
|
+
"""
|
|
65
|
+
if not ANALYSIS_AVAILABLE:
|
|
66
|
+
return {
|
|
67
|
+
"status": "error",
|
|
68
|
+
"error": "Analysis features not available",
|
|
69
|
+
"message": "Install analysis dependencies with: pip install mcp-ticketer[analysis]",
|
|
70
|
+
"required_packages": [
|
|
71
|
+
"scikit-learn>=1.3.0",
|
|
72
|
+
"rapidfuzz>=3.0.0",
|
|
73
|
+
"numpy>=1.24.0",
|
|
74
|
+
],
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
adapter = get_adapter()
|
|
79
|
+
|
|
80
|
+
# Validate threshold
|
|
81
|
+
if threshold < 0.0 or threshold > 1.0:
|
|
82
|
+
return {
|
|
83
|
+
"status": "error",
|
|
84
|
+
"error": "threshold must be between 0.0 and 1.0",
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
# Fetch tickets
|
|
88
|
+
if ticket_id:
|
|
89
|
+
try:
|
|
90
|
+
target = await adapter.read(ticket_id)
|
|
91
|
+
if not target:
|
|
92
|
+
return {
|
|
93
|
+
"status": "error",
|
|
94
|
+
"error": f"Ticket {ticket_id} not found",
|
|
95
|
+
}
|
|
96
|
+
except Exception as e:
|
|
97
|
+
return {
|
|
98
|
+
"status": "error",
|
|
99
|
+
"error": f"Failed to read ticket {ticket_id}: {str(e)}",
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
# Fetch more tickets for comparison
|
|
103
|
+
tickets = await adapter.list(limit=100)
|
|
104
|
+
else:
|
|
105
|
+
target = None
|
|
106
|
+
tickets = await adapter.list(limit=500) # Larger for pairwise analysis
|
|
107
|
+
|
|
108
|
+
if len(tickets) < 2:
|
|
109
|
+
return {
|
|
110
|
+
"status": "completed",
|
|
111
|
+
"similar_tickets": [],
|
|
112
|
+
"count": 0,
|
|
113
|
+
"message": "Not enough tickets to compare (need at least 2)",
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
# Analyze similarity
|
|
117
|
+
analyzer = TicketSimilarityAnalyzer(threshold=threshold)
|
|
118
|
+
results = analyzer.find_similar_tickets(tickets, target, limit)
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
"status": "completed",
|
|
122
|
+
"similar_tickets": [r.model_dump() for r in results],
|
|
123
|
+
"count": len(results),
|
|
124
|
+
"threshold": threshold,
|
|
125
|
+
"tickets_analyzed": len(tickets),
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
except Exception as e:
|
|
129
|
+
logger.error(f"Failed to find similar tickets: {e}")
|
|
130
|
+
return {
|
|
131
|
+
"status": "error",
|
|
132
|
+
"error": f"Failed to find similar tickets: {str(e)}",
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@mcp.tool()
|
|
137
|
+
async def ticket_find_stale(
|
|
138
|
+
age_threshold_days: int = 90,
|
|
139
|
+
activity_threshold_days: int = 30,
|
|
140
|
+
states: list[str] | None = None,
|
|
141
|
+
limit: int = 50,
|
|
142
|
+
) -> dict[str, Any]:
|
|
143
|
+
"""Find stale tickets that may need closing.
|
|
144
|
+
|
|
145
|
+
Identifies old tickets with no recent activity that might be
|
|
146
|
+
"won't do" or abandoned work. Uses age, inactivity, state, and
|
|
147
|
+
priority to calculate staleness score.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
age_threshold_days: Minimum age to consider (default: 90)
|
|
151
|
+
activity_threshold_days: Days without activity (default: 30)
|
|
152
|
+
states: Ticket states to check (default: ["open", "waiting", "blocked"])
|
|
153
|
+
limit: Maximum results (default: 50)
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
List of stale tickets with staleness scores and suggested actions
|
|
157
|
+
|
|
158
|
+
Example:
|
|
159
|
+
# Find very old, inactive tickets
|
|
160
|
+
result = await ticket_find_stale(
|
|
161
|
+
age_threshold_days=180,
|
|
162
|
+
activity_threshold_days=60
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# Find stale open tickets only
|
|
166
|
+
result = await ticket_find_stale(states=["open"], limit=100)
|
|
167
|
+
|
|
168
|
+
"""
|
|
169
|
+
if not ANALYSIS_AVAILABLE:
|
|
170
|
+
return {
|
|
171
|
+
"status": "error",
|
|
172
|
+
"error": "Analysis features not available",
|
|
173
|
+
"message": "Install analysis dependencies with: pip install mcp-ticketer[analysis]",
|
|
174
|
+
"required_packages": [
|
|
175
|
+
"scikit-learn>=1.3.0",
|
|
176
|
+
"rapidfuzz>=3.0.0",
|
|
177
|
+
"numpy>=1.24.0",
|
|
178
|
+
],
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
adapter = get_adapter()
|
|
183
|
+
|
|
184
|
+
# Parse states
|
|
185
|
+
check_states = None
|
|
186
|
+
if states:
|
|
187
|
+
try:
|
|
188
|
+
check_states = [TicketState(s.lower()) for s in states]
|
|
189
|
+
except ValueError as e:
|
|
190
|
+
return {
|
|
191
|
+
"status": "error",
|
|
192
|
+
"error": f"Invalid state: {str(e)}. Must be one of: open, in_progress, ready, tested, done, closed, waiting, blocked",
|
|
193
|
+
}
|
|
194
|
+
else:
|
|
195
|
+
check_states = [
|
|
196
|
+
TicketState.OPEN,
|
|
197
|
+
TicketState.WAITING,
|
|
198
|
+
TicketState.BLOCKED,
|
|
199
|
+
]
|
|
200
|
+
|
|
201
|
+
# Fetch tickets - try to filter by state if adapter supports it
|
|
202
|
+
all_tickets = []
|
|
203
|
+
for state in check_states:
|
|
204
|
+
try:
|
|
205
|
+
query = SearchQuery(state=state, limit=100)
|
|
206
|
+
tickets = await adapter.search(query)
|
|
207
|
+
all_tickets.extend(tickets)
|
|
208
|
+
except Exception:
|
|
209
|
+
# If search with state fails, fall back to list all
|
|
210
|
+
all_tickets = await adapter.list(limit=500)
|
|
211
|
+
break
|
|
212
|
+
|
|
213
|
+
if not all_tickets:
|
|
214
|
+
return {
|
|
215
|
+
"status": "completed",
|
|
216
|
+
"stale_tickets": [],
|
|
217
|
+
"count": 0,
|
|
218
|
+
"message": "No tickets found to analyze",
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
# Detect stale tickets
|
|
222
|
+
detector = StaleTicketDetector(
|
|
223
|
+
age_threshold_days=age_threshold_days,
|
|
224
|
+
activity_threshold_days=activity_threshold_days,
|
|
225
|
+
check_states=check_states,
|
|
226
|
+
)
|
|
227
|
+
results = detector.find_stale_tickets(all_tickets, limit)
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
"status": "completed",
|
|
231
|
+
"stale_tickets": [r.model_dump() for r in results],
|
|
232
|
+
"count": len(results),
|
|
233
|
+
"thresholds": {
|
|
234
|
+
"age_days": age_threshold_days,
|
|
235
|
+
"activity_days": activity_threshold_days,
|
|
236
|
+
},
|
|
237
|
+
"states_checked": [s.value for s in check_states],
|
|
238
|
+
"tickets_analyzed": len(all_tickets),
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
except Exception as e:
|
|
242
|
+
logger.error(f"Failed to find stale tickets: {e}")
|
|
243
|
+
return {
|
|
244
|
+
"status": "error",
|
|
245
|
+
"error": f"Failed to find stale tickets: {str(e)}",
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
@mcp.tool()
|
|
250
|
+
async def ticket_find_orphaned(
|
|
251
|
+
limit: int = 100,
|
|
252
|
+
) -> dict[str, Any]:
|
|
253
|
+
"""Find orphaned tickets without parent epic or project.
|
|
254
|
+
|
|
255
|
+
Identifies tickets that aren't properly organized in the hierarchy:
|
|
256
|
+
- Tickets without parent epic/milestone
|
|
257
|
+
- Tickets not assigned to any project/team
|
|
258
|
+
- Standalone issues that should be part of larger initiatives
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
limit: Maximum tickets to check (default: 100)
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
List of orphaned tickets with orphan type and suggested actions
|
|
265
|
+
|
|
266
|
+
Example:
|
|
267
|
+
# Find all orphaned tickets
|
|
268
|
+
result = await ticket_find_orphaned(limit=200)
|
|
269
|
+
|
|
270
|
+
"""
|
|
271
|
+
if not ANALYSIS_AVAILABLE:
|
|
272
|
+
return {
|
|
273
|
+
"status": "error",
|
|
274
|
+
"error": "Analysis features not available",
|
|
275
|
+
"message": "Install analysis dependencies with: pip install mcp-ticketer[analysis]",
|
|
276
|
+
"required_packages": [
|
|
277
|
+
"scikit-learn>=1.3.0",
|
|
278
|
+
"rapidfuzz>=3.0.0",
|
|
279
|
+
"numpy>=1.24.0",
|
|
280
|
+
],
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
try:
|
|
284
|
+
adapter = get_adapter()
|
|
285
|
+
|
|
286
|
+
# Fetch tickets
|
|
287
|
+
tickets = await adapter.list(limit=limit)
|
|
288
|
+
|
|
289
|
+
if not tickets:
|
|
290
|
+
return {
|
|
291
|
+
"status": "completed",
|
|
292
|
+
"orphaned_tickets": [],
|
|
293
|
+
"count": 0,
|
|
294
|
+
"message": "No tickets found to analyze",
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
# Detect orphaned tickets
|
|
298
|
+
detector = OrphanedTicketDetector()
|
|
299
|
+
results = detector.find_orphaned_tickets(tickets)
|
|
300
|
+
|
|
301
|
+
# Calculate statistics
|
|
302
|
+
orphan_stats = {
|
|
303
|
+
"no_parent": len([r for r in results if r.orphan_type == "no_parent"]),
|
|
304
|
+
"no_epic": len([r for r in results if r.orphan_type == "no_epic"]),
|
|
305
|
+
"no_project": len([r for r in results if r.orphan_type == "no_project"]),
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
"status": "completed",
|
|
310
|
+
"orphaned_tickets": [r.model_dump() for r in results],
|
|
311
|
+
"count": len(results),
|
|
312
|
+
"orphan_types": orphan_stats,
|
|
313
|
+
"tickets_analyzed": len(tickets),
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
except Exception as e:
|
|
317
|
+
logger.error(f"Failed to find orphaned tickets: {e}")
|
|
318
|
+
return {
|
|
319
|
+
"status": "error",
|
|
320
|
+
"error": f"Failed to find orphaned tickets: {str(e)}",
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
@mcp.tool()
|
|
325
|
+
async def ticket_cleanup_report(
|
|
326
|
+
include_similar: bool = True,
|
|
327
|
+
include_stale: bool = True,
|
|
328
|
+
include_orphaned: bool = True,
|
|
329
|
+
format: str = "json",
|
|
330
|
+
) -> dict[str, Any]:
|
|
331
|
+
"""Generate comprehensive ticket cleanup report.
|
|
332
|
+
|
|
333
|
+
Combines all cleanup analysis tools into a single report:
|
|
334
|
+
- Similar tickets (duplicates)
|
|
335
|
+
- Stale tickets (candidates for closing)
|
|
336
|
+
- Orphaned tickets (missing hierarchy)
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
include_similar: Include similarity analysis (default: True)
|
|
340
|
+
include_stale: Include staleness analysis (default: True)
|
|
341
|
+
include_orphaned: Include orphaned ticket analysis (default: True)
|
|
342
|
+
format: Output format: "json" or "markdown" (default: "json")
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
Comprehensive cleanup report with all analyses and recommendations
|
|
346
|
+
|
|
347
|
+
Example:
|
|
348
|
+
# Full cleanup report
|
|
349
|
+
result = await ticket_cleanup_report()
|
|
350
|
+
|
|
351
|
+
# Only stale and orphaned analysis
|
|
352
|
+
result = await ticket_cleanup_report(include_similar=False)
|
|
353
|
+
|
|
354
|
+
# Generate markdown report
|
|
355
|
+
result = await ticket_cleanup_report(format="markdown")
|
|
356
|
+
|
|
357
|
+
"""
|
|
358
|
+
if not ANALYSIS_AVAILABLE:
|
|
359
|
+
return {
|
|
360
|
+
"status": "error",
|
|
361
|
+
"error": "Analysis features not available",
|
|
362
|
+
"message": "Install analysis dependencies with: pip install mcp-ticketer[analysis]",
|
|
363
|
+
"required_packages": [
|
|
364
|
+
"scikit-learn>=1.3.0",
|
|
365
|
+
"rapidfuzz>=3.0.0",
|
|
366
|
+
"numpy>=1.24.0",
|
|
367
|
+
],
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
try:
|
|
371
|
+
report: dict[str, Any] = {
|
|
372
|
+
"status": "completed",
|
|
373
|
+
"generated_at": datetime.now().isoformat(),
|
|
374
|
+
"analyses": {},
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
# Similar tickets
|
|
378
|
+
if include_similar:
|
|
379
|
+
similar_result = await ticket_find_similar(limit=20)
|
|
380
|
+
report["analyses"]["similar_tickets"] = similar_result
|
|
381
|
+
|
|
382
|
+
# Stale tickets
|
|
383
|
+
if include_stale:
|
|
384
|
+
stale_result = await ticket_find_stale(limit=50)
|
|
385
|
+
report["analyses"]["stale_tickets"] = stale_result
|
|
386
|
+
|
|
387
|
+
# Orphaned tickets
|
|
388
|
+
if include_orphaned:
|
|
389
|
+
orphaned_result = await ticket_find_orphaned(limit=100)
|
|
390
|
+
report["analyses"]["orphaned_tickets"] = orphaned_result
|
|
391
|
+
|
|
392
|
+
# Summary statistics
|
|
393
|
+
similar_count = report["analyses"].get("similar_tickets", {}).get("count", 0)
|
|
394
|
+
stale_count = report["analyses"].get("stale_tickets", {}).get("count", 0)
|
|
395
|
+
orphaned_count = report["analyses"].get("orphaned_tickets", {}).get("count", 0)
|
|
396
|
+
|
|
397
|
+
report["summary"] = {
|
|
398
|
+
"total_issues_found": similar_count + stale_count + orphaned_count,
|
|
399
|
+
"similar_pairs": similar_count,
|
|
400
|
+
"stale_count": stale_count,
|
|
401
|
+
"orphaned_count": orphaned_count,
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
# Format as markdown if requested
|
|
405
|
+
if format == "markdown":
|
|
406
|
+
report["markdown"] = _format_report_as_markdown(report)
|
|
407
|
+
|
|
408
|
+
return report
|
|
409
|
+
|
|
410
|
+
except Exception as e:
|
|
411
|
+
logger.error(f"Failed to generate cleanup report: {e}")
|
|
412
|
+
return {
|
|
413
|
+
"status": "error",
|
|
414
|
+
"error": f"Failed to generate cleanup report: {str(e)}",
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _format_report_as_markdown(report: dict[str, Any]) -> str:
|
|
419
|
+
"""Format cleanup report as markdown.
|
|
420
|
+
|
|
421
|
+
Args:
|
|
422
|
+
report: Report data dictionary
|
|
423
|
+
|
|
424
|
+
Returns:
|
|
425
|
+
Markdown-formatted report string
|
|
426
|
+
|
|
427
|
+
"""
|
|
428
|
+
md = "# Ticket Cleanup Report\n\n"
|
|
429
|
+
md += f"**Generated:** {report['generated_at']}\n\n"
|
|
430
|
+
|
|
431
|
+
summary = report["summary"]
|
|
432
|
+
md += "## Summary\n\n"
|
|
433
|
+
md += f"- **Total Issues Found:** {summary['total_issues_found']}\n"
|
|
434
|
+
md += f"- **Similar Ticket Pairs:** {summary['similar_pairs']}\n"
|
|
435
|
+
md += f"- **Stale Tickets:** {summary['stale_count']}\n"
|
|
436
|
+
md += f"- **Orphaned Tickets:** {summary['orphaned_count']}\n\n"
|
|
437
|
+
|
|
438
|
+
# Similar tickets section
|
|
439
|
+
similar_data = report["analyses"].get("similar_tickets", {})
|
|
440
|
+
if similar_data.get("similar_tickets"):
|
|
441
|
+
md += "## Similar Tickets (Potential Duplicates)\n\n"
|
|
442
|
+
for result in similar_data["similar_tickets"][:10]: # Top 10
|
|
443
|
+
md += f"### {result['ticket1_title']} ↔ {result['ticket2_title']}\n"
|
|
444
|
+
md += f"- **Similarity:** {result['similarity_score']:.2%}\n"
|
|
445
|
+
md += f"- **Ticket 1:** `{result['ticket1_id']}`\n"
|
|
446
|
+
md += f"- **Ticket 2:** `{result['ticket2_id']}`\n"
|
|
447
|
+
md += f"- **Action:** {result['suggested_action'].upper()}\n"
|
|
448
|
+
md += f"- **Reasons:** {', '.join(result['similarity_reasons'])}\n\n"
|
|
449
|
+
|
|
450
|
+
# Stale tickets section
|
|
451
|
+
stale_data = report["analyses"].get("stale_tickets", {})
|
|
452
|
+
if stale_data.get("stale_tickets"):
|
|
453
|
+
md += "## Stale Tickets (Candidates for Closing)\n\n"
|
|
454
|
+
for result in stale_data["stale_tickets"][:15]: # Top 15
|
|
455
|
+
md += f"### {result['ticket_title']}\n"
|
|
456
|
+
md += f"- **ID:** `{result['ticket_id']}`\n"
|
|
457
|
+
md += f"- **State:** {result['ticket_state']}\n"
|
|
458
|
+
md += f"- **Age:** {result['age_days']} days\n"
|
|
459
|
+
md += f"- **Last Updated:** {result['days_since_update']} days ago\n"
|
|
460
|
+
md += f"- **Staleness Score:** {result['staleness_score']:.2%}\n"
|
|
461
|
+
md += f"- **Action:** {result['suggested_action'].upper()}\n"
|
|
462
|
+
md += f"- **Reason:** {result['reason']}\n\n"
|
|
463
|
+
|
|
464
|
+
# Orphaned tickets section
|
|
465
|
+
orphaned_data = report["analyses"].get("orphaned_tickets", {})
|
|
466
|
+
if orphaned_data.get("orphaned_tickets"):
|
|
467
|
+
md += "## Orphaned Tickets (Missing Hierarchy)\n\n"
|
|
468
|
+
|
|
469
|
+
# Group by orphan type
|
|
470
|
+
by_type: dict[str, list[Any]] = {}
|
|
471
|
+
for result in orphaned_data["orphaned_tickets"]:
|
|
472
|
+
orphan_type = result["orphan_type"]
|
|
473
|
+
if orphan_type not in by_type:
|
|
474
|
+
by_type[orphan_type] = []
|
|
475
|
+
by_type[orphan_type].append(result)
|
|
476
|
+
|
|
477
|
+
for orphan_type, tickets in by_type.items():
|
|
478
|
+
md += f"### {orphan_type.replace('_', ' ').title()} ({len(tickets)})\n\n"
|
|
479
|
+
for result in tickets[:10]: # Top 10 per type
|
|
480
|
+
md += f"- **{result['ticket_title']}** (`{result['ticket_id']}`)\n"
|
|
481
|
+
md += f" - Type: {result['ticket_type']}\n"
|
|
482
|
+
md += f" - Action: {result['suggested_action']}\n"
|
|
483
|
+
md += f" - Reason: {result['reason']}\n"
|
|
484
|
+
md += "\n"
|
|
485
|
+
|
|
486
|
+
# Recommendations section
|
|
487
|
+
md += "## Recommendations\n\n"
|
|
488
|
+
md += "1. **Review Similar Tickets:** Check pairs marked for 'merge' action\n"
|
|
489
|
+
md += "2. **Close Stale Tickets:** Review tickets marked for 'close' action\n"
|
|
490
|
+
md += (
|
|
491
|
+
"3. **Organize Orphaned Tickets:** Assign epics/projects to orphaned tickets\n"
|
|
492
|
+
)
|
|
493
|
+
md += "4. **Update Workflow:** Consider closing very old low-priority tickets\n\n"
|
|
494
|
+
|
|
495
|
+
return md
|