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,152 @@
|
|
|
1
|
+
"""Pull request integration tools for tickets.
|
|
2
|
+
|
|
3
|
+
This module implements tools for linking tickets with pull requests and
|
|
4
|
+
creating PRs from tickets. Note that PR functionality may not be available
|
|
5
|
+
in all adapters.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from ..server_sdk import get_adapter, mcp
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@mcp.tool()
|
|
14
|
+
async def ticket_create_pr(
|
|
15
|
+
ticket_id: str,
|
|
16
|
+
title: str,
|
|
17
|
+
description: str = "",
|
|
18
|
+
source_branch: str | None = None,
|
|
19
|
+
target_branch: str = "main",
|
|
20
|
+
) -> dict[str, Any]:
|
|
21
|
+
"""Create a pull request linked to a ticket.
|
|
22
|
+
|
|
23
|
+
Creates a new pull request and automatically links it to the specified
|
|
24
|
+
ticket. This functionality may not be available in all adapters.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
ticket_id: Unique identifier of the ticket to link the PR to
|
|
28
|
+
title: Pull request title
|
|
29
|
+
description: Pull request description
|
|
30
|
+
source_branch: Source branch for the PR (if not specified, may use ticket ID)
|
|
31
|
+
target_branch: Target branch for the PR (default: main)
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Created PR details and link information, or error information
|
|
35
|
+
|
|
36
|
+
"""
|
|
37
|
+
try:
|
|
38
|
+
adapter = get_adapter()
|
|
39
|
+
|
|
40
|
+
# Check if adapter supports PR operations
|
|
41
|
+
if not hasattr(adapter, "create_pull_request"):
|
|
42
|
+
return {
|
|
43
|
+
"status": "error",
|
|
44
|
+
"error": f"Pull request creation not supported by {type(adapter).__name__} adapter",
|
|
45
|
+
"ticket_id": ticket_id,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# Read ticket to validate it exists
|
|
49
|
+
ticket = await adapter.read(ticket_id)
|
|
50
|
+
if ticket is None:
|
|
51
|
+
return {
|
|
52
|
+
"status": "error",
|
|
53
|
+
"error": f"Ticket {ticket_id} not found",
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
# Use ticket ID as source branch if not specified
|
|
57
|
+
if source_branch is None:
|
|
58
|
+
source_branch = f"feature/{ticket_id}"
|
|
59
|
+
|
|
60
|
+
# Create PR via adapter
|
|
61
|
+
pr_data = await adapter.create_pull_request(
|
|
62
|
+
ticket_id=ticket_id,
|
|
63
|
+
title=title,
|
|
64
|
+
description=description,
|
|
65
|
+
source_branch=source_branch,
|
|
66
|
+
target_branch=target_branch,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
"status": "completed",
|
|
71
|
+
"ticket_id": ticket_id,
|
|
72
|
+
"pull_request": pr_data,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
except AttributeError:
|
|
76
|
+
return {
|
|
77
|
+
"status": "error",
|
|
78
|
+
"error": "Pull request creation not supported by this adapter",
|
|
79
|
+
"ticket_id": ticket_id,
|
|
80
|
+
}
|
|
81
|
+
except Exception as e:
|
|
82
|
+
return {
|
|
83
|
+
"status": "error",
|
|
84
|
+
"error": f"Failed to create pull request: {str(e)}",
|
|
85
|
+
"ticket_id": ticket_id,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@mcp.tool()
|
|
90
|
+
async def ticket_link_pr(
|
|
91
|
+
ticket_id: str,
|
|
92
|
+
pr_url: str,
|
|
93
|
+
) -> dict[str, Any]:
|
|
94
|
+
"""Link an existing pull request to a ticket.
|
|
95
|
+
|
|
96
|
+
Associates an existing pull request (identified by URL) with a ticket.
|
|
97
|
+
This is typically done by adding the PR URL to the ticket's metadata
|
|
98
|
+
or as a comment.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
ticket_id: Unique identifier of the ticket
|
|
102
|
+
pr_url: URL of the pull request to link
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Link confirmation and updated ticket details, or error information
|
|
106
|
+
|
|
107
|
+
"""
|
|
108
|
+
try:
|
|
109
|
+
adapter = get_adapter()
|
|
110
|
+
|
|
111
|
+
# Read ticket to validate it exists
|
|
112
|
+
ticket = await adapter.read(ticket_id)
|
|
113
|
+
if ticket is None:
|
|
114
|
+
return {
|
|
115
|
+
"status": "error",
|
|
116
|
+
"error": f"Ticket {ticket_id} not found",
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
# Check if adapter has specialized PR linking
|
|
120
|
+
if hasattr(adapter, "link_pull_request"):
|
|
121
|
+
result = await adapter.link_pull_request(ticket_id=ticket_id, pr_url=pr_url)
|
|
122
|
+
return {
|
|
123
|
+
"status": "completed",
|
|
124
|
+
"ticket_id": ticket_id,
|
|
125
|
+
"pr_url": pr_url,
|
|
126
|
+
"result": result,
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
# Fallback: Add PR link as comment
|
|
130
|
+
from ....core.models import Comment
|
|
131
|
+
|
|
132
|
+
comment = Comment(
|
|
133
|
+
ticket_id=ticket_id,
|
|
134
|
+
content=f"Pull Request: {pr_url}",
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
created_comment = await adapter.add_comment(comment)
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
"status": "completed",
|
|
141
|
+
"ticket_id": ticket_id,
|
|
142
|
+
"pr_url": pr_url,
|
|
143
|
+
"method": "comment",
|
|
144
|
+
"comment": created_comment.model_dump(),
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
except Exception as e:
|
|
148
|
+
return {
|
|
149
|
+
"status": "error",
|
|
150
|
+
"error": f"Failed to link pull request: {str(e)}",
|
|
151
|
+
"ticket_id": ticket_id,
|
|
152
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""Search and query tools for finding tickets.
|
|
2
|
+
|
|
3
|
+
This module implements advanced search capabilities for tickets using
|
|
4
|
+
various filters and criteria.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from ....core.models import Priority, SearchQuery, TicketState
|
|
11
|
+
from ..server_sdk import get_adapter, mcp
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@mcp.tool()
|
|
15
|
+
async def ticket_search(
|
|
16
|
+
query: str | None = None,
|
|
17
|
+
state: str | None = None,
|
|
18
|
+
priority: str | None = None,
|
|
19
|
+
tags: list[str] | None = None,
|
|
20
|
+
assignee: str | None = None,
|
|
21
|
+
limit: int = 10,
|
|
22
|
+
) -> dict[str, Any]:
|
|
23
|
+
"""Search tickets using advanced filters.
|
|
24
|
+
|
|
25
|
+
Searches for tickets matching the specified criteria. All filters are
|
|
26
|
+
optional and can be combined.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
query: Text search query to match against title and description
|
|
30
|
+
state: Filter by state - must be one of: open, in_progress, ready, tested, done, closed, waiting, blocked
|
|
31
|
+
priority: Filter by priority - must be one of: low, medium, high, critical
|
|
32
|
+
tags: Filter by tags - tickets must have all specified tags
|
|
33
|
+
assignee: Filter by assigned user ID or email
|
|
34
|
+
limit: Maximum number of results to return (default: 10, max: 100)
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
List of tickets matching search criteria, or error information
|
|
38
|
+
|
|
39
|
+
"""
|
|
40
|
+
try:
|
|
41
|
+
adapter = get_adapter()
|
|
42
|
+
|
|
43
|
+
# Add warning for unscoped searches
|
|
44
|
+
if not query and not (state or priority or tags or assignee):
|
|
45
|
+
logging.warning(
|
|
46
|
+
"Unscoped search with no query or filters. "
|
|
47
|
+
"This will search ALL tickets across all projects. "
|
|
48
|
+
"Tip: Configure default_project or default_team for automatic scoping."
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Validate and build search query
|
|
52
|
+
state_enum = None
|
|
53
|
+
if state is not None:
|
|
54
|
+
try:
|
|
55
|
+
state_enum = TicketState(state.lower())
|
|
56
|
+
except ValueError:
|
|
57
|
+
return {
|
|
58
|
+
"status": "error",
|
|
59
|
+
"error": f"Invalid state '{state}'. Must be one of: open, in_progress, ready, tested, done, closed, waiting, blocked",
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
priority_enum = None
|
|
63
|
+
if priority is not None:
|
|
64
|
+
try:
|
|
65
|
+
priority_enum = Priority(priority.lower())
|
|
66
|
+
except ValueError:
|
|
67
|
+
return {
|
|
68
|
+
"status": "error",
|
|
69
|
+
"error": f"Invalid priority '{priority}'. Must be one of: low, medium, high, critical",
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# Create search query
|
|
73
|
+
search_query = SearchQuery(
|
|
74
|
+
query=query,
|
|
75
|
+
state=state_enum,
|
|
76
|
+
priority=priority_enum,
|
|
77
|
+
tags=tags,
|
|
78
|
+
assignee=assignee,
|
|
79
|
+
limit=min(limit, 100), # Enforce max limit
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Execute search via adapter
|
|
83
|
+
results = await adapter.search(search_query)
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
"status": "completed",
|
|
87
|
+
"tickets": [ticket.model_dump() for ticket in results],
|
|
88
|
+
"count": len(results),
|
|
89
|
+
"query": {
|
|
90
|
+
"text": query,
|
|
91
|
+
"state": state,
|
|
92
|
+
"priority": priority,
|
|
93
|
+
"tags": tags,
|
|
94
|
+
"assignee": assignee,
|
|
95
|
+
},
|
|
96
|
+
}
|
|
97
|
+
except Exception as e:
|
|
98
|
+
return {
|
|
99
|
+
"status": "error",
|
|
100
|
+
"error": f"Failed to search tickets: {str(e)}",
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@mcp.tool()
|
|
105
|
+
async def ticket_search_hierarchy(
|
|
106
|
+
query: str,
|
|
107
|
+
include_children: bool = True,
|
|
108
|
+
max_depth: int = 3,
|
|
109
|
+
) -> dict[str, Any]:
|
|
110
|
+
"""Search tickets and include their hierarchy.
|
|
111
|
+
|
|
112
|
+
Performs a text search and returns matching tickets along with their
|
|
113
|
+
hierarchical context (parent epics/issues and child issues/tasks).
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
query: Text search query to match against title and description
|
|
117
|
+
include_children: Whether to include child tickets in results
|
|
118
|
+
max_depth: Maximum hierarchy depth to include (1-3, default: 3)
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
List of tickets with hierarchy information, or error information
|
|
122
|
+
|
|
123
|
+
"""
|
|
124
|
+
try:
|
|
125
|
+
adapter = get_adapter()
|
|
126
|
+
|
|
127
|
+
# Validate max_depth
|
|
128
|
+
if max_depth < 1 or max_depth > 3:
|
|
129
|
+
return {
|
|
130
|
+
"status": "error",
|
|
131
|
+
"error": "max_depth must be between 1 and 3",
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
# Create search query
|
|
135
|
+
search_query = SearchQuery(
|
|
136
|
+
query=query,
|
|
137
|
+
limit=50, # Reasonable limit for hierarchical search
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# Execute search via adapter
|
|
141
|
+
results = await adapter.search(search_query)
|
|
142
|
+
|
|
143
|
+
# Build hierarchical results
|
|
144
|
+
hierarchical_results = []
|
|
145
|
+
for ticket in results:
|
|
146
|
+
ticket_data = {
|
|
147
|
+
"ticket": ticket.model_dump(),
|
|
148
|
+
"hierarchy": {},
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
# Get parent epic if applicable
|
|
152
|
+
parent_epic_id = getattr(ticket, "parent_epic", None)
|
|
153
|
+
if parent_epic_id and max_depth >= 2:
|
|
154
|
+
try:
|
|
155
|
+
parent_epic = await adapter.read(parent_epic_id)
|
|
156
|
+
if parent_epic:
|
|
157
|
+
ticket_data["hierarchy"][
|
|
158
|
+
"parent_epic"
|
|
159
|
+
] = parent_epic.model_dump()
|
|
160
|
+
except Exception:
|
|
161
|
+
pass # Parent not found, continue
|
|
162
|
+
|
|
163
|
+
# Get parent issue if applicable (for tasks)
|
|
164
|
+
parent_issue_id = getattr(ticket, "parent_issue", None)
|
|
165
|
+
if parent_issue_id and max_depth >= 2:
|
|
166
|
+
try:
|
|
167
|
+
parent_issue = await adapter.read(parent_issue_id)
|
|
168
|
+
if parent_issue:
|
|
169
|
+
ticket_data["hierarchy"][
|
|
170
|
+
"parent_issue"
|
|
171
|
+
] = parent_issue.model_dump()
|
|
172
|
+
except Exception:
|
|
173
|
+
pass # Parent not found, continue
|
|
174
|
+
|
|
175
|
+
# Get children if requested
|
|
176
|
+
if include_children and max_depth >= 2:
|
|
177
|
+
children = []
|
|
178
|
+
|
|
179
|
+
# Get child issues (for epics)
|
|
180
|
+
child_issue_ids = getattr(ticket, "child_issues", [])
|
|
181
|
+
for child_id in child_issue_ids:
|
|
182
|
+
try:
|
|
183
|
+
child = await adapter.read(child_id)
|
|
184
|
+
if child:
|
|
185
|
+
children.append(child.model_dump())
|
|
186
|
+
except Exception:
|
|
187
|
+
pass # Child not found, continue
|
|
188
|
+
|
|
189
|
+
# Get child tasks (for issues)
|
|
190
|
+
child_task_ids = getattr(ticket, "children", [])
|
|
191
|
+
for child_id in child_task_ids:
|
|
192
|
+
try:
|
|
193
|
+
child = await adapter.read(child_id)
|
|
194
|
+
if child:
|
|
195
|
+
children.append(child.model_dump())
|
|
196
|
+
except Exception:
|
|
197
|
+
pass # Child not found, continue
|
|
198
|
+
|
|
199
|
+
if children:
|
|
200
|
+
ticket_data["hierarchy"]["children"] = children
|
|
201
|
+
|
|
202
|
+
hierarchical_results.append(ticket_data)
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
"status": "completed",
|
|
206
|
+
"results": hierarchical_results,
|
|
207
|
+
"count": len(hierarchical_results),
|
|
208
|
+
"query": query,
|
|
209
|
+
"max_depth": max_depth,
|
|
210
|
+
}
|
|
211
|
+
except Exception as e:
|
|
212
|
+
return {
|
|
213
|
+
"status": "error",
|
|
214
|
+
"error": f"Failed to search with hierarchy: {str(e)}",
|
|
215
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""MCP tools for session and ticket association management."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from ....core.session_state import SessionStateManager
|
|
8
|
+
from ..server_sdk import mcp
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@mcp.tool()
|
|
14
|
+
async def attach_ticket(
|
|
15
|
+
action: str,
|
|
16
|
+
ticket_id: str | None = None,
|
|
17
|
+
) -> dict[str, Any]:
|
|
18
|
+
"""Associate current work session with a ticket.
|
|
19
|
+
|
|
20
|
+
This tool helps track which ticket your current work is related to.
|
|
21
|
+
The association persists for the session (30 minutes of inactivity).
|
|
22
|
+
|
|
23
|
+
**Important**: It's recommended to associate work with a ticket for proper
|
|
24
|
+
tracking and organization.
|
|
25
|
+
|
|
26
|
+
Actions:
|
|
27
|
+
- **set**: Associate work with a specific ticket
|
|
28
|
+
- **clear**: Remove current ticket association
|
|
29
|
+
- **none**: Opt out of ticket association for this session
|
|
30
|
+
- **status**: Check current ticket association
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
action: What to do with the ticket association (set/clear/none/status)
|
|
34
|
+
ticket_id: Ticket ID to associate (e.g., "PROJ-123", UUID), required for 'set'
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Success status and current session state
|
|
38
|
+
|
|
39
|
+
Examples:
|
|
40
|
+
# Associate with a ticket
|
|
41
|
+
attach_ticket(action="set", ticket_id="PROJ-123")
|
|
42
|
+
|
|
43
|
+
# Opt out for this session
|
|
44
|
+
attach_ticket(action="none")
|
|
45
|
+
|
|
46
|
+
# Check current status
|
|
47
|
+
attach_ticket(action="status")
|
|
48
|
+
|
|
49
|
+
"""
|
|
50
|
+
try:
|
|
51
|
+
manager = SessionStateManager(project_path=Path.cwd())
|
|
52
|
+
state = manager.load_session()
|
|
53
|
+
|
|
54
|
+
if action == "set":
|
|
55
|
+
if not ticket_id:
|
|
56
|
+
return {
|
|
57
|
+
"success": False,
|
|
58
|
+
"error": "ticket_id is required when action='set'",
|
|
59
|
+
"guidance": "Please provide a ticket ID to associate with this session",
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
manager.set_current_ticket(ticket_id)
|
|
63
|
+
return {
|
|
64
|
+
"success": True,
|
|
65
|
+
"message": f"Work session now associated with ticket: {ticket_id}",
|
|
66
|
+
"current_ticket": ticket_id,
|
|
67
|
+
"session_id": state.session_id,
|
|
68
|
+
"opted_out": False,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
elif action == "clear":
|
|
72
|
+
manager.set_current_ticket(None)
|
|
73
|
+
return {
|
|
74
|
+
"success": True,
|
|
75
|
+
"message": "Ticket association cleared",
|
|
76
|
+
"current_ticket": None,
|
|
77
|
+
"session_id": state.session_id,
|
|
78
|
+
"opted_out": False,
|
|
79
|
+
"guidance": "You can associate with a ticket anytime using attach_ticket(action='set', ticket_id='...')",
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
elif action == "none":
|
|
83
|
+
manager.opt_out_ticket()
|
|
84
|
+
return {
|
|
85
|
+
"success": True,
|
|
86
|
+
"message": "Opted out of ticket association for this session",
|
|
87
|
+
"current_ticket": None,
|
|
88
|
+
"session_id": state.session_id,
|
|
89
|
+
"opted_out": True,
|
|
90
|
+
"note": "This opt-out will reset after 30 minutes of inactivity",
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
elif action == "status":
|
|
94
|
+
current_ticket = manager.get_current_ticket()
|
|
95
|
+
|
|
96
|
+
if state.ticket_opted_out:
|
|
97
|
+
status_msg = "No ticket associated (opted out for this session)"
|
|
98
|
+
elif current_ticket:
|
|
99
|
+
status_msg = f"Currently associated with ticket: {current_ticket}"
|
|
100
|
+
else:
|
|
101
|
+
status_msg = "No ticket associated"
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
"success": True,
|
|
105
|
+
"message": status_msg,
|
|
106
|
+
"current_ticket": current_ticket,
|
|
107
|
+
"session_id": state.session_id,
|
|
108
|
+
"opted_out": state.ticket_opted_out,
|
|
109
|
+
"guidance": (
|
|
110
|
+
(
|
|
111
|
+
"Associate with a ticket: attach_ticket(action='set', ticket_id='...')\n"
|
|
112
|
+
"Opt out: attach_ticket(action='none')"
|
|
113
|
+
)
|
|
114
|
+
if not current_ticket and not state.ticket_opted_out
|
|
115
|
+
else None
|
|
116
|
+
),
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
else:
|
|
120
|
+
return {
|
|
121
|
+
"success": False,
|
|
122
|
+
"error": f"Invalid action: {action}",
|
|
123
|
+
"valid_actions": ["set", "clear", "none", "status"],
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
except Exception as e:
|
|
127
|
+
logger.error(f"Error in attach_ticket: {e}")
|
|
128
|
+
return {
|
|
129
|
+
"success": False,
|
|
130
|
+
"error": str(e),
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@mcp.tool()
|
|
135
|
+
async def get_session_info() -> dict[str, Any]:
|
|
136
|
+
"""Get current session information and ticket association status.
|
|
137
|
+
|
|
138
|
+
Returns session metadata including ID, current ticket, and activity status.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
Session information dictionary
|
|
142
|
+
|
|
143
|
+
Example:
|
|
144
|
+
{
|
|
145
|
+
"session_id": "abc-123",
|
|
146
|
+
"current_ticket": "PROJ-123",
|
|
147
|
+
"opted_out": false,
|
|
148
|
+
"last_activity": "2025-01-19T20:00:00"
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
"""
|
|
152
|
+
try:
|
|
153
|
+
manager = SessionStateManager(project_path=Path.cwd())
|
|
154
|
+
state = manager.load_session()
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
"success": True,
|
|
158
|
+
"session_id": state.session_id,
|
|
159
|
+
"current_ticket": state.current_ticket,
|
|
160
|
+
"opted_out": state.ticket_opted_out,
|
|
161
|
+
"last_activity": state.last_activity,
|
|
162
|
+
"session_timeout_minutes": 30,
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
except Exception as e:
|
|
166
|
+
logger.error(f"Error in get_session_info: {e}")
|
|
167
|
+
return {
|
|
168
|
+
"success": False,
|
|
169
|
+
"error": str(e),
|
|
170
|
+
}
|