mcp-ticketer 0.12.0__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.

Files changed (87) hide show
  1. mcp_ticketer/__init__.py +10 -10
  2. mcp_ticketer/__version__.py +1 -1
  3. mcp_ticketer/adapters/aitrackdown.py +385 -6
  4. mcp_ticketer/adapters/asana/adapter.py +108 -0
  5. mcp_ticketer/adapters/asana/mappers.py +14 -0
  6. mcp_ticketer/adapters/github.py +525 -11
  7. mcp_ticketer/adapters/hybrid.py +47 -5
  8. mcp_ticketer/adapters/jira.py +521 -0
  9. mcp_ticketer/adapters/linear/adapter.py +1784 -101
  10. mcp_ticketer/adapters/linear/client.py +85 -3
  11. mcp_ticketer/adapters/linear/mappers.py +96 -8
  12. mcp_ticketer/adapters/linear/queries.py +168 -1
  13. mcp_ticketer/adapters/linear/types.py +80 -4
  14. mcp_ticketer/analysis/__init__.py +56 -0
  15. mcp_ticketer/analysis/dependency_graph.py +255 -0
  16. mcp_ticketer/analysis/health_assessment.py +304 -0
  17. mcp_ticketer/analysis/orphaned.py +218 -0
  18. mcp_ticketer/analysis/project_status.py +594 -0
  19. mcp_ticketer/analysis/similarity.py +224 -0
  20. mcp_ticketer/analysis/staleness.py +266 -0
  21. mcp_ticketer/automation/__init__.py +11 -0
  22. mcp_ticketer/automation/project_updates.py +378 -0
  23. mcp_ticketer/cli/adapter_diagnostics.py +3 -1
  24. mcp_ticketer/cli/auggie_configure.py +17 -5
  25. mcp_ticketer/cli/codex_configure.py +97 -61
  26. mcp_ticketer/cli/configure.py +851 -103
  27. mcp_ticketer/cli/cursor_configure.py +314 -0
  28. mcp_ticketer/cli/diagnostics.py +13 -12
  29. mcp_ticketer/cli/discover.py +5 -0
  30. mcp_ticketer/cli/gemini_configure.py +17 -5
  31. mcp_ticketer/cli/init_command.py +880 -0
  32. mcp_ticketer/cli/instruction_commands.py +6 -0
  33. mcp_ticketer/cli/main.py +233 -3151
  34. mcp_ticketer/cli/mcp_configure.py +672 -98
  35. mcp_ticketer/cli/mcp_server_commands.py +415 -0
  36. mcp_ticketer/cli/platform_detection.py +77 -12
  37. mcp_ticketer/cli/platform_installer.py +536 -0
  38. mcp_ticketer/cli/project_update_commands.py +350 -0
  39. mcp_ticketer/cli/setup_command.py +639 -0
  40. mcp_ticketer/cli/simple_health.py +12 -10
  41. mcp_ticketer/cli/ticket_commands.py +264 -24
  42. mcp_ticketer/core/__init__.py +28 -6
  43. mcp_ticketer/core/adapter.py +166 -1
  44. mcp_ticketer/core/config.py +21 -21
  45. mcp_ticketer/core/exceptions.py +7 -1
  46. mcp_ticketer/core/label_manager.py +732 -0
  47. mcp_ticketer/core/mappers.py +31 -19
  48. mcp_ticketer/core/models.py +135 -0
  49. mcp_ticketer/core/onepassword_secrets.py +1 -1
  50. mcp_ticketer/core/priority_matcher.py +463 -0
  51. mcp_ticketer/core/project_config.py +132 -14
  52. mcp_ticketer/core/session_state.py +171 -0
  53. mcp_ticketer/core/state_matcher.py +592 -0
  54. mcp_ticketer/core/url_parser.py +425 -0
  55. mcp_ticketer/core/validators.py +69 -0
  56. mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
  57. mcp_ticketer/mcp/server/main.py +106 -25
  58. mcp_ticketer/mcp/server/routing.py +655 -0
  59. mcp_ticketer/mcp/server/server_sdk.py +58 -0
  60. mcp_ticketer/mcp/server/tools/__init__.py +31 -12
  61. mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
  62. mcp_ticketer/mcp/server/tools/attachment_tools.py +6 -8
  63. mcp_ticketer/mcp/server/tools/bulk_tools.py +259 -202
  64. mcp_ticketer/mcp/server/tools/comment_tools.py +74 -12
  65. mcp_ticketer/mcp/server/tools/config_tools.py +1184 -136
  66. mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
  67. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +870 -460
  68. mcp_ticketer/mcp/server/tools/instruction_tools.py +7 -5
  69. mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
  70. mcp_ticketer/mcp/server/tools/pr_tools.py +3 -7
  71. mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
  72. mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
  73. mcp_ticketer/mcp/server/tools/search_tools.py +180 -97
  74. mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
  75. mcp_ticketer/mcp/server/tools/ticket_tools.py +1070 -123
  76. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +218 -236
  77. mcp_ticketer/queue/worker.py +1 -1
  78. mcp_ticketer/utils/__init__.py +5 -0
  79. mcp_ticketer/utils/token_utils.py +246 -0
  80. mcp_ticketer-2.0.1.dist-info/METADATA +1366 -0
  81. mcp_ticketer-2.0.1.dist-info/RECORD +122 -0
  82. mcp_ticketer-0.12.0.dist-info/METADATA +0 -550
  83. mcp_ticketer-0.12.0.dist-info/RECORD +0 -91
  84. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/WHEEL +0 -0
  85. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/entry_points.txt +0 -0
  86. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/licenses/LICENSE +0 -0
  87. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,378 @@
1
+ """Automatic project update posting on ticket transitions.
2
+
3
+ This module implements automatic project status updates that trigger when
4
+ tickets are transitioned, generating real-time epic/project status summaries.
5
+
6
+ Design Decision: Reuse StatusAnalyzer from 1M-316
7
+ ------------------------------------------------
8
+ The project_status tool (1M-316) already provides comprehensive analysis via
9
+ StatusAnalyzer. This module focuses on:
10
+ 1. Hooking into ticket transitions
11
+ 2. Formatting analysis as concise updates
12
+ 3. Posting to Linear automatically
13
+
14
+ Related Tickets:
15
+ - 1M-315: Automatic project update posting
16
+ - 1M-316: project_status tool (provides StatusAnalyzer)
17
+ """
18
+
19
+ import logging
20
+ from datetime import datetime, timezone
21
+ from typing import Any
22
+
23
+ from ..analysis.project_status import StatusAnalyzer
24
+ from ..core.adapter import BaseAdapter
25
+ from ..core.models import ProjectUpdateHealth, Task
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ class AutoProjectUpdateManager:
31
+ """Manager for automatic project status updates on ticket transitions.
32
+
33
+ This class hooks into ticket transitions and automatically generates
34
+ project status updates using the StatusAnalyzer from 1M-316.
35
+
36
+ Design Principles:
37
+ - Fail gracefully: Update failures don't block ticket transitions
38
+ - Reuse existing analysis: Leverage StatusAnalyzer for consistency
39
+ - Concise summaries: Focus on key information for readability
40
+ - Health tracking: Include health status in all updates
41
+
42
+ Attributes:
43
+ config: Project configuration dictionary
44
+ adapter: Ticket adapter instance
45
+ analyzer: StatusAnalyzer for project analysis
46
+
47
+ """
48
+
49
+ def __init__(self, config: dict[str, Any], adapter: BaseAdapter):
50
+ """Initialize the auto project update manager.
51
+
52
+ Args:
53
+ config: Project configuration dictionary
54
+ adapter: Ticket adapter instance
55
+
56
+ """
57
+ self.config = config
58
+ self.adapter = adapter
59
+ self.analyzer = StatusAnalyzer()
60
+
61
+ def is_enabled(self) -> bool:
62
+ """Check if automatic project updates are enabled.
63
+
64
+ Returns:
65
+ True if auto_project_updates.enabled is True in config
66
+
67
+ """
68
+ auto_updates_config = self.config.get("auto_project_updates", {})
69
+ return auto_updates_config.get("enabled", False)
70
+
71
+ def get_update_frequency(self) -> str:
72
+ """Get configured update frequency.
73
+
74
+ Returns:
75
+ Update frequency: "on_transition", "on_completion", or "daily"
76
+ Default: "on_transition"
77
+
78
+ """
79
+ auto_updates_config = self.config.get("auto_project_updates", {})
80
+ return auto_updates_config.get("update_frequency", "on_transition")
81
+
82
+ def get_health_tracking_enabled(self) -> bool:
83
+ """Check if health tracking is enabled.
84
+
85
+ Returns:
86
+ True if auto_project_updates.health_tracking is True
87
+ Default: True
88
+
89
+ """
90
+ auto_updates_config = self.config.get("auto_project_updates", {})
91
+ return auto_updates_config.get("health_tracking", True)
92
+
93
+ async def create_transition_update(
94
+ self,
95
+ ticket_id: str,
96
+ ticket_title: str,
97
+ old_state: str,
98
+ new_state: str,
99
+ parent_epic: str,
100
+ ) -> dict[str, Any]:
101
+ """Create and post a project update for a ticket transition.
102
+
103
+ This is the main entry point called from ticket_transition.
104
+
105
+ Args:
106
+ ticket_id: ID of the transitioned ticket
107
+ ticket_title: Title of the transitioned ticket
108
+ old_state: Previous state of the ticket
109
+ new_state: New state of the ticket
110
+ parent_epic: ID of the parent epic/project
111
+
112
+ Returns:
113
+ Dictionary containing:
114
+ - status: "completed" or "error"
115
+ - update_id: ID of created update (if successful)
116
+ - error: Error message (if failed)
117
+
118
+ Error Handling:
119
+ - Failures don't propagate to caller (ticket transition continues)
120
+ - Errors are logged for debugging
121
+ - Returns error status for observability
122
+
123
+ """
124
+ try:
125
+ # Check if adapter supports project updates
126
+ if not hasattr(self.adapter, "create_project_update"):
127
+ logger.debug(
128
+ f"Adapter '{self.adapter.adapter_type}' does not support "
129
+ f"project updates, skipping auto-update"
130
+ )
131
+ return {
132
+ "status": "skipped",
133
+ "reason": "adapter_unsupported",
134
+ }
135
+
136
+ # Fetch epic/project data
137
+ epic_data = await self._fetch_epic_data(parent_epic)
138
+ if not epic_data:
139
+ logger.warning(
140
+ f"Could not fetch epic data for {parent_epic}, "
141
+ f"skipping auto-update"
142
+ )
143
+ return {
144
+ "status": "error",
145
+ "error": f"Epic {parent_epic} not found",
146
+ }
147
+
148
+ # Fetch all tickets in the epic
149
+ tickets = await self._fetch_epic_tickets(parent_epic)
150
+ if not tickets:
151
+ logger.debug(
152
+ f"No tickets found for epic {parent_epic}, "
153
+ f"creating minimal update"
154
+ )
155
+ # Still create update with just the transition info
156
+ tickets = []
157
+
158
+ # Perform analysis using StatusAnalyzer
159
+ project_name = epic_data.get("name", parent_epic)
160
+ analysis = self.analyzer.analyze(parent_epic, project_name, tickets)
161
+
162
+ # Format as markdown summary
163
+ summary = self._format_markdown_summary(
164
+ analysis=analysis,
165
+ ticket_id=ticket_id,
166
+ ticket_title=ticket_title,
167
+ old_state=old_state,
168
+ new_state=new_state,
169
+ )
170
+
171
+ # Determine health status
172
+ health = None
173
+ if self.get_health_tracking_enabled():
174
+ health_value = analysis.health
175
+ # Map health string to ProjectUpdateHealth enum
176
+ if health_value:
177
+ try:
178
+ health = ProjectUpdateHealth(health_value.lower())
179
+ except ValueError:
180
+ logger.warning(
181
+ f"Invalid health value '{health_value}', "
182
+ f"defaulting to None"
183
+ )
184
+
185
+ # Post update using project_update_create via adapter
186
+ update = await self.adapter.create_project_update(
187
+ project_id=parent_epic,
188
+ body=summary,
189
+ health=health,
190
+ )
191
+
192
+ logger.info(
193
+ f"Created automatic project update for {parent_epic} "
194
+ f"after {ticket_id} transition"
195
+ )
196
+
197
+ return {
198
+ "status": "completed",
199
+ "update_id": update.id if hasattr(update, "id") else None,
200
+ "project_id": parent_epic,
201
+ }
202
+
203
+ except Exception as e:
204
+ # Log error but don't propagate (transition should still succeed)
205
+ logger.error(
206
+ f"Failed to create automatic project update: {e}",
207
+ exc_info=True,
208
+ )
209
+ return {
210
+ "status": "error",
211
+ "error": str(e),
212
+ }
213
+
214
+ async def _fetch_epic_data(self, epic_id: str) -> dict[str, Any] | None:
215
+ """Fetch epic/project data from adapter.
216
+
217
+ Args:
218
+ epic_id: Epic/project ID
219
+
220
+ Returns:
221
+ Epic data dictionary or None if not found
222
+
223
+ """
224
+ try:
225
+ if hasattr(self.adapter, "get_epic"):
226
+ epic = await self.adapter.get_epic(epic_id)
227
+ if epic:
228
+ # Convert to dict for processing
229
+ if hasattr(epic, "model_dump"):
230
+ return epic.model_dump()
231
+ elif hasattr(epic, "dict"):
232
+ return epic.dict()
233
+ else:
234
+ return {"id": epic_id, "name": str(epic)}
235
+ return None
236
+ except Exception as e:
237
+ logger.debug(f"Error fetching epic {epic_id}: {e}")
238
+ return None
239
+
240
+ async def _fetch_epic_tickets(self, epic_id: str) -> list[Task]:
241
+ """Fetch all tickets in an epic.
242
+
243
+ Args:
244
+ epic_id: Epic/project ID
245
+
246
+ Returns:
247
+ List of Task objects in the epic
248
+
249
+ """
250
+ try:
251
+ if hasattr(self.adapter, "list_issues_by_epic"):
252
+ tickets = await self.adapter.list_issues_by_epic(epic_id)
253
+ return tickets
254
+ elif hasattr(self.adapter, "list"):
255
+ # Fallback to generic list with parent filter
256
+ tickets = await self.adapter.list(
257
+ limit=100,
258
+ offset=0,
259
+ filters={"parent_epic": epic_id},
260
+ )
261
+ return tickets
262
+ return []
263
+ except Exception as e:
264
+ logger.debug(f"Error fetching tickets for epic {epic_id}: {e}")
265
+ return []
266
+
267
+ def _format_markdown_summary(
268
+ self,
269
+ analysis: Any,
270
+ ticket_id: str,
271
+ ticket_title: str,
272
+ old_state: str,
273
+ new_state: str,
274
+ ) -> str:
275
+ """Format analysis as markdown summary.
276
+
277
+ This creates a concise, readable summary optimized for Linear's
278
+ project update interface.
279
+
280
+ Args:
281
+ analysis: ProjectStatusResult from StatusAnalyzer
282
+ ticket_id: ID of transitioned ticket
283
+ ticket_title: Title of transitioned ticket
284
+ old_state: Previous state
285
+ new_state: New state
286
+
287
+ Returns:
288
+ Formatted markdown summary
289
+
290
+ """
291
+ # Header: Transition trigger
292
+ lines = [
293
+ "## Progress Update (Automated)",
294
+ "",
295
+ f'**Ticket Transitioned**: {ticket_id} "{ticket_title}" '
296
+ f"→ **{new_state.upper()}**",
297
+ "",
298
+ ]
299
+
300
+ # Epic status summary
301
+ summary = analysis.summary
302
+ total = summary.get("total", 0)
303
+ done_count = summary.get("done", 0) + summary.get("closed", 0)
304
+ in_progress = summary.get("in_progress", 0)
305
+ blocked = summary.get("blocked", 0)
306
+
307
+ completion_pct = int(analysis.health_metrics.completion_rate * 100)
308
+
309
+ lines.extend(
310
+ [
311
+ f"**Epic Status** ({analysis.project_name}):",
312
+ f"- Completed: {done_count}/{total} tickets ({completion_pct}%)",
313
+ f"- In Progress: {in_progress} ticket{'s' if in_progress != 1 else ''}",
314
+ f"- Blocked: {blocked} ticket{'s' if blocked != 1 else ''}",
315
+ "",
316
+ ]
317
+ )
318
+
319
+ # Recent completions (if any)
320
+ if done_count > 0:
321
+ lines.append("**Recent Completions**:")
322
+ # Show up to 3 most recent completions
323
+ completed_tickets = [
324
+ rec for rec in analysis.recommended_next if hasattr(rec, "ticket_id")
325
+ ][:3]
326
+ if completed_tickets:
327
+ for ticket in completed_tickets:
328
+ lines.append(f"- {ticket.ticket_id}: {ticket.title}")
329
+ else:
330
+ lines.append("- (Details not available)")
331
+ lines.append("")
332
+
333
+ # Next up: Recommendations
334
+ if analysis.recommended_next:
335
+ lines.append("**Next Up**:")
336
+ for rec in analysis.recommended_next[:3]:
337
+ priority_label = rec.priority.upper()
338
+ lines.append(
339
+ f"- {rec.ticket_id}: {rec.title} " f"(Priority: {priority_label})"
340
+ )
341
+ lines.append("")
342
+
343
+ # Health status
344
+ if self.get_health_tracking_enabled():
345
+ health_emoji = {
346
+ "on_track": "✅",
347
+ "at_risk": "⚠️",
348
+ "off_track": "🚨",
349
+ }
350
+ emoji = health_emoji.get(analysis.health, "ℹ️")
351
+ lines.append(
352
+ f"**Health**: {emoji} {analysis.health.replace('_', ' ').title()}"
353
+ )
354
+ lines.append("")
355
+
356
+ # Blockers (if any)
357
+ if analysis.blockers:
358
+ lines.append("**Blockers**:")
359
+ for blocker in analysis.blockers[:3]:
360
+ lines.append(
361
+ f"- {blocker['ticket_id']}: {blocker['title']} "
362
+ f"(blocks {blocker['blocks_count']} ticket{'s' if blocker['blocks_count'] > 1 else ''})"
363
+ )
364
+ lines.append("")
365
+ else:
366
+ lines.append("**Blockers**: None")
367
+ lines.append("")
368
+
369
+ # Footer
370
+ timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
371
+ lines.extend(
372
+ [
373
+ "---",
374
+ f"*Auto-generated by mcp-ticketer on {timestamp}*",
375
+ ]
376
+ )
377
+
378
+ return "\n".join(lines)
@@ -14,6 +14,7 @@ def diagnose_adapter_configuration(console: Console) -> None:
14
14
  """Diagnose adapter configuration and provide recommendations.
15
15
 
16
16
  Args:
17
+ ----
17
18
  console: Rich console for output
18
19
 
19
20
  """
@@ -354,10 +355,11 @@ def get_adapter_status() -> dict[str, Any]:
354
355
  """Get current adapter status for programmatic use.
355
356
 
356
357
  Returns:
358
+ -------
357
359
  Dictionary with adapter status information
358
360
 
359
361
  """
360
- status = {
362
+ status: dict[str, Any] = {
361
363
  "adapter_type": None,
362
364
  "configuration_source": None,
363
365
  "credentials_valid": False,
@@ -76,6 +76,9 @@ def create_auggie_server_config(
76
76
  ) -> dict[str, Any]:
77
77
  """Create Auggie MCP server configuration for mcp-ticketer.
78
78
 
79
+ Uses the CLI command (mcp-ticketer mcp) which implements proper
80
+ Content-Length framing via FastMCP SDK, required for modern MCP clients.
81
+
79
82
  Args:
80
83
  python_path: Path to Python executable in mcp-ticketer venv
81
84
  project_config: Project configuration from .mcp-ticketer/config.json
@@ -85,7 +88,9 @@ def create_auggie_server_config(
85
88
  Auggie MCP server configuration dict
86
89
 
87
90
  """
88
- # Use Python module invocation pattern (works regardless of where package is installed)
91
+ # IMPORTANT: Use CLI command, NOT Python module invocation
92
+ # The CLI uses FastMCP SDK which implements proper Content-Length framing
93
+ # Legacy python -m mcp_ticketer.mcp.server uses line-delimited JSON (incompatible)
89
94
  from pathlib import Path
90
95
 
91
96
  # Get adapter configuration
@@ -137,14 +142,21 @@ def create_auggie_server_config(
137
142
  if "project_key" in adapter_config:
138
143
  env_vars["JIRA_PROJECT_KEY"] = adapter_config["project_key"]
139
144
 
140
- # Use Python module invocation pattern
141
- args = ["-m", "mcp_ticketer.mcp.server"]
145
+ # Get mcp-ticketer CLI path from Python path
146
+ # If python_path is /path/to/venv/bin/python, CLI is /path/to/venv/bin/mcp-ticketer
147
+ python_dir = Path(python_path).parent
148
+ cli_path = str(python_dir / "mcp-ticketer")
149
+
150
+ # Build CLI arguments
151
+ args = ["mcp"]
142
152
  if project_path:
143
- args.append(project_path)
153
+ args.extend(["--path", project_path])
144
154
 
145
155
  # Create server configuration (simpler than Gemini - no timeout/trust)
156
+ # NOTE: Environment variables below are optional fallbacks
157
+ # The CLI loads config from .mcp-ticketer/config.json
146
158
  config = {
147
- "command": python_path,
159
+ "command": cli_path,
148
160
  "args": args,
149
161
  "env": env_vars,
150
162
  }