mcp-ticketer 0.12.0__py3-none-any.whl → 2.2.13__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.

Files changed (129) hide show
  1. mcp_ticketer/__init__.py +10 -10
  2. mcp_ticketer/__version__.py +1 -1
  3. mcp_ticketer/_version_scm.py +1 -0
  4. mcp_ticketer/adapters/aitrackdown.py +507 -6
  5. mcp_ticketer/adapters/asana/adapter.py +229 -0
  6. mcp_ticketer/adapters/asana/mappers.py +14 -0
  7. mcp_ticketer/adapters/github/__init__.py +26 -0
  8. mcp_ticketer/adapters/github/adapter.py +3229 -0
  9. mcp_ticketer/adapters/github/client.py +335 -0
  10. mcp_ticketer/adapters/github/mappers.py +797 -0
  11. mcp_ticketer/adapters/github/queries.py +692 -0
  12. mcp_ticketer/adapters/github/types.py +460 -0
  13. mcp_ticketer/adapters/hybrid.py +47 -5
  14. mcp_ticketer/adapters/jira/__init__.py +35 -0
  15. mcp_ticketer/adapters/jira/adapter.py +1351 -0
  16. mcp_ticketer/adapters/jira/client.py +271 -0
  17. mcp_ticketer/adapters/jira/mappers.py +246 -0
  18. mcp_ticketer/adapters/jira/queries.py +216 -0
  19. mcp_ticketer/adapters/jira/types.py +304 -0
  20. mcp_ticketer/adapters/linear/adapter.py +2730 -139
  21. mcp_ticketer/adapters/linear/client.py +175 -3
  22. mcp_ticketer/adapters/linear/mappers.py +203 -8
  23. mcp_ticketer/adapters/linear/queries.py +280 -3
  24. mcp_ticketer/adapters/linear/types.py +120 -4
  25. mcp_ticketer/analysis/__init__.py +56 -0
  26. mcp_ticketer/analysis/dependency_graph.py +255 -0
  27. mcp_ticketer/analysis/health_assessment.py +304 -0
  28. mcp_ticketer/analysis/orphaned.py +218 -0
  29. mcp_ticketer/analysis/project_status.py +594 -0
  30. mcp_ticketer/analysis/similarity.py +224 -0
  31. mcp_ticketer/analysis/staleness.py +266 -0
  32. mcp_ticketer/automation/__init__.py +11 -0
  33. mcp_ticketer/automation/project_updates.py +378 -0
  34. mcp_ticketer/cli/adapter_diagnostics.py +3 -1
  35. mcp_ticketer/cli/auggie_configure.py +17 -5
  36. mcp_ticketer/cli/codex_configure.py +97 -61
  37. mcp_ticketer/cli/configure.py +1288 -105
  38. mcp_ticketer/cli/cursor_configure.py +314 -0
  39. mcp_ticketer/cli/diagnostics.py +13 -12
  40. mcp_ticketer/cli/discover.py +5 -0
  41. mcp_ticketer/cli/gemini_configure.py +17 -5
  42. mcp_ticketer/cli/init_command.py +880 -0
  43. mcp_ticketer/cli/install_mcp_server.py +418 -0
  44. mcp_ticketer/cli/instruction_commands.py +6 -0
  45. mcp_ticketer/cli/main.py +267 -3175
  46. mcp_ticketer/cli/mcp_configure.py +821 -119
  47. mcp_ticketer/cli/mcp_server_commands.py +415 -0
  48. mcp_ticketer/cli/platform_detection.py +77 -12
  49. mcp_ticketer/cli/platform_installer.py +545 -0
  50. mcp_ticketer/cli/project_update_commands.py +350 -0
  51. mcp_ticketer/cli/setup_command.py +795 -0
  52. mcp_ticketer/cli/simple_health.py +12 -10
  53. mcp_ticketer/cli/ticket_commands.py +705 -103
  54. mcp_ticketer/cli/utils.py +113 -0
  55. mcp_ticketer/core/__init__.py +56 -6
  56. mcp_ticketer/core/adapter.py +533 -2
  57. mcp_ticketer/core/config.py +21 -21
  58. mcp_ticketer/core/exceptions.py +7 -1
  59. mcp_ticketer/core/label_manager.py +732 -0
  60. mcp_ticketer/core/mappers.py +31 -19
  61. mcp_ticketer/core/milestone_manager.py +252 -0
  62. mcp_ticketer/core/models.py +480 -0
  63. mcp_ticketer/core/onepassword_secrets.py +1 -1
  64. mcp_ticketer/core/priority_matcher.py +463 -0
  65. mcp_ticketer/core/project_config.py +132 -14
  66. mcp_ticketer/core/project_utils.py +281 -0
  67. mcp_ticketer/core/project_validator.py +376 -0
  68. mcp_ticketer/core/session_state.py +176 -0
  69. mcp_ticketer/core/state_matcher.py +625 -0
  70. mcp_ticketer/core/url_parser.py +425 -0
  71. mcp_ticketer/core/validators.py +69 -0
  72. mcp_ticketer/mcp/server/__main__.py +2 -1
  73. mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
  74. mcp_ticketer/mcp/server/main.py +106 -25
  75. mcp_ticketer/mcp/server/routing.py +723 -0
  76. mcp_ticketer/mcp/server/server_sdk.py +58 -0
  77. mcp_ticketer/mcp/server/tools/__init__.py +33 -11
  78. mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
  79. mcp_ticketer/mcp/server/tools/attachment_tools.py +5 -5
  80. mcp_ticketer/mcp/server/tools/bulk_tools.py +259 -202
  81. mcp_ticketer/mcp/server/tools/comment_tools.py +74 -12
  82. mcp_ticketer/mcp/server/tools/config_tools.py +1391 -145
  83. mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
  84. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +870 -460
  85. mcp_ticketer/mcp/server/tools/instruction_tools.py +7 -5
  86. mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
  87. mcp_ticketer/mcp/server/tools/milestone_tools.py +338 -0
  88. mcp_ticketer/mcp/server/tools/pr_tools.py +3 -7
  89. mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
  90. mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
  91. mcp_ticketer/mcp/server/tools/search_tools.py +209 -97
  92. mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
  93. mcp_ticketer/mcp/server/tools/ticket_tools.py +1107 -124
  94. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +218 -236
  95. mcp_ticketer/queue/queue.py +68 -0
  96. mcp_ticketer/queue/worker.py +1 -1
  97. mcp_ticketer/utils/__init__.py +5 -0
  98. mcp_ticketer/utils/token_utils.py +246 -0
  99. mcp_ticketer-2.2.13.dist-info/METADATA +1396 -0
  100. mcp_ticketer-2.2.13.dist-info/RECORD +158 -0
  101. mcp_ticketer-2.2.13.dist-info/top_level.txt +2 -0
  102. py_mcp_installer/examples/phase3_demo.py +178 -0
  103. py_mcp_installer/scripts/manage_version.py +54 -0
  104. py_mcp_installer/setup.py +6 -0
  105. py_mcp_installer/src/py_mcp_installer/__init__.py +153 -0
  106. py_mcp_installer/src/py_mcp_installer/command_builder.py +445 -0
  107. py_mcp_installer/src/py_mcp_installer/config_manager.py +541 -0
  108. py_mcp_installer/src/py_mcp_installer/exceptions.py +243 -0
  109. py_mcp_installer/src/py_mcp_installer/installation_strategy.py +617 -0
  110. py_mcp_installer/src/py_mcp_installer/installer.py +656 -0
  111. py_mcp_installer/src/py_mcp_installer/mcp_inspector.py +750 -0
  112. py_mcp_installer/src/py_mcp_installer/platform_detector.py +451 -0
  113. py_mcp_installer/src/py_mcp_installer/platforms/__init__.py +26 -0
  114. py_mcp_installer/src/py_mcp_installer/platforms/claude_code.py +225 -0
  115. py_mcp_installer/src/py_mcp_installer/platforms/codex.py +181 -0
  116. py_mcp_installer/src/py_mcp_installer/platforms/cursor.py +191 -0
  117. py_mcp_installer/src/py_mcp_installer/types.py +222 -0
  118. py_mcp_installer/src/py_mcp_installer/utils.py +463 -0
  119. py_mcp_installer/tests/__init__.py +0 -0
  120. py_mcp_installer/tests/platforms/__init__.py +0 -0
  121. py_mcp_installer/tests/test_platform_detector.py +17 -0
  122. mcp_ticketer/adapters/github.py +0 -1574
  123. mcp_ticketer/adapters/jira.py +0 -1258
  124. mcp_ticketer-0.12.0.dist-info/METADATA +0 -550
  125. mcp_ticketer-0.12.0.dist-info/RECORD +0 -91
  126. mcp_ticketer-0.12.0.dist-info/top_level.txt +0 -1
  127. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.2.13.dist-info}/WHEEL +0 -0
  128. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.2.13.dist-info}/entry_points.txt +0 -0
  129. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.2.13.dist-info}/licenses/LICENSE +0 -0
@@ -4,11 +4,14 @@ 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
10
11
  from ..server_sdk import get_adapter, mcp
11
12
 
13
+ logger = logging.getLogger(__name__)
14
+
12
15
 
13
16
  @mcp.tool()
14
17
  async def ticket_search(
@@ -17,12 +20,39 @@ async def ticket_search(
17
20
  priority: str | None = None,
18
21
  tags: list[str] | None = None,
19
22
  assignee: str | None = None,
23
+ project_id: str | None = None,
24
+ milestone_id: str | None = None,
20
25
  limit: int = 10,
26
+ include_hierarchy: bool = False,
27
+ include_children: bool = True,
28
+ max_depth: int = 3,
21
29
  ) -> dict[str, Any]:
22
- """Search tickets using advanced filters.
30
+ """Search tickets with optional hierarchy information and milestone filtering.
31
+
32
+ **Consolidates:**
33
+ - ticket_search() → Default behavior (include_hierarchy=False)
34
+ - ticket_search_hierarchy() → Set include_hierarchy=True
35
+
36
+ ⚠️ Project Filtering Required:
37
+ This tool requires project_id parameter OR default_project configuration.
38
+ To set default project: config_set_default_project(project_id="YOUR-PROJECT")
39
+ To check current config: config_get()
23
40
 
24
- Searches for tickets matching the specified criteria. All filters are
25
- optional and can be combined.
41
+ Exception: Single ticket operations (ticket_read) don't require project filtering.
42
+
43
+ **Search Filters:**
44
+ - query: Text search in title and description
45
+ - state: Filter by workflow state
46
+ - priority: Filter by priority level
47
+ - tags: Filter by tags (AND logic)
48
+ - assignee: Filter by assigned user
49
+ - project_id: Scope to specific project
50
+ - milestone_id: Filter by milestone (NEW in 1M-607)
51
+
52
+ **Hierarchy Options:**
53
+ - include_hierarchy: Include parent/child relationships (default: False)
54
+ - include_children: Include child tickets (default: True, requires include_hierarchy=True)
55
+ - max_depth: Maximum hierarchy depth (default: 3, requires include_hierarchy=True)
26
56
 
27
57
  Args:
28
58
  query: Text search query to match against title and description
@@ -30,15 +60,64 @@ async def ticket_search(
30
60
  priority: Filter by priority - must be one of: low, medium, high, critical
31
61
  tags: Filter by tags - tickets must have all specified tags
32
62
  assignee: Filter by assigned user ID or email
63
+ project_id: Project/epic ID (required unless default_project configured)
64
+ milestone_id: Filter by milestone ID (NEW in 1M-607)
33
65
  limit: Maximum number of results to return (default: 10, max: 100)
66
+ include_hierarchy: Include parent/child relationships (default: False)
67
+ include_children: Include child tickets in hierarchy (default: True)
68
+ max_depth: Maximum hierarchy depth to traverse (default: 3)
34
69
 
35
70
  Returns:
36
71
  List of tickets matching search criteria, or error information
37
72
 
73
+ Examples:
74
+ # Simple search (backward compatible)
75
+ await ticket_search(query="authentication bug", state="open", limit=5)
76
+
77
+ # Search with hierarchy
78
+ await ticket_search(
79
+ query="oauth implementation",
80
+ project_id="proj-123",
81
+ include_hierarchy=True,
82
+ max_depth=2
83
+ )
84
+
85
+ # Search within milestone
86
+ await ticket_search(
87
+ milestone_id="milestone-123",
88
+ state="open",
89
+ limit=20
90
+ )
91
+
38
92
  """
39
93
  try:
94
+ # Validate project context (NEW: Required for search operations)
95
+ from pathlib import Path
96
+
97
+ from ....core.project_config import ConfigResolver
98
+
99
+ resolver = ConfigResolver(project_path=Path.cwd())
100
+ config = resolver.load_project_config()
101
+ final_project = project_id or (config.default_project if config else None)
102
+
103
+ if not final_project:
104
+ return {
105
+ "status": "error",
106
+ "error": "project_id required. Provide project_id parameter or configure default_project.",
107
+ "help": "Use config_set_default_project(project_id='YOUR-PROJECT') to set default project",
108
+ "check_config": "Use config_get() to view current configuration",
109
+ }
110
+
40
111
  adapter = get_adapter()
41
112
 
113
+ # Add warning for unscoped searches
114
+ if not query and not (state or priority or tags or assignee):
115
+ logging.warning(
116
+ "Unscoped search with no query or filters. "
117
+ "This will search ALL tickets across all projects. "
118
+ "Tip: Configure default_project or default_team for automatic scoping."
119
+ )
120
+
42
121
  # Validate and build search query
43
122
  state_enum = None
44
123
  if state is not None:
@@ -60,19 +139,116 @@ async def ticket_search(
60
139
  "error": f"Invalid priority '{priority}'. Must be one of: low, medium, high, critical",
61
140
  }
62
141
 
63
- # Create search query
142
+ # Create search query with project scoping
64
143
  search_query = SearchQuery(
65
144
  query=query,
66
145
  state=state_enum,
67
146
  priority=priority_enum,
68
147
  tags=tags,
69
148
  assignee=assignee,
149
+ project=final_project, # Always required for search operations
70
150
  limit=min(limit, 100), # Enforce max limit
71
151
  )
72
152
 
73
153
  # Execute search via adapter
74
154
  results = await adapter.search(search_query)
75
155
 
156
+ # Filter by milestone if requested (NEW in 1M-607)
157
+ if milestone_id:
158
+ try:
159
+ # Get issues in milestone
160
+ milestone_issues = await adapter.milestone_get_issues(
161
+ milestone_id, state=state
162
+ )
163
+ milestone_issue_ids = {issue.id for issue in milestone_issues}
164
+
165
+ # Filter search results to only include milestone issues
166
+ results = [
167
+ ticket for ticket in results if ticket.id in milestone_issue_ids
168
+ ]
169
+ except Exception as e:
170
+ logger.warning(f"Failed to filter by milestone {milestone_id}: {e}")
171
+ # Continue with unfiltered results if milestone filtering fails
172
+
173
+ # Add hierarchy if requested
174
+ if include_hierarchy:
175
+ # Validate max_depth
176
+ if max_depth < 1 or max_depth > 3:
177
+ return {
178
+ "status": "error",
179
+ "error": "max_depth must be between 1 and 3",
180
+ }
181
+
182
+ # Build hierarchical results
183
+ hierarchical_results = []
184
+ for ticket in results:
185
+ ticket_data = {
186
+ "ticket": ticket.model_dump(),
187
+ "hierarchy": {},
188
+ }
189
+
190
+ # Get parent epic if applicable
191
+ parent_epic_id = getattr(ticket, "parent_epic", None)
192
+ if parent_epic_id and max_depth >= 2:
193
+ try:
194
+ parent_epic = await adapter.read(parent_epic_id)
195
+ if parent_epic:
196
+ ticket_data["hierarchy"][
197
+ "parent_epic"
198
+ ] = parent_epic.model_dump()
199
+ except Exception:
200
+ pass # Parent not found, continue
201
+
202
+ # Get parent issue if applicable (for tasks)
203
+ parent_issue_id = getattr(ticket, "parent_issue", None)
204
+ if parent_issue_id and max_depth >= 2:
205
+ try:
206
+ parent_issue = await adapter.read(parent_issue_id)
207
+ if parent_issue:
208
+ ticket_data["hierarchy"][
209
+ "parent_issue"
210
+ ] = parent_issue.model_dump()
211
+ except Exception:
212
+ pass # Parent not found, continue
213
+
214
+ # Get children if requested
215
+ if include_children and max_depth >= 2:
216
+ children = []
217
+
218
+ # Get child issues (for epics)
219
+ child_issue_ids = getattr(ticket, "child_issues", [])
220
+ for child_id in child_issue_ids:
221
+ try:
222
+ child = await adapter.read(child_id)
223
+ if child:
224
+ children.append(child.model_dump())
225
+ except Exception:
226
+ pass # Child not found, continue
227
+
228
+ # Get child tasks (for issues)
229
+ child_task_ids = getattr(ticket, "children", [])
230
+ for child_id in child_task_ids:
231
+ try:
232
+ child = await adapter.read(child_id)
233
+ if child:
234
+ children.append(child.model_dump())
235
+ except Exception:
236
+ pass # Child not found, continue
237
+
238
+ if children:
239
+ ticket_data["hierarchy"]["children"] = children
240
+
241
+ hierarchical_results.append(ticket_data)
242
+
243
+ return {
244
+ "status": "completed",
245
+ "results": hierarchical_results,
246
+ "count": len(hierarchical_results),
247
+ "query": query,
248
+ "max_depth": max_depth,
249
+ }
250
+
251
+ # Standard search response
76
252
  return {
77
253
  "status": "completed",
78
254
  "tickets": [ticket.model_dump() for ticket in results],
@@ -83,6 +259,7 @@ async def ticket_search(
83
259
  "priority": priority,
84
260
  "tags": tags,
85
261
  "assignee": assignee,
262
+ "project": final_project,
86
263
  },
87
264
  }
88
265
  except Exception as e:
@@ -95,112 +272,47 @@ async def ticket_search(
95
272
  @mcp.tool()
96
273
  async def ticket_search_hierarchy(
97
274
  query: str,
275
+ project_id: str | None = None,
98
276
  include_children: bool = True,
99
277
  max_depth: int = 3,
100
278
  ) -> dict[str, Any]:
101
- """Search tickets and include their hierarchy.
279
+ """DEPRECATED: Use ticket_search(include_hierarchy=True, ...) instead.
102
280
 
103
- Performs a text search and returns matching tickets along with their
104
- hierarchical context (parent epics/issues and child issues/tasks).
281
+ This tool will be removed in v2.0.0. Migrate to the unified ticket_search tool.
105
282
 
106
283
  Args:
107
284
  query: Text search query to match against title and description
285
+ project_id: Project/epic ID (required unless default_project configured)
108
286
  include_children: Whether to include child tickets in results
109
287
  max_depth: Maximum hierarchy depth to include (1-3, default: 3)
110
288
 
111
289
  Returns:
112
290
  List of tickets with hierarchy information, or error information
113
291
 
114
- """
115
- try:
116
- adapter = get_adapter()
292
+ Migration:
293
+ Before (ticket_search_hierarchy):
294
+ >>> await ticket_search_hierarchy(query="feature", project_id="proj-123", max_depth=2)
117
295
 
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
- }
296
+ After (ticket_search with include_hierarchy):
297
+ >>> await ticket_search(query="feature", project_id="proj-123", include_hierarchy=True, max_depth=2)
124
298
 
125
- # Create search query
126
- search_query = SearchQuery(
127
- query=query,
128
- limit=50, # Reasonable limit for hierarchical search
129
- )
130
-
131
- # Execute search via adapter
132
- results = await adapter.search(search_query)
133
-
134
- # Build hierarchical results
135
- hierarchical_results = []
136
- for ticket in results:
137
- ticket_data = {
138
- "ticket": ticket.model_dump(),
139
- "hierarchy": {},
140
- }
299
+ See: docs/UPGRADING-v2.0.md#ticket-search-consolidation
141
300
 
142
- # Get parent epic if applicable
143
- parent_epic_id = getattr(ticket, "parent_epic", None)
144
- if parent_epic_id and max_depth >= 2:
145
- try:
146
- parent_epic = await adapter.read(parent_epic_id)
147
- if parent_epic:
148
- ticket_data["hierarchy"][
149
- "parent_epic"
150
- ] = parent_epic.model_dump()
151
- except Exception:
152
- pass # Parent not found, continue
153
-
154
- # Get parent issue if applicable (for tasks)
155
- parent_issue_id = getattr(ticket, "parent_issue", None)
156
- if parent_issue_id and max_depth >= 2:
157
- try:
158
- parent_issue = await adapter.read(parent_issue_id)
159
- if parent_issue:
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
- }
301
+ """
302
+ import warnings
303
+
304
+ warnings.warn(
305
+ "ticket_search_hierarchy is deprecated. Use ticket_search(include_hierarchy=True, ...) instead. "
306
+ "See docs/UPGRADING-v2.0.md#ticket-search-consolidation",
307
+ DeprecationWarning,
308
+ stacklevel=2,
309
+ )
310
+
311
+ # Route to unified ticket_search tool
312
+ return await ticket_search(
313
+ query=query,
314
+ project_id=project_id,
315
+ include_hierarchy=True,
316
+ include_children=include_children,
317
+ max_depth=max_depth,
318
+ )
@@ -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
+ }