mcp-ticketer 0.4.11__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 (70) hide show
  1. mcp_ticketer/__version__.py +3 -3
  2. mcp_ticketer/adapters/__init__.py +2 -0
  3. mcp_ticketer/adapters/aitrackdown.py +9 -3
  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 +313 -96
  10. mcp_ticketer/adapters/jira.py +251 -1
  11. mcp_ticketer/adapters/linear/adapter.py +524 -22
  12. mcp_ticketer/adapters/linear/client.py +61 -9
  13. mcp_ticketer/adapters/linear/mappers.py +9 -3
  14. mcp_ticketer/cache/memory.py +3 -3
  15. mcp_ticketer/cli/adapter_diagnostics.py +1 -1
  16. mcp_ticketer/cli/auggie_configure.py +1 -1
  17. mcp_ticketer/cli/codex_configure.py +80 -1
  18. mcp_ticketer/cli/configure.py +33 -43
  19. mcp_ticketer/cli/diagnostics.py +18 -16
  20. mcp_ticketer/cli/discover.py +288 -21
  21. mcp_ticketer/cli/gemini_configure.py +1 -1
  22. mcp_ticketer/cli/instruction_commands.py +429 -0
  23. mcp_ticketer/cli/linear_commands.py +99 -15
  24. mcp_ticketer/cli/main.py +1199 -227
  25. mcp_ticketer/cli/mcp_configure.py +1 -1
  26. mcp_ticketer/cli/migrate_config.py +12 -8
  27. mcp_ticketer/cli/platform_commands.py +6 -6
  28. mcp_ticketer/cli/platform_detection.py +412 -0
  29. mcp_ticketer/cli/queue_commands.py +15 -15
  30. mcp_ticketer/cli/simple_health.py +1 -1
  31. mcp_ticketer/cli/ticket_commands.py +14 -13
  32. mcp_ticketer/cli/update_checker.py +313 -0
  33. mcp_ticketer/cli/utils.py +45 -41
  34. mcp_ticketer/core/__init__.py +12 -0
  35. mcp_ticketer/core/adapter.py +4 -4
  36. mcp_ticketer/core/config.py +17 -10
  37. mcp_ticketer/core/env_discovery.py +33 -3
  38. mcp_ticketer/core/env_loader.py +7 -6
  39. mcp_ticketer/core/exceptions.py +3 -3
  40. mcp_ticketer/core/http_client.py +10 -10
  41. mcp_ticketer/core/instructions.py +405 -0
  42. mcp_ticketer/core/mappers.py +1 -1
  43. mcp_ticketer/core/models.py +1 -1
  44. mcp_ticketer/core/onepassword_secrets.py +379 -0
  45. mcp_ticketer/core/project_config.py +17 -1
  46. mcp_ticketer/core/registry.py +1 -1
  47. mcp_ticketer/defaults/ticket_instructions.md +644 -0
  48. mcp_ticketer/mcp/__init__.py +2 -2
  49. mcp_ticketer/mcp/server/__init__.py +2 -2
  50. mcp_ticketer/mcp/server/main.py +82 -69
  51. mcp_ticketer/mcp/server/tools/__init__.py +9 -0
  52. mcp_ticketer/mcp/server/tools/attachment_tools.py +63 -16
  53. mcp_ticketer/mcp/server/tools/config_tools.py +381 -0
  54. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +154 -5
  55. mcp_ticketer/mcp/server/tools/instruction_tools.py +293 -0
  56. mcp_ticketer/mcp/server/tools/ticket_tools.py +157 -4
  57. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +382 -0
  58. mcp_ticketer/queue/health_monitor.py +1 -0
  59. mcp_ticketer/queue/manager.py +4 -4
  60. mcp_ticketer/queue/queue.py +3 -3
  61. mcp_ticketer/queue/run_worker.py +1 -1
  62. mcp_ticketer/queue/ticket_registry.py +2 -2
  63. mcp_ticketer/queue/worker.py +14 -12
  64. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-0.12.0.dist-info}/METADATA +106 -52
  65. mcp_ticketer-0.12.0.dist-info/RECORD +91 -0
  66. mcp_ticketer-0.4.11.dist-info/RECORD +0 -77
  67. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-0.12.0.dist-info}/WHEEL +0 -0
  68. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-0.12.0.dist-info}/entry_points.txt +0 -0
  69. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-0.12.0.dist-info}/licenses/LICENSE +0 -0
  70. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-0.12.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,293 @@
1
+ """Ticket instructions management tools.
2
+
3
+ This module implements MCP tools for managing ticket writing instructions,
4
+ allowing AI agents to query and customize the guidelines that help create
5
+ well-structured, consistent tickets.
6
+ """
7
+
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from ....core.instructions import (
12
+ InstructionsError,
13
+ InstructionsValidationError,
14
+ TicketInstructionsManager,
15
+ )
16
+ from ..server_sdk import mcp
17
+
18
+
19
+ @mcp.tool()
20
+ async def instructions_get() -> dict[str, Any]:
21
+ """Get current ticket writing instructions.
22
+
23
+ Retrieves the active instructions for the current project, which may be
24
+ custom project-specific instructions or the default embedded instructions.
25
+
26
+ Returns:
27
+ A dictionary containing:
28
+ - status: "completed" or "error"
29
+ - instructions: The full instruction text (if successful)
30
+ - source: "custom" or "default" indicating which instructions are active
31
+ - path: Path to custom instructions file (if exists)
32
+ - error: Error message (if failed)
33
+
34
+ Example response:
35
+ {
36
+ "status": "completed",
37
+ "instructions": "# Ticket Writing Guidelines...",
38
+ "source": "custom",
39
+ "path": "/path/to/project/.mcp-ticketer/instructions.md"
40
+ }
41
+
42
+ """
43
+ try:
44
+ # Use current working directory as project directory
45
+ manager = TicketInstructionsManager(project_dir=Path.cwd())
46
+
47
+ # Get instructions
48
+ instructions = manager.get_instructions()
49
+
50
+ # Determine source
51
+ source = "custom" if manager.has_custom_instructions() else "default"
52
+
53
+ # Build response
54
+ response: dict[str, Any] = {
55
+ "status": "completed",
56
+ "instructions": instructions,
57
+ "source": source,
58
+ }
59
+
60
+ # Add path if custom instructions exist
61
+ if source == "custom":
62
+ response["path"] = str(manager.get_instructions_path())
63
+
64
+ return response
65
+
66
+ except InstructionsError as e:
67
+ return {
68
+ "status": "error",
69
+ "error": f"Failed to get instructions: {str(e)}",
70
+ }
71
+ except Exception as e:
72
+ return {
73
+ "status": "error",
74
+ "error": f"Unexpected error: {str(e)}",
75
+ }
76
+
77
+
78
+ @mcp.tool()
79
+ async def instructions_set(content: str, source: str = "inline") -> dict[str, Any]:
80
+ r"""Set custom ticket writing instructions for the project.
81
+
82
+ Creates or overwrites custom instructions with the provided content.
83
+ The content is validated before saving.
84
+
85
+ Args:
86
+ content: The custom instructions content (markdown text)
87
+ source: Source type - "inline" for direct content or "file" for file path
88
+ (currently only "inline" is supported by MCP tools)
89
+
90
+ Returns:
91
+ A dictionary containing:
92
+ - status: "completed" or "error"
93
+ - message: Success or error message
94
+ - path: Path where instructions were saved (if successful)
95
+ - error: Detailed error message (if failed)
96
+
97
+ Example:
98
+ To set custom instructions:
99
+ instructions_set(
100
+ content="# Our Team's Ticket Guidelines\\n\\n...",
101
+ source="inline"
102
+ )
103
+
104
+ """
105
+ try:
106
+ # Validate source parameter
107
+ if source not in ["inline", "file"]:
108
+ return {
109
+ "status": "error",
110
+ "error": f"Invalid source '{source}'. Must be 'inline' or 'file'",
111
+ }
112
+
113
+ # Use current working directory as project directory
114
+ manager = TicketInstructionsManager(project_dir=Path.cwd())
115
+
116
+ # Set instructions
117
+ manager.set_instructions(content)
118
+
119
+ # Get path where instructions were saved
120
+ inst_path = manager.get_instructions_path()
121
+
122
+ return {
123
+ "status": "completed",
124
+ "message": "Custom instructions saved successfully",
125
+ "path": str(inst_path),
126
+ }
127
+
128
+ except InstructionsValidationError as e:
129
+ return {
130
+ "status": "error",
131
+ "error": f"Validation failed: {str(e)}",
132
+ "message": "Instructions content did not pass validation checks",
133
+ }
134
+ except InstructionsError as e:
135
+ return {
136
+ "status": "error",
137
+ "error": f"Failed to set instructions: {str(e)}",
138
+ }
139
+ except Exception as e:
140
+ return {
141
+ "status": "error",
142
+ "error": f"Unexpected error: {str(e)}",
143
+ }
144
+
145
+
146
+ @mcp.tool()
147
+ async def instructions_reset() -> dict[str, Any]:
148
+ """Reset to default instructions by deleting custom instructions.
149
+
150
+ Removes any custom project-specific instructions, causing the system
151
+ to revert to using the default embedded instructions.
152
+
153
+ Returns:
154
+ A dictionary containing:
155
+ - status: "completed" or "error"
156
+ - message: Description of what happened
157
+ - error: Error message (if failed)
158
+
159
+ Example response (when custom instructions existed):
160
+ {
161
+ "status": "completed",
162
+ "message": "Custom instructions deleted. Now using defaults."
163
+ }
164
+
165
+ Example response (when no custom instructions):
166
+ {
167
+ "status": "completed",
168
+ "message": "No custom instructions to delete. Already using defaults."
169
+ }
170
+
171
+ """
172
+ try:
173
+ # Use current working directory as project directory
174
+ manager = TicketInstructionsManager(project_dir=Path.cwd())
175
+
176
+ # Check if custom instructions exist
177
+ if not manager.has_custom_instructions():
178
+ return {
179
+ "status": "completed",
180
+ "message": "No custom instructions to delete. Already using defaults.",
181
+ }
182
+
183
+ # Delete custom instructions
184
+ deleted = manager.delete_instructions()
185
+
186
+ if deleted:
187
+ return {
188
+ "status": "completed",
189
+ "message": "Custom instructions deleted. Now using defaults.",
190
+ }
191
+ else:
192
+ return {
193
+ "status": "completed",
194
+ "message": "No custom instructions found to delete.",
195
+ }
196
+
197
+ except InstructionsError as e:
198
+ return {
199
+ "status": "error",
200
+ "error": f"Failed to reset instructions: {str(e)}",
201
+ }
202
+ except Exception as e:
203
+ return {
204
+ "status": "error",
205
+ "error": f"Unexpected error: {str(e)}",
206
+ }
207
+
208
+
209
+ @mcp.tool()
210
+ async def instructions_validate(content: str) -> dict[str, Any]:
211
+ """Validate ticket instructions content without saving.
212
+
213
+ Checks if the provided content meets validation requirements:
214
+ - Not empty
215
+ - Minimum length (100 characters)
216
+ - Contains markdown headers (warning only)
217
+
218
+ This allows AI agents to validate content before attempting to save it.
219
+
220
+ Args:
221
+ content: The instructions content to validate (markdown text)
222
+
223
+ Returns:
224
+ A dictionary containing:
225
+ - status: "valid" or "invalid"
226
+ - warnings: List of non-critical issues (e.g., missing headers)
227
+ - errors: List of critical validation failures
228
+ - message: Summary message
229
+
230
+ Example response (valid):
231
+ {
232
+ "status": "valid",
233
+ "warnings": ["No markdown headers found"],
234
+ "errors": [],
235
+ "message": "Content is valid but has 1 warning"
236
+ }
237
+
238
+ Example response (invalid):
239
+ {
240
+ "status": "invalid",
241
+ "warnings": [],
242
+ "errors": ["Content too short (50 characters). Minimum 100 required."],
243
+ "message": "Content validation failed"
244
+ }
245
+
246
+ """
247
+ warnings: list[str] = []
248
+ errors: list[str] = []
249
+
250
+ try:
251
+ # Check for empty content
252
+ if not content or not content.strip():
253
+ errors.append("Instructions content cannot be empty")
254
+ else:
255
+ # Check minimum length
256
+ if len(content.strip()) < 100:
257
+ errors.append(
258
+ f"Content too short ({len(content)} characters). "
259
+ "Minimum 100 characters required for meaningful guidelines."
260
+ )
261
+
262
+ # Check for markdown headers (warning only)
263
+ if not any(line.strip().startswith("#") for line in content.split("\n")):
264
+ warnings.append(
265
+ "No markdown headers found. "
266
+ "Consider using headers for better structure."
267
+ )
268
+
269
+ # Determine status
270
+ if errors:
271
+ status = "invalid"
272
+ message = "Content validation failed"
273
+ elif warnings:
274
+ status = "valid"
275
+ message = f"Content is valid but has {len(warnings)} warning(s)"
276
+ else:
277
+ status = "valid"
278
+ message = "Content is valid with no issues"
279
+
280
+ return {
281
+ "status": status,
282
+ "warnings": warnings,
283
+ "errors": errors,
284
+ "message": message,
285
+ }
286
+
287
+ except Exception as e:
288
+ return {
289
+ "status": "error",
290
+ "warnings": [],
291
+ "errors": [f"Validation error: {str(e)}"],
292
+ "message": "Validation process failed",
293
+ }
@@ -4,12 +4,118 @@ This module implements the core create, read, update, delete, and list
4
4
  operations for tickets using the FastMCP SDK.
5
5
  """
6
6
 
7
+ from pathlib import Path
7
8
  from typing import Any
8
9
 
9
10
  from ....core.models import Priority, Task, TicketState
11
+ from ....core.project_config import ConfigResolver, TicketerConfig
10
12
  from ..server_sdk import get_adapter, mcp
11
13
 
12
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
+
13
119
  @mcp.tool()
14
120
  async def ticket_create(
15
121
  title: str,
@@ -17,15 +123,33 @@ async def ticket_create(
17
123
  priority: str = "medium",
18
124
  tags: list[str] | None = None,
19
125
  assignee: str | None = None,
126
+ parent_epic: str | None = None,
127
+ auto_detect_labels: bool = True,
20
128
  ) -> dict[str, Any]:
21
- """Create a new ticket with specified details.
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
22
144
 
23
145
  Args:
24
146
  title: Ticket title (required)
25
147
  description: Detailed description of the ticket
26
148
  priority: Priority level - must be one of: low, medium, high, critical
27
- tags: List of tags to categorize the ticket
149
+ tags: List of tags to categorize the ticket (auto-detection adds to these)
28
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)
29
153
 
30
154
  Returns:
31
155
  Created ticket details including ID and metadata, or error information
@@ -43,13 +167,40 @@ async def ticket_create(
43
167
  "error": f"Invalid priority '{priority}'. Must be one of: low, medium, high, critical",
44
168
  }
45
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
+
46
196
  # Create task object
47
197
  task = Task(
48
198
  title=title,
49
199
  description=description or "",
50
200
  priority=priority_enum,
51
- tags=tags or [],
52
- assignee=assignee,
201
+ tags=final_tags or [],
202
+ assignee=final_assignee,
203
+ parent_epic=final_parent_epic,
53
204
  )
54
205
 
55
206
  # Create via adapter
@@ -58,6 +209,8 @@ async def ticket_create(
58
209
  return {
59
210
  "status": "completed",
60
211
  "ticket": created.model_dump(),
212
+ "labels_applied": created.tags or [],
213
+ "auto_detected": auto_detect_labels,
61
214
  }
62
215
  except Exception as e:
63
216
  return {