mcp-ticketer 0.4.11__py3-none-any.whl → 2.0.1__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 +394 -9
- 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 +836 -105
- mcp_ticketer/adapters/hybrid.py +47 -5
- mcp_ticketer/adapters/jira.py +772 -1
- mcp_ticketer/adapters/linear/adapter.py +2293 -108
- mcp_ticketer/adapters/linear/client.py +146 -12
- mcp_ticketer/adapters/linear/mappers.py +105 -11
- mcp_ticketer/adapters/linear/queries.py +168 -1
- mcp_ticketer/adapters/linear/types.py +80 -4
- 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 +3 -3
- mcp_ticketer/cli/adapter_diagnostics.py +4 -2
- mcp_ticketer/cli/auggie_configure.py +18 -6
- mcp_ticketer/cli/codex_configure.py +175 -60
- mcp_ticketer/cli/configure.py +884 -146
- mcp_ticketer/cli/cursor_configure.py +314 -0
- mcp_ticketer/cli/diagnostics.py +31 -28
- mcp_ticketer/cli/discover.py +293 -21
- mcp_ticketer/cli/gemini_configure.py +18 -6
- mcp_ticketer/cli/init_command.py +880 -0
- mcp_ticketer/cli/instruction_commands.py +435 -0
- mcp_ticketer/cli/linear_commands.py +99 -15
- mcp_ticketer/cli/main.py +109 -2055
- mcp_ticketer/cli/mcp_configure.py +673 -99
- mcp_ticketer/cli/mcp_server_commands.py +415 -0
- mcp_ticketer/cli/migrate_config.py +12 -8
- mcp_ticketer/cli/platform_commands.py +6 -6
- mcp_ticketer/cli/platform_detection.py +477 -0
- mcp_ticketer/cli/platform_installer.py +536 -0
- mcp_ticketer/cli/project_update_commands.py +350 -0
- mcp_ticketer/cli/queue_commands.py +15 -15
- mcp_ticketer/cli/setup_command.py +639 -0
- mcp_ticketer/cli/simple_health.py +13 -11
- mcp_ticketer/cli/ticket_commands.py +277 -36
- mcp_ticketer/cli/update_checker.py +313 -0
- mcp_ticketer/cli/utils.py +45 -41
- mcp_ticketer/core/__init__.py +35 -1
- mcp_ticketer/core/adapter.py +170 -5
- mcp_ticketer/core/config.py +38 -31
- mcp_ticketer/core/env_discovery.py +33 -3
- mcp_ticketer/core/env_loader.py +7 -6
- mcp_ticketer/core/exceptions.py +10 -4
- mcp_ticketer/core/http_client.py +10 -10
- mcp_ticketer/core/instructions.py +405 -0
- mcp_ticketer/core/label_manager.py +732 -0
- mcp_ticketer/core/mappers.py +32 -20
- mcp_ticketer/core/models.py +136 -1
- mcp_ticketer/core/onepassword_secrets.py +379 -0
- mcp_ticketer/core/priority_matcher.py +463 -0
- mcp_ticketer/core/project_config.py +148 -14
- mcp_ticketer/core/registry.py +1 -1
- 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 +2 -2
- mcp_ticketer/mcp/server/__init__.py +2 -2
- mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
- mcp_ticketer/mcp/server/main.py +187 -93
- mcp_ticketer/mcp/server/routing.py +655 -0
- mcp_ticketer/mcp/server/server_sdk.py +58 -0
- mcp_ticketer/mcp/server/tools/__init__.py +37 -9
- mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
- mcp_ticketer/mcp/server/tools/attachment_tools.py +65 -20
- mcp_ticketer/mcp/server/tools/bulk_tools.py +259 -202
- mcp_ticketer/mcp/server/tools/comment_tools.py +74 -12
- mcp_ticketer/mcp/server/tools/config_tools.py +1429 -0
- mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
- mcp_ticketer/mcp/server/tools/hierarchy_tools.py +878 -319
- 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/pr_tools.py +3 -7
- 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 +180 -97
- mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
- mcp_ticketer/mcp/server/tools/ticket_tools.py +1182 -82
- mcp_ticketer/mcp/server/tools/user_ticket_tools.py +364 -0
- mcp_ticketer/queue/health_monitor.py +1 -0
- mcp_ticketer/queue/manager.py +4 -4
- mcp_ticketer/queue/queue.py +3 -3
- mcp_ticketer/queue/run_worker.py +1 -1
- mcp_ticketer/queue/ticket_registry.py +2 -2
- mcp_ticketer/queue/worker.py +15 -13
- mcp_ticketer/utils/__init__.py +5 -0
- mcp_ticketer/utils/token_utils.py +246 -0
- mcp_ticketer-2.0.1.dist-info/METADATA +1366 -0
- mcp_ticketer-2.0.1.dist-info/RECORD +122 -0
- mcp_ticketer-0.4.11.dist-info/METADATA +0 -496
- mcp_ticketer-0.4.11.dist-info/RECORD +0 -77
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/top_level.txt +0 -0
|
@@ -4,6 +4,7 @@ This module implements advanced search capabilities for tickets using
|
|
|
4
4
|
various filters and criteria.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
import logging
|
|
7
8
|
from typing import Any
|
|
8
9
|
|
|
9
10
|
from ....core.models import Priority, SearchQuery, TicketState
|
|
@@ -17,12 +18,37 @@ async def ticket_search(
|
|
|
17
18
|
priority: str | None = None,
|
|
18
19
|
tags: list[str] | None = None,
|
|
19
20
|
assignee: str | None = None,
|
|
21
|
+
project_id: str | None = None,
|
|
20
22
|
limit: int = 10,
|
|
23
|
+
include_hierarchy: bool = False,
|
|
24
|
+
include_children: bool = True,
|
|
25
|
+
max_depth: int = 3,
|
|
21
26
|
) -> dict[str, Any]:
|
|
22
|
-
"""Search tickets
|
|
27
|
+
"""Search tickets with optional hierarchy information.
|
|
28
|
+
|
|
29
|
+
**Consolidates:**
|
|
30
|
+
- ticket_search() → Default behavior (include_hierarchy=False)
|
|
31
|
+
- ticket_search_hierarchy() → Set include_hierarchy=True
|
|
32
|
+
|
|
33
|
+
⚠️ Project Filtering Required:
|
|
34
|
+
This tool requires project_id parameter OR default_project configuration.
|
|
35
|
+
To set default project: config_set_default_project(project_id="YOUR-PROJECT")
|
|
36
|
+
To check current config: config_get()
|
|
23
37
|
|
|
24
|
-
|
|
25
|
-
|
|
38
|
+
Exception: Single ticket operations (ticket_read) don't require project filtering.
|
|
39
|
+
|
|
40
|
+
**Search Filters:**
|
|
41
|
+
- query: Text search in title and description
|
|
42
|
+
- state: Filter by workflow state
|
|
43
|
+
- priority: Filter by priority level
|
|
44
|
+
- tags: Filter by tags (AND logic)
|
|
45
|
+
- assignee: Filter by assigned user
|
|
46
|
+
- project_id: Scope to specific project
|
|
47
|
+
|
|
48
|
+
**Hierarchy Options:**
|
|
49
|
+
- include_hierarchy: Include parent/child relationships (default: False)
|
|
50
|
+
- include_children: Include child tickets (default: True, requires include_hierarchy=True)
|
|
51
|
+
- max_depth: Maximum hierarchy depth (default: 3, requires include_hierarchy=True)
|
|
26
52
|
|
|
27
53
|
Args:
|
|
28
54
|
query: Text search query to match against title and description
|
|
@@ -30,15 +56,56 @@ async def ticket_search(
|
|
|
30
56
|
priority: Filter by priority - must be one of: low, medium, high, critical
|
|
31
57
|
tags: Filter by tags - tickets must have all specified tags
|
|
32
58
|
assignee: Filter by assigned user ID or email
|
|
59
|
+
project_id: Project/epic ID (required unless default_project configured)
|
|
33
60
|
limit: Maximum number of results to return (default: 10, max: 100)
|
|
61
|
+
include_hierarchy: Include parent/child relationships (default: False)
|
|
62
|
+
include_children: Include child tickets in hierarchy (default: True)
|
|
63
|
+
max_depth: Maximum hierarchy depth to traverse (default: 3)
|
|
34
64
|
|
|
35
65
|
Returns:
|
|
36
66
|
List of tickets matching search criteria, or error information
|
|
37
67
|
|
|
68
|
+
Examples:
|
|
69
|
+
# Simple search (backward compatible)
|
|
70
|
+
await ticket_search(query="authentication bug", state="open", limit=5)
|
|
71
|
+
|
|
72
|
+
# Search with hierarchy
|
|
73
|
+
await ticket_search(
|
|
74
|
+
query="oauth implementation",
|
|
75
|
+
project_id="proj-123",
|
|
76
|
+
include_hierarchy=True,
|
|
77
|
+
max_depth=2
|
|
78
|
+
)
|
|
79
|
+
|
|
38
80
|
"""
|
|
39
81
|
try:
|
|
82
|
+
# Validate project context (NEW: Required for search operations)
|
|
83
|
+
from pathlib import Path
|
|
84
|
+
|
|
85
|
+
from ....core.project_config import ConfigResolver
|
|
86
|
+
|
|
87
|
+
resolver = ConfigResolver(project_path=Path.cwd())
|
|
88
|
+
config = resolver.load_project_config()
|
|
89
|
+
final_project = project_id or (config.default_project if config else None)
|
|
90
|
+
|
|
91
|
+
if not final_project:
|
|
92
|
+
return {
|
|
93
|
+
"status": "error",
|
|
94
|
+
"error": "project_id required. Provide project_id parameter or configure default_project.",
|
|
95
|
+
"help": "Use config_set_default_project(project_id='YOUR-PROJECT') to set default project",
|
|
96
|
+
"check_config": "Use config_get() to view current configuration",
|
|
97
|
+
}
|
|
98
|
+
|
|
40
99
|
adapter = get_adapter()
|
|
41
100
|
|
|
101
|
+
# Add warning for unscoped searches
|
|
102
|
+
if not query and not (state or priority or tags or assignee):
|
|
103
|
+
logging.warning(
|
|
104
|
+
"Unscoped search with no query or filters. "
|
|
105
|
+
"This will search ALL tickets across all projects. "
|
|
106
|
+
"Tip: Configure default_project or default_team for automatic scoping."
|
|
107
|
+
)
|
|
108
|
+
|
|
42
109
|
# Validate and build search query
|
|
43
110
|
state_enum = None
|
|
44
111
|
if state is not None:
|
|
@@ -60,19 +127,99 @@ async def ticket_search(
|
|
|
60
127
|
"error": f"Invalid priority '{priority}'. Must be one of: low, medium, high, critical",
|
|
61
128
|
}
|
|
62
129
|
|
|
63
|
-
# Create search query
|
|
130
|
+
# Create search query with project scoping
|
|
64
131
|
search_query = SearchQuery(
|
|
65
132
|
query=query,
|
|
66
133
|
state=state_enum,
|
|
67
134
|
priority=priority_enum,
|
|
68
135
|
tags=tags,
|
|
69
136
|
assignee=assignee,
|
|
137
|
+
project=final_project, # Always required for search operations
|
|
70
138
|
limit=min(limit, 100), # Enforce max limit
|
|
71
139
|
)
|
|
72
140
|
|
|
73
141
|
# Execute search via adapter
|
|
74
142
|
results = await adapter.search(search_query)
|
|
75
143
|
|
|
144
|
+
# Add hierarchy if requested
|
|
145
|
+
if include_hierarchy:
|
|
146
|
+
# Validate max_depth
|
|
147
|
+
if max_depth < 1 or max_depth > 3:
|
|
148
|
+
return {
|
|
149
|
+
"status": "error",
|
|
150
|
+
"error": "max_depth must be between 1 and 3",
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
# Build hierarchical results
|
|
154
|
+
hierarchical_results = []
|
|
155
|
+
for ticket in results:
|
|
156
|
+
ticket_data = {
|
|
157
|
+
"ticket": ticket.model_dump(),
|
|
158
|
+
"hierarchy": {},
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
# Get parent epic if applicable
|
|
162
|
+
parent_epic_id = getattr(ticket, "parent_epic", None)
|
|
163
|
+
if parent_epic_id and max_depth >= 2:
|
|
164
|
+
try:
|
|
165
|
+
parent_epic = await adapter.read(parent_epic_id)
|
|
166
|
+
if parent_epic:
|
|
167
|
+
ticket_data["hierarchy"][
|
|
168
|
+
"parent_epic"
|
|
169
|
+
] = parent_epic.model_dump()
|
|
170
|
+
except Exception:
|
|
171
|
+
pass # Parent not found, continue
|
|
172
|
+
|
|
173
|
+
# Get parent issue if applicable (for tasks)
|
|
174
|
+
parent_issue_id = getattr(ticket, "parent_issue", None)
|
|
175
|
+
if parent_issue_id and max_depth >= 2:
|
|
176
|
+
try:
|
|
177
|
+
parent_issue = await adapter.read(parent_issue_id)
|
|
178
|
+
if parent_issue:
|
|
179
|
+
ticket_data["hierarchy"][
|
|
180
|
+
"parent_issue"
|
|
181
|
+
] = parent_issue.model_dump()
|
|
182
|
+
except Exception:
|
|
183
|
+
pass # Parent not found, continue
|
|
184
|
+
|
|
185
|
+
# Get children if requested
|
|
186
|
+
if include_children and max_depth >= 2:
|
|
187
|
+
children = []
|
|
188
|
+
|
|
189
|
+
# Get child issues (for epics)
|
|
190
|
+
child_issue_ids = getattr(ticket, "child_issues", [])
|
|
191
|
+
for child_id in child_issue_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
|
+
# Get child tasks (for issues)
|
|
200
|
+
child_task_ids = getattr(ticket, "children", [])
|
|
201
|
+
for child_id in child_task_ids:
|
|
202
|
+
try:
|
|
203
|
+
child = await adapter.read(child_id)
|
|
204
|
+
if child:
|
|
205
|
+
children.append(child.model_dump())
|
|
206
|
+
except Exception:
|
|
207
|
+
pass # Child not found, continue
|
|
208
|
+
|
|
209
|
+
if children:
|
|
210
|
+
ticket_data["hierarchy"]["children"] = children
|
|
211
|
+
|
|
212
|
+
hierarchical_results.append(ticket_data)
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
"status": "completed",
|
|
216
|
+
"results": hierarchical_results,
|
|
217
|
+
"count": len(hierarchical_results),
|
|
218
|
+
"query": query,
|
|
219
|
+
"max_depth": max_depth,
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
# Standard search response
|
|
76
223
|
return {
|
|
77
224
|
"status": "completed",
|
|
78
225
|
"tickets": [ticket.model_dump() for ticket in results],
|
|
@@ -83,6 +230,7 @@ async def ticket_search(
|
|
|
83
230
|
"priority": priority,
|
|
84
231
|
"tags": tags,
|
|
85
232
|
"assignee": assignee,
|
|
233
|
+
"project": final_project,
|
|
86
234
|
},
|
|
87
235
|
}
|
|
88
236
|
except Exception as e:
|
|
@@ -95,112 +243,47 @@ async def ticket_search(
|
|
|
95
243
|
@mcp.tool()
|
|
96
244
|
async def ticket_search_hierarchy(
|
|
97
245
|
query: str,
|
|
246
|
+
project_id: str | None = None,
|
|
98
247
|
include_children: bool = True,
|
|
99
248
|
max_depth: int = 3,
|
|
100
249
|
) -> dict[str, Any]:
|
|
101
|
-
"""
|
|
250
|
+
"""DEPRECATED: Use ticket_search(include_hierarchy=True, ...) instead.
|
|
102
251
|
|
|
103
|
-
|
|
104
|
-
hierarchical context (parent epics/issues and child issues/tasks).
|
|
252
|
+
This tool will be removed in v2.0.0. Migrate to the unified ticket_search tool.
|
|
105
253
|
|
|
106
254
|
Args:
|
|
107
255
|
query: Text search query to match against title and description
|
|
256
|
+
project_id: Project/epic ID (required unless default_project configured)
|
|
108
257
|
include_children: Whether to include child tickets in results
|
|
109
258
|
max_depth: Maximum hierarchy depth to include (1-3, default: 3)
|
|
110
259
|
|
|
111
260
|
Returns:
|
|
112
261
|
List of tickets with hierarchy information, or error information
|
|
113
262
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
# Validate max_depth
|
|
119
|
-
if max_depth < 1 or max_depth > 3:
|
|
120
|
-
return {
|
|
121
|
-
"status": "error",
|
|
122
|
-
"error": "max_depth must be between 1 and 3",
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
# Create search query
|
|
126
|
-
search_query = SearchQuery(
|
|
127
|
-
query=query,
|
|
128
|
-
limit=50, # Reasonable limit for hierarchical search
|
|
129
|
-
)
|
|
263
|
+
Migration:
|
|
264
|
+
Before (ticket_search_hierarchy):
|
|
265
|
+
>>> await ticket_search_hierarchy(query="feature", project_id="proj-123", max_depth=2)
|
|
130
266
|
|
|
131
|
-
|
|
132
|
-
|
|
267
|
+
After (ticket_search with include_hierarchy):
|
|
268
|
+
>>> await ticket_search(query="feature", project_id="proj-123", include_hierarchy=True, max_depth=2)
|
|
133
269
|
|
|
134
|
-
|
|
135
|
-
hierarchical_results = []
|
|
136
|
-
for ticket in results:
|
|
137
|
-
ticket_data = {
|
|
138
|
-
"ticket": ticket.model_dump(),
|
|
139
|
-
"hierarchy": {},
|
|
140
|
-
}
|
|
270
|
+
See: docs/UPGRADING-v2.0.md#ticket-search-consolidation
|
|
141
271
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
ticket_data["hierarchy"][
|
|
161
|
-
"parent_issue"
|
|
162
|
-
] = parent_issue.model_dump()
|
|
163
|
-
except Exception:
|
|
164
|
-
pass # Parent not found, continue
|
|
165
|
-
|
|
166
|
-
# Get children if requested
|
|
167
|
-
if include_children and max_depth >= 2:
|
|
168
|
-
children = []
|
|
169
|
-
|
|
170
|
-
# Get child issues (for epics)
|
|
171
|
-
child_issue_ids = getattr(ticket, "child_issues", [])
|
|
172
|
-
for child_id in child_issue_ids:
|
|
173
|
-
try:
|
|
174
|
-
child = await adapter.read(child_id)
|
|
175
|
-
if child:
|
|
176
|
-
children.append(child.model_dump())
|
|
177
|
-
except Exception:
|
|
178
|
-
pass # Child not found, continue
|
|
179
|
-
|
|
180
|
-
# Get child tasks (for issues)
|
|
181
|
-
child_task_ids = getattr(ticket, "children", [])
|
|
182
|
-
for child_id in child_task_ids:
|
|
183
|
-
try:
|
|
184
|
-
child = await adapter.read(child_id)
|
|
185
|
-
if child:
|
|
186
|
-
children.append(child.model_dump())
|
|
187
|
-
except Exception:
|
|
188
|
-
pass # Child not found, continue
|
|
189
|
-
|
|
190
|
-
if children:
|
|
191
|
-
ticket_data["hierarchy"]["children"] = children
|
|
192
|
-
|
|
193
|
-
hierarchical_results.append(ticket_data)
|
|
194
|
-
|
|
195
|
-
return {
|
|
196
|
-
"status": "completed",
|
|
197
|
-
"results": hierarchical_results,
|
|
198
|
-
"count": len(hierarchical_results),
|
|
199
|
-
"query": query,
|
|
200
|
-
"max_depth": max_depth,
|
|
201
|
-
}
|
|
202
|
-
except Exception as e:
|
|
203
|
-
return {
|
|
204
|
-
"status": "error",
|
|
205
|
-
"error": f"Failed to search with hierarchy: {str(e)}",
|
|
206
|
-
}
|
|
272
|
+
"""
|
|
273
|
+
import warnings
|
|
274
|
+
|
|
275
|
+
warnings.warn(
|
|
276
|
+
"ticket_search_hierarchy is deprecated. Use ticket_search(include_hierarchy=True, ...) instead. "
|
|
277
|
+
"See docs/UPGRADING-v2.0.md#ticket-search-consolidation",
|
|
278
|
+
DeprecationWarning,
|
|
279
|
+
stacklevel=2,
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
# Route to unified ticket_search tool
|
|
283
|
+
return await ticket_search(
|
|
284
|
+
query=query,
|
|
285
|
+
project_id=project_id,
|
|
286
|
+
include_hierarchy=True,
|
|
287
|
+
include_children=include_children,
|
|
288
|
+
max_depth=max_depth,
|
|
289
|
+
)
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
"""MCP tools for session and ticket association management.
|
|
2
|
+
|
|
3
|
+
This module implements tools for session management and user ticket operations.
|
|
4
|
+
|
|
5
|
+
Features:
|
|
6
|
+
- user_session: Unified interface for user ticket queries and session info
|
|
7
|
+
- attach_ticket: Associate work session with ticket
|
|
8
|
+
|
|
9
|
+
All tools follow the MCP response pattern:
|
|
10
|
+
{
|
|
11
|
+
"status": "completed" | "error",
|
|
12
|
+
"data": {...}
|
|
13
|
+
}
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any, Literal
|
|
19
|
+
|
|
20
|
+
from ....core.session_state import SessionStateManager
|
|
21
|
+
from ..server_sdk import mcp
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@mcp.tool()
|
|
27
|
+
async def user_session(
|
|
28
|
+
action: Literal["get_my_tickets", "get_session_info"],
|
|
29
|
+
state: str | None = None,
|
|
30
|
+
project_id: str | None = None,
|
|
31
|
+
limit: int = 10,
|
|
32
|
+
) -> dict[str, Any]:
|
|
33
|
+
"""Unified user session management tool.
|
|
34
|
+
|
|
35
|
+
Handles user ticket queries and session information through a single
|
|
36
|
+
interface. This tool consolidates get_my_tickets and get_session_info.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
action: Operation to perform. Valid values:
|
|
40
|
+
- "get_my_tickets": Get tickets assigned to default user
|
|
41
|
+
- "get_session_info": Get current session information
|
|
42
|
+
state: Filter tickets by state (for get_my_tickets only)
|
|
43
|
+
project_id: Filter tickets by project (for get_my_tickets only)
|
|
44
|
+
limit: Maximum tickets to return (for get_my_tickets, default: 10, max: 100)
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Results dictionary containing operation-specific data
|
|
48
|
+
|
|
49
|
+
Raises:
|
|
50
|
+
ValueError: If action is invalid
|
|
51
|
+
|
|
52
|
+
Examples:
|
|
53
|
+
# Get user's tickets
|
|
54
|
+
result = await user_session(
|
|
55
|
+
action="get_my_tickets",
|
|
56
|
+
state="open",
|
|
57
|
+
limit=20
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Get user's tickets with project filter
|
|
61
|
+
result = await user_session(
|
|
62
|
+
action="get_my_tickets",
|
|
63
|
+
project_id="PROJ-123",
|
|
64
|
+
state="in_progress"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Get session info
|
|
68
|
+
result = await user_session(
|
|
69
|
+
action="get_session_info"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
Migration from old tools:
|
|
73
|
+
- get_my_tickets(state=..., limit=...) → user_session(action="get_my_tickets", state=..., limit=...)
|
|
74
|
+
- get_session_info() → user_session(action="get_session_info")
|
|
75
|
+
|
|
76
|
+
See: docs/mcp-api-reference.md for detailed response formats
|
|
77
|
+
"""
|
|
78
|
+
action_lower = action.lower()
|
|
79
|
+
|
|
80
|
+
# Route to appropriate handler based on action
|
|
81
|
+
if action_lower == "get_my_tickets":
|
|
82
|
+
# Inline implementation of get_my_tickets
|
|
83
|
+
try:
|
|
84
|
+
from ....core.models import TicketState
|
|
85
|
+
from ....core.project_config import ConfigResolver, TicketerConfig
|
|
86
|
+
from ..server_sdk import get_adapter
|
|
87
|
+
|
|
88
|
+
# Validate limit
|
|
89
|
+
if limit > 100:
|
|
90
|
+
limit = 100
|
|
91
|
+
|
|
92
|
+
# Load configuration to get default user and project
|
|
93
|
+
resolver = ConfigResolver(project_path=Path.cwd())
|
|
94
|
+
config = resolver.load_project_config() or TicketerConfig()
|
|
95
|
+
|
|
96
|
+
if not config.default_user:
|
|
97
|
+
return {
|
|
98
|
+
"status": "error",
|
|
99
|
+
"error": "No default user configured. Use config_set_default_user() to set a default user first.",
|
|
100
|
+
"setup_command": "config_set_default_user",
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
# Validate project context (Required for list operations)
|
|
104
|
+
final_project = project_id or config.default_project
|
|
105
|
+
|
|
106
|
+
if not final_project:
|
|
107
|
+
return {
|
|
108
|
+
"status": "error",
|
|
109
|
+
"error": "project_id required. Provide project_id parameter or configure default_project.",
|
|
110
|
+
"help": "Use config_set_default_project(project_id='YOUR-PROJECT') to set default project",
|
|
111
|
+
"check_config": "Use config_get() to view current configuration",
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
# Validate state if provided
|
|
115
|
+
state_filter = None
|
|
116
|
+
if state is not None:
|
|
117
|
+
try:
|
|
118
|
+
state_filter = TicketState(state.lower())
|
|
119
|
+
except ValueError:
|
|
120
|
+
valid_states = [s.value for s in TicketState]
|
|
121
|
+
return {
|
|
122
|
+
"status": "error",
|
|
123
|
+
"error": f"Invalid state '{state}'. Must be one of: {', '.join(valid_states)}",
|
|
124
|
+
"valid_states": valid_states,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
# Build filters with required project scoping
|
|
128
|
+
filters: dict[str, Any] = {
|
|
129
|
+
"assignee": config.default_user,
|
|
130
|
+
"project": final_project,
|
|
131
|
+
}
|
|
132
|
+
if state_filter:
|
|
133
|
+
filters["state"] = state_filter
|
|
134
|
+
|
|
135
|
+
# Query adapter
|
|
136
|
+
adapter = get_adapter()
|
|
137
|
+
tickets = await adapter.list(limit=limit, offset=0, filters=filters)
|
|
138
|
+
|
|
139
|
+
# Build adapter metadata
|
|
140
|
+
metadata = {
|
|
141
|
+
"adapter": adapter.adapter_type,
|
|
142
|
+
"adapter_name": adapter.adapter_display_name,
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
"status": "completed",
|
|
147
|
+
**metadata,
|
|
148
|
+
"tickets": [ticket.model_dump() for ticket in tickets],
|
|
149
|
+
"count": len(tickets),
|
|
150
|
+
"user": config.default_user,
|
|
151
|
+
"state_filter": state if state else "all",
|
|
152
|
+
"limit": limit,
|
|
153
|
+
}
|
|
154
|
+
except Exception as e:
|
|
155
|
+
return {
|
|
156
|
+
"status": "error",
|
|
157
|
+
"error": f"Failed to retrieve tickets: {str(e)}",
|
|
158
|
+
}
|
|
159
|
+
elif action_lower == "get_session_info":
|
|
160
|
+
# Inline implementation of get_session_info
|
|
161
|
+
try:
|
|
162
|
+
manager = SessionStateManager(project_path=Path.cwd())
|
|
163
|
+
state_obj = manager.load_session()
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
"success": True,
|
|
167
|
+
"session_id": state_obj.session_id,
|
|
168
|
+
"current_ticket": state_obj.current_ticket,
|
|
169
|
+
"opted_out": state_obj.ticket_opted_out,
|
|
170
|
+
"last_activity": state_obj.last_activity,
|
|
171
|
+
"session_timeout_minutes": 30,
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
except Exception as e:
|
|
175
|
+
logger.error(f"Error in get_session_info: {e}")
|
|
176
|
+
return {
|
|
177
|
+
"success": False,
|
|
178
|
+
"error": str(e),
|
|
179
|
+
}
|
|
180
|
+
else:
|
|
181
|
+
valid_actions = ["get_my_tickets", "get_session_info"]
|
|
182
|
+
return {
|
|
183
|
+
"status": "error",
|
|
184
|
+
"error": f"Invalid action '{action}'. Must be one of: {', '.join(valid_actions)}",
|
|
185
|
+
"valid_actions": valid_actions,
|
|
186
|
+
"hint": "Use user_session(action='get_my_tickets'|'get_session_info', ...)",
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@mcp.tool()
|
|
191
|
+
async def attach_ticket(
|
|
192
|
+
action: str,
|
|
193
|
+
ticket_id: str | None = None,
|
|
194
|
+
) -> dict[str, Any]:
|
|
195
|
+
"""Associate current work session with a ticket.
|
|
196
|
+
|
|
197
|
+
This tool helps track which ticket your current work is related to.
|
|
198
|
+
The association persists for the session (30 minutes of inactivity).
|
|
199
|
+
|
|
200
|
+
**Important**: It's recommended to associate work with a ticket for proper
|
|
201
|
+
tracking and organization.
|
|
202
|
+
|
|
203
|
+
Actions:
|
|
204
|
+
- **set**: Associate work with a specific ticket
|
|
205
|
+
- **clear**: Remove current ticket association
|
|
206
|
+
- **none**: Opt out of ticket association for this session
|
|
207
|
+
- **status**: Check current ticket association
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
action: What to do with the ticket association (set/clear/none/status)
|
|
211
|
+
ticket_id: Ticket ID to associate (e.g., "PROJ-123", UUID), required for 'set'
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
Success status and current session state
|
|
215
|
+
|
|
216
|
+
Examples:
|
|
217
|
+
# Associate with a ticket
|
|
218
|
+
attach_ticket(action="set", ticket_id="PROJ-123")
|
|
219
|
+
|
|
220
|
+
# Opt out for this session
|
|
221
|
+
attach_ticket(action="none")
|
|
222
|
+
|
|
223
|
+
# Check current status
|
|
224
|
+
attach_ticket(action="status")
|
|
225
|
+
|
|
226
|
+
"""
|
|
227
|
+
try:
|
|
228
|
+
manager = SessionStateManager(project_path=Path.cwd())
|
|
229
|
+
state = manager.load_session()
|
|
230
|
+
|
|
231
|
+
if action == "set":
|
|
232
|
+
if not ticket_id:
|
|
233
|
+
return {
|
|
234
|
+
"success": False,
|
|
235
|
+
"error": "ticket_id is required when action='set'",
|
|
236
|
+
"guidance": "Please provide a ticket ID to associate with this session",
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
manager.set_current_ticket(ticket_id)
|
|
240
|
+
return {
|
|
241
|
+
"success": True,
|
|
242
|
+
"message": f"Work session now associated with ticket: {ticket_id}",
|
|
243
|
+
"current_ticket": ticket_id,
|
|
244
|
+
"session_id": state.session_id,
|
|
245
|
+
"opted_out": False,
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
elif action == "clear":
|
|
249
|
+
manager.set_current_ticket(None)
|
|
250
|
+
return {
|
|
251
|
+
"success": True,
|
|
252
|
+
"message": "Ticket association cleared",
|
|
253
|
+
"current_ticket": None,
|
|
254
|
+
"session_id": state.session_id,
|
|
255
|
+
"opted_out": False,
|
|
256
|
+
"guidance": "You can associate with a ticket anytime using attach_ticket(action='set', ticket_id='...')",
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
elif action == "none":
|
|
260
|
+
manager.opt_out_ticket()
|
|
261
|
+
return {
|
|
262
|
+
"success": True,
|
|
263
|
+
"message": "Opted out of ticket association for this session",
|
|
264
|
+
"current_ticket": None,
|
|
265
|
+
"session_id": state.session_id,
|
|
266
|
+
"opted_out": True,
|
|
267
|
+
"note": "This opt-out will reset after 30 minutes of inactivity",
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
elif action == "status":
|
|
271
|
+
current_ticket = manager.get_current_ticket()
|
|
272
|
+
|
|
273
|
+
if state.ticket_opted_out:
|
|
274
|
+
status_msg = "No ticket associated (opted out for this session)"
|
|
275
|
+
elif current_ticket:
|
|
276
|
+
status_msg = f"Currently associated with ticket: {current_ticket}"
|
|
277
|
+
else:
|
|
278
|
+
status_msg = "No ticket associated"
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
"success": True,
|
|
282
|
+
"message": status_msg,
|
|
283
|
+
"current_ticket": current_ticket,
|
|
284
|
+
"session_id": state.session_id,
|
|
285
|
+
"opted_out": state.ticket_opted_out,
|
|
286
|
+
"guidance": (
|
|
287
|
+
(
|
|
288
|
+
"Associate with a ticket: attach_ticket(action='set', ticket_id='...')\n"
|
|
289
|
+
"Opt out: attach_ticket(action='none')"
|
|
290
|
+
)
|
|
291
|
+
if not current_ticket and not state.ticket_opted_out
|
|
292
|
+
else None
|
|
293
|
+
),
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
else:
|
|
297
|
+
return {
|
|
298
|
+
"success": False,
|
|
299
|
+
"error": f"Invalid action: {action}",
|
|
300
|
+
"valid_actions": ["set", "clear", "none", "status"],
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
except Exception as e:
|
|
304
|
+
logger.error(f"Error in attach_ticket: {e}")
|
|
305
|
+
return {
|
|
306
|
+
"success": False,
|
|
307
|
+
"error": str(e),
|
|
308
|
+
}
|