mcp-ticketer 0.3.0__py3-none-any.whl → 2.2.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. mcp_ticketer/__init__.py +10 -10
  2. mcp_ticketer/__version__.py +3 -3
  3. mcp_ticketer/_version_scm.py +1 -0
  4. mcp_ticketer/adapters/__init__.py +2 -0
  5. mcp_ticketer/adapters/aitrackdown.py +930 -52
  6. mcp_ticketer/adapters/asana/__init__.py +15 -0
  7. mcp_ticketer/adapters/asana/adapter.py +1537 -0
  8. mcp_ticketer/adapters/asana/client.py +292 -0
  9. mcp_ticketer/adapters/asana/mappers.py +348 -0
  10. mcp_ticketer/adapters/asana/types.py +146 -0
  11. mcp_ticketer/adapters/github/__init__.py +26 -0
  12. mcp_ticketer/adapters/github/adapter.py +3229 -0
  13. mcp_ticketer/adapters/github/client.py +335 -0
  14. mcp_ticketer/adapters/github/mappers.py +797 -0
  15. mcp_ticketer/adapters/github/queries.py +692 -0
  16. mcp_ticketer/adapters/github/types.py +460 -0
  17. mcp_ticketer/adapters/hybrid.py +58 -16
  18. mcp_ticketer/adapters/jira/__init__.py +35 -0
  19. mcp_ticketer/adapters/jira/adapter.py +1351 -0
  20. mcp_ticketer/adapters/jira/client.py +271 -0
  21. mcp_ticketer/adapters/jira/mappers.py +246 -0
  22. mcp_ticketer/adapters/jira/queries.py +216 -0
  23. mcp_ticketer/adapters/jira/types.py +304 -0
  24. mcp_ticketer/adapters/linear/__init__.py +1 -1
  25. mcp_ticketer/adapters/linear/adapter.py +3810 -462
  26. mcp_ticketer/adapters/linear/client.py +312 -69
  27. mcp_ticketer/adapters/linear/mappers.py +305 -85
  28. mcp_ticketer/adapters/linear/queries.py +317 -17
  29. mcp_ticketer/adapters/linear/types.py +187 -64
  30. mcp_ticketer/adapters/linear.py +2 -2
  31. mcp_ticketer/analysis/__init__.py +56 -0
  32. mcp_ticketer/analysis/dependency_graph.py +255 -0
  33. mcp_ticketer/analysis/health_assessment.py +304 -0
  34. mcp_ticketer/analysis/orphaned.py +218 -0
  35. mcp_ticketer/analysis/project_status.py +594 -0
  36. mcp_ticketer/analysis/similarity.py +224 -0
  37. mcp_ticketer/analysis/staleness.py +266 -0
  38. mcp_ticketer/automation/__init__.py +11 -0
  39. mcp_ticketer/automation/project_updates.py +378 -0
  40. mcp_ticketer/cache/memory.py +9 -8
  41. mcp_ticketer/cli/adapter_diagnostics.py +91 -54
  42. mcp_ticketer/cli/auggie_configure.py +116 -15
  43. mcp_ticketer/cli/codex_configure.py +274 -82
  44. mcp_ticketer/cli/configure.py +1323 -151
  45. mcp_ticketer/cli/cursor_configure.py +314 -0
  46. mcp_ticketer/cli/diagnostics.py +209 -114
  47. mcp_ticketer/cli/discover.py +297 -26
  48. mcp_ticketer/cli/gemini_configure.py +119 -26
  49. mcp_ticketer/cli/init_command.py +880 -0
  50. mcp_ticketer/cli/install_mcp_server.py +418 -0
  51. mcp_ticketer/cli/instruction_commands.py +435 -0
  52. mcp_ticketer/cli/linear_commands.py +256 -130
  53. mcp_ticketer/cli/main.py +140 -1544
  54. mcp_ticketer/cli/mcp_configure.py +1013 -100
  55. mcp_ticketer/cli/mcp_server_commands.py +415 -0
  56. mcp_ticketer/cli/migrate_config.py +12 -8
  57. mcp_ticketer/cli/platform_commands.py +123 -0
  58. mcp_ticketer/cli/platform_detection.py +477 -0
  59. mcp_ticketer/cli/platform_installer.py +545 -0
  60. mcp_ticketer/cli/project_update_commands.py +350 -0
  61. mcp_ticketer/cli/python_detection.py +126 -0
  62. mcp_ticketer/cli/queue_commands.py +15 -15
  63. mcp_ticketer/cli/setup_command.py +794 -0
  64. mcp_ticketer/cli/simple_health.py +84 -59
  65. mcp_ticketer/cli/ticket_commands.py +1375 -0
  66. mcp_ticketer/cli/update_checker.py +313 -0
  67. mcp_ticketer/cli/utils.py +195 -72
  68. mcp_ticketer/core/__init__.py +64 -1
  69. mcp_ticketer/core/adapter.py +618 -18
  70. mcp_ticketer/core/config.py +77 -68
  71. mcp_ticketer/core/env_discovery.py +75 -16
  72. mcp_ticketer/core/env_loader.py +121 -97
  73. mcp_ticketer/core/exceptions.py +32 -24
  74. mcp_ticketer/core/http_client.py +26 -26
  75. mcp_ticketer/core/instructions.py +405 -0
  76. mcp_ticketer/core/label_manager.py +732 -0
  77. mcp_ticketer/core/mappers.py +42 -30
  78. mcp_ticketer/core/milestone_manager.py +252 -0
  79. mcp_ticketer/core/models.py +566 -19
  80. mcp_ticketer/core/onepassword_secrets.py +379 -0
  81. mcp_ticketer/core/priority_matcher.py +463 -0
  82. mcp_ticketer/core/project_config.py +189 -49
  83. mcp_ticketer/core/project_utils.py +281 -0
  84. mcp_ticketer/core/project_validator.py +376 -0
  85. mcp_ticketer/core/registry.py +3 -3
  86. mcp_ticketer/core/session_state.py +176 -0
  87. mcp_ticketer/core/state_matcher.py +592 -0
  88. mcp_ticketer/core/url_parser.py +425 -0
  89. mcp_ticketer/core/validators.py +69 -0
  90. mcp_ticketer/defaults/ticket_instructions.md +644 -0
  91. mcp_ticketer/mcp/__init__.py +29 -1
  92. mcp_ticketer/mcp/__main__.py +60 -0
  93. mcp_ticketer/mcp/server/__init__.py +25 -0
  94. mcp_ticketer/mcp/server/__main__.py +60 -0
  95. mcp_ticketer/mcp/server/constants.py +58 -0
  96. mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
  97. mcp_ticketer/mcp/server/dto.py +195 -0
  98. mcp_ticketer/mcp/server/main.py +1343 -0
  99. mcp_ticketer/mcp/server/response_builder.py +206 -0
  100. mcp_ticketer/mcp/server/routing.py +723 -0
  101. mcp_ticketer/mcp/server/server_sdk.py +151 -0
  102. mcp_ticketer/mcp/server/tools/__init__.py +69 -0
  103. mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
  104. mcp_ticketer/mcp/server/tools/attachment_tools.py +224 -0
  105. mcp_ticketer/mcp/server/tools/bulk_tools.py +330 -0
  106. mcp_ticketer/mcp/server/tools/comment_tools.py +152 -0
  107. mcp_ticketer/mcp/server/tools/config_tools.py +1564 -0
  108. mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
  109. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +942 -0
  110. mcp_ticketer/mcp/server/tools/instruction_tools.py +295 -0
  111. mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
  112. mcp_ticketer/mcp/server/tools/milestone_tools.py +338 -0
  113. mcp_ticketer/mcp/server/tools/pr_tools.py +150 -0
  114. mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
  115. mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
  116. mcp_ticketer/mcp/server/tools/search_tools.py +318 -0
  117. mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
  118. mcp_ticketer/mcp/server/tools/ticket_tools.py +1413 -0
  119. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +364 -0
  120. mcp_ticketer/queue/__init__.py +1 -0
  121. mcp_ticketer/queue/health_monitor.py +168 -136
  122. mcp_ticketer/queue/manager.py +78 -63
  123. mcp_ticketer/queue/queue.py +108 -21
  124. mcp_ticketer/queue/run_worker.py +2 -2
  125. mcp_ticketer/queue/ticket_registry.py +213 -155
  126. mcp_ticketer/queue/worker.py +96 -58
  127. mcp_ticketer/utils/__init__.py +5 -0
  128. mcp_ticketer/utils/token_utils.py +246 -0
  129. mcp_ticketer-2.2.9.dist-info/METADATA +1396 -0
  130. mcp_ticketer-2.2.9.dist-info/RECORD +158 -0
  131. mcp_ticketer-2.2.9.dist-info/top_level.txt +2 -0
  132. py_mcp_installer/examples/phase3_demo.py +178 -0
  133. py_mcp_installer/scripts/manage_version.py +54 -0
  134. py_mcp_installer/setup.py +6 -0
  135. py_mcp_installer/src/py_mcp_installer/__init__.py +153 -0
  136. py_mcp_installer/src/py_mcp_installer/command_builder.py +445 -0
  137. py_mcp_installer/src/py_mcp_installer/config_manager.py +541 -0
  138. py_mcp_installer/src/py_mcp_installer/exceptions.py +243 -0
  139. py_mcp_installer/src/py_mcp_installer/installation_strategy.py +617 -0
  140. py_mcp_installer/src/py_mcp_installer/installer.py +656 -0
  141. py_mcp_installer/src/py_mcp_installer/mcp_inspector.py +750 -0
  142. py_mcp_installer/src/py_mcp_installer/platform_detector.py +451 -0
  143. py_mcp_installer/src/py_mcp_installer/platforms/__init__.py +26 -0
  144. py_mcp_installer/src/py_mcp_installer/platforms/claude_code.py +225 -0
  145. py_mcp_installer/src/py_mcp_installer/platforms/codex.py +181 -0
  146. py_mcp_installer/src/py_mcp_installer/platforms/cursor.py +191 -0
  147. py_mcp_installer/src/py_mcp_installer/types.py +222 -0
  148. py_mcp_installer/src/py_mcp_installer/utils.py +463 -0
  149. py_mcp_installer/tests/__init__.py +0 -0
  150. py_mcp_installer/tests/platforms/__init__.py +0 -0
  151. py_mcp_installer/tests/test_platform_detector.py +17 -0
  152. mcp_ticketer/adapters/github.py +0 -1354
  153. mcp_ticketer/adapters/jira.py +0 -1011
  154. mcp_ticketer/mcp/server.py +0 -2030
  155. mcp_ticketer-0.3.0.dist-info/METADATA +0 -414
  156. mcp_ticketer-0.3.0.dist-info/RECORD +0 -59
  157. mcp_ticketer-0.3.0.dist-info/top_level.txt +0 -1
  158. {mcp_ticketer-0.3.0.dist-info → mcp_ticketer-2.2.9.dist-info}/WHEEL +0 -0
  159. {mcp_ticketer-0.3.0.dist-info → mcp_ticketer-2.2.9.dist-info}/entry_points.txt +0 -0
  160. {mcp_ticketer-0.3.0.dist-info → mcp_ticketer-2.2.9.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,318 @@
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
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ @mcp.tool()
17
+ async def ticket_search(
18
+ query: str | None = None,
19
+ state: str | None = None,
20
+ priority: str | None = None,
21
+ tags: list[str] | None = None,
22
+ assignee: str | None = None,
23
+ project_id: str | None = None,
24
+ milestone_id: str | None = None,
25
+ limit: int = 10,
26
+ include_hierarchy: bool = False,
27
+ include_children: bool = True,
28
+ max_depth: int = 3,
29
+ ) -> dict[str, Any]:
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()
40
+
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)
56
+
57
+ Args:
58
+ query: Text search query to match against title and description
59
+ state: Filter by state - must be one of: open, in_progress, ready, tested, done, closed, waiting, blocked
60
+ priority: Filter by priority - must be one of: low, medium, high, critical
61
+ tags: Filter by tags - tickets must have all specified tags
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)
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)
69
+
70
+ Returns:
71
+ List of tickets matching search criteria, or error information
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
+
92
+ """
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
+
111
+ adapter = get_adapter()
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
+
121
+ # Validate and build search query
122
+ state_enum = None
123
+ if state is not None:
124
+ try:
125
+ state_enum = TicketState(state.lower())
126
+ except ValueError:
127
+ return {
128
+ "status": "error",
129
+ "error": f"Invalid state '{state}'. Must be one of: open, in_progress, ready, tested, done, closed, waiting, blocked",
130
+ }
131
+
132
+ priority_enum = None
133
+ if priority is not None:
134
+ try:
135
+ priority_enum = Priority(priority.lower())
136
+ except ValueError:
137
+ return {
138
+ "status": "error",
139
+ "error": f"Invalid priority '{priority}'. Must be one of: low, medium, high, critical",
140
+ }
141
+
142
+ # Create search query with project scoping
143
+ search_query = SearchQuery(
144
+ query=query,
145
+ state=state_enum,
146
+ priority=priority_enum,
147
+ tags=tags,
148
+ assignee=assignee,
149
+ project=final_project, # Always required for search operations
150
+ limit=min(limit, 100), # Enforce max limit
151
+ )
152
+
153
+ # Execute search via adapter
154
+ results = await adapter.search(search_query)
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
252
+ return {
253
+ "status": "completed",
254
+ "tickets": [ticket.model_dump() for ticket in results],
255
+ "count": len(results),
256
+ "query": {
257
+ "text": query,
258
+ "state": state,
259
+ "priority": priority,
260
+ "tags": tags,
261
+ "assignee": assignee,
262
+ "project": final_project,
263
+ },
264
+ }
265
+ except Exception as e:
266
+ return {
267
+ "status": "error",
268
+ "error": f"Failed to search tickets: {str(e)}",
269
+ }
270
+
271
+
272
+ @mcp.tool()
273
+ async def ticket_search_hierarchy(
274
+ query: str,
275
+ project_id: str | None = None,
276
+ include_children: bool = True,
277
+ max_depth: int = 3,
278
+ ) -> dict[str, Any]:
279
+ """DEPRECATED: Use ticket_search(include_hierarchy=True, ...) instead.
280
+
281
+ This tool will be removed in v2.0.0. Migrate to the unified ticket_search tool.
282
+
283
+ Args:
284
+ query: Text search query to match against title and description
285
+ project_id: Project/epic ID (required unless default_project configured)
286
+ include_children: Whether to include child tickets in results
287
+ max_depth: Maximum hierarchy depth to include (1-3, default: 3)
288
+
289
+ Returns:
290
+ List of tickets with hierarchy information, or error information
291
+
292
+ Migration:
293
+ Before (ticket_search_hierarchy):
294
+ >>> await ticket_search_hierarchy(query="feature", project_id="proj-123", max_depth=2)
295
+
296
+ After (ticket_search with include_hierarchy):
297
+ >>> await ticket_search(query="feature", project_id="proj-123", include_hierarchy=True, max_depth=2)
298
+
299
+ See: docs/UPGRADING-v2.0.md#ticket-search-consolidation
300
+
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
+ }