mcp-ticketer 0.3.5__py3-none-any.whl → 0.12.0__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 (84) hide show
  1. mcp_ticketer/__version__.py +3 -3
  2. mcp_ticketer/adapters/__init__.py +2 -0
  3. mcp_ticketer/adapters/aitrackdown.py +263 -14
  4. mcp_ticketer/adapters/asana/__init__.py +15 -0
  5. mcp_ticketer/adapters/asana/adapter.py +1308 -0
  6. mcp_ticketer/adapters/asana/client.py +292 -0
  7. mcp_ticketer/adapters/asana/mappers.py +334 -0
  8. mcp_ticketer/adapters/asana/types.py +146 -0
  9. mcp_ticketer/adapters/github.py +326 -109
  10. mcp_ticketer/adapters/hybrid.py +11 -11
  11. mcp_ticketer/adapters/jira.py +271 -25
  12. mcp_ticketer/adapters/linear/adapter.py +693 -39
  13. mcp_ticketer/adapters/linear/client.py +61 -9
  14. mcp_ticketer/adapters/linear/mappers.py +9 -3
  15. mcp_ticketer/adapters/linear/queries.py +9 -7
  16. mcp_ticketer/cache/memory.py +9 -8
  17. mcp_ticketer/cli/adapter_diagnostics.py +1 -1
  18. mcp_ticketer/cli/auggie_configure.py +104 -15
  19. mcp_ticketer/cli/codex_configure.py +188 -32
  20. mcp_ticketer/cli/configure.py +37 -48
  21. mcp_ticketer/cli/diagnostics.py +20 -18
  22. mcp_ticketer/cli/discover.py +292 -26
  23. mcp_ticketer/cli/gemini_configure.py +107 -26
  24. mcp_ticketer/cli/instruction_commands.py +429 -0
  25. mcp_ticketer/cli/linear_commands.py +105 -22
  26. mcp_ticketer/cli/main.py +1830 -435
  27. mcp_ticketer/cli/mcp_configure.py +296 -89
  28. mcp_ticketer/cli/migrate_config.py +12 -8
  29. mcp_ticketer/cli/platform_commands.py +123 -0
  30. mcp_ticketer/cli/platform_detection.py +412 -0
  31. mcp_ticketer/cli/python_detection.py +126 -0
  32. mcp_ticketer/cli/queue_commands.py +15 -15
  33. mcp_ticketer/cli/simple_health.py +1 -1
  34. mcp_ticketer/cli/ticket_commands.py +773 -0
  35. mcp_ticketer/cli/update_checker.py +313 -0
  36. mcp_ticketer/cli/utils.py +67 -62
  37. mcp_ticketer/core/__init__.py +14 -1
  38. mcp_ticketer/core/adapter.py +84 -15
  39. mcp_ticketer/core/config.py +44 -39
  40. mcp_ticketer/core/env_discovery.py +42 -12
  41. mcp_ticketer/core/env_loader.py +15 -14
  42. mcp_ticketer/core/exceptions.py +3 -3
  43. mcp_ticketer/core/http_client.py +26 -26
  44. mcp_ticketer/core/instructions.py +405 -0
  45. mcp_ticketer/core/mappers.py +11 -11
  46. mcp_ticketer/core/models.py +50 -20
  47. mcp_ticketer/core/onepassword_secrets.py +379 -0
  48. mcp_ticketer/core/project_config.py +57 -35
  49. mcp_ticketer/core/registry.py +3 -3
  50. mcp_ticketer/defaults/ticket_instructions.md +644 -0
  51. mcp_ticketer/mcp/__init__.py +29 -1
  52. mcp_ticketer/mcp/__main__.py +60 -0
  53. mcp_ticketer/mcp/server/__init__.py +25 -0
  54. mcp_ticketer/mcp/server/__main__.py +60 -0
  55. mcp_ticketer/mcp/{dto.py → server/dto.py} +32 -32
  56. mcp_ticketer/mcp/{server.py → server/main.py} +127 -74
  57. mcp_ticketer/mcp/{response_builder.py → server/response_builder.py} +2 -2
  58. mcp_ticketer/mcp/server/server_sdk.py +93 -0
  59. mcp_ticketer/mcp/server/tools/__init__.py +47 -0
  60. mcp_ticketer/mcp/server/tools/attachment_tools.py +226 -0
  61. mcp_ticketer/mcp/server/tools/bulk_tools.py +273 -0
  62. mcp_ticketer/mcp/server/tools/comment_tools.py +90 -0
  63. mcp_ticketer/mcp/server/tools/config_tools.py +381 -0
  64. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +532 -0
  65. mcp_ticketer/mcp/server/tools/instruction_tools.py +293 -0
  66. mcp_ticketer/mcp/server/tools/pr_tools.py +154 -0
  67. mcp_ticketer/mcp/server/tools/search_tools.py +206 -0
  68. mcp_ticketer/mcp/server/tools/ticket_tools.py +430 -0
  69. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +382 -0
  70. mcp_ticketer/queue/__init__.py +1 -0
  71. mcp_ticketer/queue/health_monitor.py +5 -4
  72. mcp_ticketer/queue/manager.py +15 -51
  73. mcp_ticketer/queue/queue.py +19 -19
  74. mcp_ticketer/queue/run_worker.py +1 -1
  75. mcp_ticketer/queue/ticket_registry.py +14 -14
  76. mcp_ticketer/queue/worker.py +16 -14
  77. {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/METADATA +168 -32
  78. mcp_ticketer-0.12.0.dist-info/RECORD +91 -0
  79. mcp_ticketer-0.3.5.dist-info/RECORD +0 -62
  80. /mcp_ticketer/mcp/{constants.py → server/constants.py} +0 -0
  81. {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/WHEEL +0 -0
  82. {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/entry_points.txt +0 -0
  83. {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/licenses/LICENSE +0 -0
  84. {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,430 @@
1
+ """Basic CRUD operations for tickets.
2
+
3
+ This module implements the core create, read, update, delete, and list
4
+ operations for tickets using the FastMCP SDK.
5
+ """
6
+
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from ....core.models import Priority, Task, TicketState
11
+ from ....core.project_config import ConfigResolver, TicketerConfig
12
+ from ..server_sdk import get_adapter, mcp
13
+
14
+
15
+ async def detect_and_apply_labels(
16
+ adapter: Any,
17
+ ticket_title: str,
18
+ ticket_description: str,
19
+ existing_labels: list[str] | None = None,
20
+ ) -> list[str]:
21
+ """Detect and suggest labels/tags based on ticket content.
22
+
23
+ This function analyzes the ticket title and description to automatically
24
+ detect relevant labels/tags from the adapter's available labels.
25
+
26
+ Args:
27
+ adapter: The ticket adapter instance
28
+ ticket_title: Ticket title text
29
+ ticket_description: Ticket description text
30
+ existing_labels: Labels already specified by user (optional)
31
+
32
+ Returns:
33
+ List of label/tag identifiers to apply (combines auto-detected + user-specified)
34
+
35
+ """
36
+ # Get available labels from adapter
37
+ available_labels = []
38
+ try:
39
+ if hasattr(adapter, "list_labels"):
40
+ available_labels = await adapter.list_labels()
41
+ elif hasattr(adapter, "get_labels"):
42
+ available_labels = await adapter.get_labels()
43
+ except Exception:
44
+ # Adapter doesn't support labels or listing failed - return user labels only
45
+ return existing_labels or []
46
+
47
+ if not available_labels:
48
+ return existing_labels or []
49
+
50
+ # Combine title and description for matching (lowercase for case-insensitive matching)
51
+ content = f"{ticket_title} {ticket_description or ''}".lower()
52
+
53
+ # Common label keyword patterns
54
+ label_keywords = {
55
+ "bug": ["bug", "error", "broken", "crash", "fix", "issue", "defect"],
56
+ "feature": ["feature", "add", "new", "implement", "create", "enhancement"],
57
+ "improvement": [
58
+ "enhance",
59
+ "improve",
60
+ "update",
61
+ "upgrade",
62
+ "refactor",
63
+ "optimize",
64
+ ],
65
+ "documentation": ["doc", "documentation", "readme", "guide", "manual"],
66
+ "test": ["test", "testing", "qa", "validation", "verify"],
67
+ "security": ["security", "vulnerability", "auth", "permission", "exploit"],
68
+ "performance": ["performance", "slow", "optimize", "speed", "latency"],
69
+ "ui": ["ui", "ux", "interface", "design", "layout", "frontend"],
70
+ "api": ["api", "endpoint", "rest", "graphql", "backend"],
71
+ "backend": ["backend", "server", "database", "storage"],
72
+ "frontend": ["frontend", "client", "web", "react", "vue"],
73
+ "critical": ["critical", "urgent", "emergency", "blocker"],
74
+ "high-priority": ["urgent", "asap", "important", "critical"],
75
+ }
76
+
77
+ # Match labels against content
78
+ matched_labels = []
79
+
80
+ for label in available_labels:
81
+ # Extract label name (handle both dict and string formats)
82
+ if isinstance(label, dict):
83
+ label_name = label.get("name", "")
84
+ label_id = label.get("id", label_name)
85
+ else:
86
+ label_name = str(label)
87
+ label_id = label_name
88
+
89
+ label_name_lower = label_name.lower()
90
+
91
+ # Direct match: label name appears in content
92
+ if label_name_lower in content:
93
+ if label_id not in matched_labels:
94
+ matched_labels.append(label_id)
95
+ continue
96
+
97
+ # Keyword match: check if label matches any keyword category
98
+ for keyword_category, keywords in label_keywords.items():
99
+ # Check if label name relates to the category
100
+ if (
101
+ keyword_category in label_name_lower
102
+ or label_name_lower in keyword_category
103
+ ):
104
+ # Check if any keyword from this category appears in content
105
+ if any(kw in content for kw in keywords):
106
+ if label_id not in matched_labels:
107
+ matched_labels.append(label_id)
108
+ break
109
+
110
+ # Combine user-specified labels with auto-detected ones
111
+ final_labels = list(existing_labels or [])
112
+ for label in matched_labels:
113
+ if label not in final_labels:
114
+ final_labels.append(label)
115
+
116
+ return final_labels
117
+
118
+
119
+ @mcp.tool()
120
+ async def ticket_create(
121
+ title: str,
122
+ description: str = "",
123
+ priority: str = "medium",
124
+ tags: list[str] | None = None,
125
+ assignee: str | None = None,
126
+ parent_epic: str | None = None,
127
+ auto_detect_labels: bool = True,
128
+ ) -> dict[str, Any]:
129
+ """Create a new ticket with automatic label/tag detection.
130
+
131
+ This tool automatically scans available labels/tags and intelligently
132
+ applies relevant ones based on the ticket title and description.
133
+
134
+ Label Detection:
135
+ - Scans all available labels in the configured adapter
136
+ - Matches labels based on keywords in title/description
137
+ - Combines auto-detected labels with user-specified ones
138
+ - Can be disabled by setting auto_detect_labels=false
139
+
140
+ Common label patterns detected:
141
+ - bug, feature, improvement, documentation
142
+ - test, security, performance
143
+ - ui, api, backend, frontend
144
+
145
+ Args:
146
+ title: Ticket title (required)
147
+ description: Detailed description of the ticket
148
+ priority: Priority level - must be one of: low, medium, high, critical
149
+ tags: List of tags to categorize the ticket (auto-detection adds to these)
150
+ assignee: User ID or email to assign the ticket to
151
+ parent_epic: Parent epic/project ID to assign this ticket to (optional)
152
+ auto_detect_labels: Automatically detect and apply relevant labels (default: True)
153
+
154
+ Returns:
155
+ Created ticket details including ID and metadata, or error information
156
+
157
+ """
158
+ try:
159
+ adapter = get_adapter()
160
+
161
+ # Validate and convert priority
162
+ try:
163
+ priority_enum = Priority(priority.lower())
164
+ except ValueError:
165
+ return {
166
+ "status": "error",
167
+ "error": f"Invalid priority '{priority}'. Must be one of: low, medium, high, critical",
168
+ }
169
+
170
+ # Use default_user if no assignee specified
171
+ final_assignee = assignee
172
+ if final_assignee is None:
173
+ resolver = ConfigResolver(project_path=Path.cwd())
174
+ config = resolver.load_project_config() or TicketerConfig()
175
+ if config.default_user:
176
+ final_assignee = config.default_user
177
+
178
+ # Use default_project if no parent_epic specified
179
+ final_parent_epic = parent_epic
180
+ if final_parent_epic is None:
181
+ resolver = ConfigResolver(project_path=Path.cwd())
182
+ config = resolver.load_project_config() or TicketerConfig()
183
+ # Try default_project first, fall back to default_epic
184
+ if config.default_project:
185
+ final_parent_epic = config.default_project
186
+ elif config.default_epic:
187
+ final_parent_epic = config.default_epic
188
+
189
+ # Auto-detect labels if enabled
190
+ final_tags = tags
191
+ if auto_detect_labels:
192
+ final_tags = await detect_and_apply_labels(
193
+ adapter, title, description or "", tags
194
+ )
195
+
196
+ # Create task object
197
+ task = Task(
198
+ title=title,
199
+ description=description or "",
200
+ priority=priority_enum,
201
+ tags=final_tags or [],
202
+ assignee=final_assignee,
203
+ parent_epic=final_parent_epic,
204
+ )
205
+
206
+ # Create via adapter
207
+ created = await adapter.create(task)
208
+
209
+ return {
210
+ "status": "completed",
211
+ "ticket": created.model_dump(),
212
+ "labels_applied": created.tags or [],
213
+ "auto_detected": auto_detect_labels,
214
+ }
215
+ except Exception as e:
216
+ return {
217
+ "status": "error",
218
+ "error": f"Failed to create ticket: {str(e)}",
219
+ }
220
+
221
+
222
+ @mcp.tool()
223
+ async def ticket_read(ticket_id: str) -> dict[str, Any]:
224
+ """Read a ticket by its ID.
225
+
226
+ Args:
227
+ ticket_id: Unique identifier of the ticket to retrieve
228
+
229
+ Returns:
230
+ Ticket details if found, or error information
231
+
232
+ """
233
+ try:
234
+ adapter = get_adapter()
235
+ ticket = await adapter.read(ticket_id)
236
+
237
+ if ticket is None:
238
+ return {
239
+ "status": "error",
240
+ "error": f"Ticket {ticket_id} not found",
241
+ }
242
+
243
+ return {
244
+ "status": "completed",
245
+ "ticket": ticket.model_dump(),
246
+ }
247
+ except Exception as e:
248
+ return {
249
+ "status": "error",
250
+ "error": f"Failed to read ticket: {str(e)}",
251
+ }
252
+
253
+
254
+ @mcp.tool()
255
+ async def ticket_update(
256
+ ticket_id: str,
257
+ title: str | None = None,
258
+ description: str | None = None,
259
+ priority: str | None = None,
260
+ state: str | None = None,
261
+ assignee: str | None = None,
262
+ tags: list[str] | None = None,
263
+ ) -> dict[str, Any]:
264
+ """Update an existing ticket.
265
+
266
+ Args:
267
+ ticket_id: Unique identifier of the ticket to update
268
+ title: New title for the ticket
269
+ description: New description for the ticket
270
+ priority: New priority - must be one of: low, medium, high, critical
271
+ state: New state - must be one of: open, in_progress, ready, tested, done, closed, waiting, blocked
272
+ assignee: User ID or email to assign the ticket to
273
+ tags: New list of tags (replaces existing tags)
274
+
275
+ Returns:
276
+ Updated ticket details, or error information
277
+
278
+ """
279
+ try:
280
+ adapter = get_adapter()
281
+
282
+ # Build updates dictionary with only provided fields
283
+ updates: dict[str, Any] = {}
284
+
285
+ if title is not None:
286
+ updates["title"] = title
287
+ if description is not None:
288
+ updates["description"] = description
289
+ if assignee is not None:
290
+ updates["assignee"] = assignee
291
+ if tags is not None:
292
+ updates["tags"] = tags
293
+
294
+ # Validate and convert priority if provided
295
+ if priority is not None:
296
+ try:
297
+ updates["priority"] = Priority(priority.lower())
298
+ except ValueError:
299
+ return {
300
+ "status": "error",
301
+ "error": f"Invalid priority '{priority}'. Must be one of: low, medium, high, critical",
302
+ }
303
+
304
+ # Validate and convert state if provided
305
+ if state is not None:
306
+ try:
307
+ updates["state"] = TicketState(state.lower())
308
+ except ValueError:
309
+ return {
310
+ "status": "error",
311
+ "error": f"Invalid state '{state}'. Must be one of: open, in_progress, ready, tested, done, closed, waiting, blocked",
312
+ }
313
+
314
+ # Update via adapter
315
+ updated = await adapter.update(ticket_id, updates)
316
+
317
+ if updated is None:
318
+ return {
319
+ "status": "error",
320
+ "error": f"Ticket {ticket_id} not found or update failed",
321
+ }
322
+
323
+ return {
324
+ "status": "completed",
325
+ "ticket": updated.model_dump(),
326
+ }
327
+ except Exception as e:
328
+ return {
329
+ "status": "error",
330
+ "error": f"Failed to update ticket: {str(e)}",
331
+ }
332
+
333
+
334
+ @mcp.tool()
335
+ async def ticket_delete(ticket_id: str) -> dict[str, Any]:
336
+ """Delete a ticket by its ID.
337
+
338
+ Args:
339
+ ticket_id: Unique identifier of the ticket to delete
340
+
341
+ Returns:
342
+ Success confirmation or error information
343
+
344
+ """
345
+ try:
346
+ adapter = get_adapter()
347
+ success = await adapter.delete(ticket_id)
348
+
349
+ if not success:
350
+ return {
351
+ "status": "error",
352
+ "error": f"Ticket {ticket_id} not found or delete failed",
353
+ }
354
+
355
+ return {
356
+ "status": "completed",
357
+ "message": f"Ticket {ticket_id} deleted successfully",
358
+ }
359
+ except Exception as e:
360
+ return {
361
+ "status": "error",
362
+ "error": f"Failed to delete ticket: {str(e)}",
363
+ }
364
+
365
+
366
+ @mcp.tool()
367
+ async def ticket_list(
368
+ limit: int = 10,
369
+ offset: int = 0,
370
+ state: str | None = None,
371
+ priority: str | None = None,
372
+ assignee: str | None = None,
373
+ ) -> dict[str, Any]:
374
+ """List tickets with pagination and optional filters.
375
+
376
+ Args:
377
+ limit: Maximum number of tickets to return (default: 10)
378
+ offset: Number of tickets to skip for pagination (default: 0)
379
+ state: Filter by state - must be one of: open, in_progress, ready, tested, done, closed, waiting, blocked
380
+ priority: Filter by priority - must be one of: low, medium, high, critical
381
+ assignee: Filter by assigned user ID or email
382
+
383
+ Returns:
384
+ List of tickets matching criteria, or error information
385
+
386
+ """
387
+ try:
388
+ adapter = get_adapter()
389
+
390
+ # Build filters dictionary
391
+ filters: dict[str, Any] = {}
392
+
393
+ if state is not None:
394
+ try:
395
+ filters["state"] = TicketState(state.lower())
396
+ except ValueError:
397
+ return {
398
+ "status": "error",
399
+ "error": f"Invalid state '{state}'. Must be one of: open, in_progress, ready, tested, done, closed, waiting, blocked",
400
+ }
401
+
402
+ if priority is not None:
403
+ try:
404
+ filters["priority"] = Priority(priority.lower())
405
+ except ValueError:
406
+ return {
407
+ "status": "error",
408
+ "error": f"Invalid priority '{priority}'. Must be one of: low, medium, high, critical",
409
+ }
410
+
411
+ if assignee is not None:
412
+ filters["assignee"] = assignee
413
+
414
+ # List tickets via adapter
415
+ tickets = await adapter.list(
416
+ limit=limit, offset=offset, filters=filters if filters else None
417
+ )
418
+
419
+ return {
420
+ "status": "completed",
421
+ "tickets": [ticket.model_dump() for ticket in tickets],
422
+ "count": len(tickets),
423
+ "limit": limit,
424
+ "offset": offset,
425
+ }
426
+ except Exception as e:
427
+ return {
428
+ "status": "error",
429
+ "error": f"Failed to list tickets: {str(e)}",
430
+ }