mcp-ticketer 2.0.1__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 (73) hide show
  1. mcp_ticketer/__version__.py +1 -1
  2. mcp_ticketer/_version_scm.py +1 -0
  3. mcp_ticketer/adapters/aitrackdown.py +122 -0
  4. mcp_ticketer/adapters/asana/adapter.py +121 -0
  5. mcp_ticketer/adapters/github/__init__.py +26 -0
  6. mcp_ticketer/adapters/{github.py → github/adapter.py} +1506 -365
  7. mcp_ticketer/adapters/github/client.py +335 -0
  8. mcp_ticketer/adapters/github/mappers.py +797 -0
  9. mcp_ticketer/adapters/github/queries.py +692 -0
  10. mcp_ticketer/adapters/github/types.py +460 -0
  11. mcp_ticketer/adapters/jira/__init__.py +35 -0
  12. mcp_ticketer/adapters/{jira.py → jira/adapter.py} +250 -678
  13. mcp_ticketer/adapters/jira/client.py +271 -0
  14. mcp_ticketer/adapters/jira/mappers.py +246 -0
  15. mcp_ticketer/adapters/jira/queries.py +216 -0
  16. mcp_ticketer/adapters/jira/types.py +304 -0
  17. mcp_ticketer/adapters/linear/adapter.py +1000 -92
  18. mcp_ticketer/adapters/linear/client.py +91 -1
  19. mcp_ticketer/adapters/linear/mappers.py +107 -0
  20. mcp_ticketer/adapters/linear/queries.py +112 -2
  21. mcp_ticketer/adapters/linear/types.py +50 -10
  22. mcp_ticketer/cli/configure.py +524 -89
  23. mcp_ticketer/cli/install_mcp_server.py +418 -0
  24. mcp_ticketer/cli/main.py +10 -0
  25. mcp_ticketer/cli/mcp_configure.py +177 -49
  26. mcp_ticketer/cli/platform_installer.py +9 -0
  27. mcp_ticketer/cli/setup_command.py +157 -1
  28. mcp_ticketer/cli/ticket_commands.py +443 -81
  29. mcp_ticketer/cli/utils.py +113 -0
  30. mcp_ticketer/core/__init__.py +28 -0
  31. mcp_ticketer/core/adapter.py +367 -1
  32. mcp_ticketer/core/milestone_manager.py +252 -0
  33. mcp_ticketer/core/models.py +345 -0
  34. mcp_ticketer/core/project_utils.py +281 -0
  35. mcp_ticketer/core/project_validator.py +376 -0
  36. mcp_ticketer/core/session_state.py +6 -1
  37. mcp_ticketer/core/state_matcher.py +36 -3
  38. mcp_ticketer/mcp/server/__main__.py +2 -1
  39. mcp_ticketer/mcp/server/routing.py +68 -0
  40. mcp_ticketer/mcp/server/tools/__init__.py +7 -4
  41. mcp_ticketer/mcp/server/tools/attachment_tools.py +3 -1
  42. mcp_ticketer/mcp/server/tools/config_tools.py +233 -35
  43. mcp_ticketer/mcp/server/tools/milestone_tools.py +338 -0
  44. mcp_ticketer/mcp/server/tools/search_tools.py +30 -1
  45. mcp_ticketer/mcp/server/tools/ticket_tools.py +37 -1
  46. mcp_ticketer/queue/queue.py +68 -0
  47. {mcp_ticketer-2.0.1.dist-info → mcp_ticketer-2.2.13.dist-info}/METADATA +33 -3
  48. {mcp_ticketer-2.0.1.dist-info → mcp_ticketer-2.2.13.dist-info}/RECORD +72 -36
  49. mcp_ticketer-2.2.13.dist-info/top_level.txt +2 -0
  50. py_mcp_installer/examples/phase3_demo.py +178 -0
  51. py_mcp_installer/scripts/manage_version.py +54 -0
  52. py_mcp_installer/setup.py +6 -0
  53. py_mcp_installer/src/py_mcp_installer/__init__.py +153 -0
  54. py_mcp_installer/src/py_mcp_installer/command_builder.py +445 -0
  55. py_mcp_installer/src/py_mcp_installer/config_manager.py +541 -0
  56. py_mcp_installer/src/py_mcp_installer/exceptions.py +243 -0
  57. py_mcp_installer/src/py_mcp_installer/installation_strategy.py +617 -0
  58. py_mcp_installer/src/py_mcp_installer/installer.py +656 -0
  59. py_mcp_installer/src/py_mcp_installer/mcp_inspector.py +750 -0
  60. py_mcp_installer/src/py_mcp_installer/platform_detector.py +451 -0
  61. py_mcp_installer/src/py_mcp_installer/platforms/__init__.py +26 -0
  62. py_mcp_installer/src/py_mcp_installer/platforms/claude_code.py +225 -0
  63. py_mcp_installer/src/py_mcp_installer/platforms/codex.py +181 -0
  64. py_mcp_installer/src/py_mcp_installer/platforms/cursor.py +191 -0
  65. py_mcp_installer/src/py_mcp_installer/types.py +222 -0
  66. py_mcp_installer/src/py_mcp_installer/utils.py +463 -0
  67. py_mcp_installer/tests/__init__.py +0 -0
  68. py_mcp_installer/tests/platforms/__init__.py +0 -0
  69. py_mcp_installer/tests/test_platform_detector.py +17 -0
  70. mcp_ticketer-2.0.1.dist-info/top_level.txt +0 -1
  71. {mcp_ticketer-2.0.1.dist-info → mcp_ticketer-2.2.13.dist-info}/WHEEL +0 -0
  72. {mcp_ticketer-2.0.1.dist-info → mcp_ticketer-2.2.13.dist-info}/entry_points.txt +0 -0
  73. {mcp_ticketer-2.0.1.dist-info → mcp_ticketer-2.2.13.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,216 @@
1
+ """JQL query builders and query utilities for Jira adapter."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from ...core.models import SearchQuery
8
+ from .types import map_priority_to_jira
9
+
10
+
11
+ def build_list_jql(
12
+ project_key: str,
13
+ filters: dict[str, Any] | None = None,
14
+ state_mapper: callable | None = None,
15
+ ) -> str:
16
+ """Build JQL query for listing issues.
17
+
18
+ Args:
19
+ ----
20
+ project_key: JIRA project key
21
+ filters: Optional filters dictionary with keys:
22
+ - state: TicketState value
23
+ - priority: Priority value
24
+ - assignee: User identifier
25
+ - ticket_type: Issue type name
26
+ state_mapper: Function to map TicketState to JIRA status string
27
+
28
+ Returns:
29
+ -------
30
+ JQL query string
31
+
32
+ """
33
+ jql_parts = []
34
+
35
+ if project_key:
36
+ jql_parts.append(f"project = {project_key}")
37
+
38
+ if filters:
39
+ if "state" in filters and state_mapper:
40
+ status = state_mapper(filters["state"])
41
+ jql_parts.append(f'status = "{status}"')
42
+ if "priority" in filters:
43
+ priority = map_priority_to_jira(filters["priority"])
44
+ jql_parts.append(f'priority = "{priority}"')
45
+ if "assignee" in filters:
46
+ jql_parts.append(f'assignee = "{filters["assignee"]}"')
47
+ if "ticket_type" in filters:
48
+ jql_parts.append(f'issuetype = "{filters["ticket_type"]}"')
49
+
50
+ return " AND ".join(jql_parts) if jql_parts else "ORDER BY created DESC"
51
+
52
+
53
+ def build_search_jql(
54
+ project_key: str,
55
+ query: SearchQuery,
56
+ state_mapper: callable | None = None,
57
+ ) -> str:
58
+ """Build JQL query for searching issues.
59
+
60
+ Args:
61
+ ----
62
+ project_key: JIRA project key
63
+ query: SearchQuery object with search parameters
64
+ state_mapper: Function to map TicketState to JIRA status string
65
+
66
+ Returns:
67
+ -------
68
+ JQL query string
69
+
70
+ """
71
+ jql_parts = []
72
+
73
+ if project_key:
74
+ jql_parts.append(f"project = {project_key}")
75
+
76
+ # Text search
77
+ if query.query:
78
+ jql_parts.append(f'text ~ "{query.query}"')
79
+
80
+ # State filter
81
+ if query.state and state_mapper:
82
+ status = state_mapper(query.state)
83
+ jql_parts.append(f'status = "{status}"')
84
+
85
+ # Priority filter
86
+ if query.priority:
87
+ priority = map_priority_to_jira(query.priority)
88
+ jql_parts.append(f'priority = "{priority}"')
89
+
90
+ # Assignee filter
91
+ if query.assignee:
92
+ jql_parts.append(f'assignee = "{query.assignee}"')
93
+
94
+ # Tags/labels filter
95
+ if query.tags:
96
+ label_conditions = [f'labels = "{tag}"' for tag in query.tags]
97
+ jql_parts.append(f"({' OR '.join(label_conditions)})")
98
+
99
+ return " AND ".join(jql_parts) if jql_parts else "ORDER BY created DESC"
100
+
101
+
102
+ def build_epic_list_jql(
103
+ project_key: str,
104
+ state: str | None = None,
105
+ ) -> str:
106
+ """Build JQL query for listing epics.
107
+
108
+ Args:
109
+ ----
110
+ project_key: JIRA project key
111
+ state: Optional status name to filter by
112
+
113
+ Returns:
114
+ -------
115
+ JQL query string
116
+
117
+ """
118
+ jql_parts = [f"project = {project_key}", 'issuetype = "Epic"']
119
+
120
+ # Add state filter if provided
121
+ if state:
122
+ jql_parts.append(f'status = "{state}"')
123
+
124
+ return " AND ".join(jql_parts) + " ORDER BY updated DESC"
125
+
126
+
127
+ def build_labels_list_jql(
128
+ project_key: str,
129
+ max_results: int = 100,
130
+ ) -> str:
131
+ """Build JQL query for listing labels from recent issues.
132
+
133
+ Args:
134
+ ----
135
+ project_key: JIRA project key
136
+ max_results: Maximum number of issues to sample
137
+
138
+ Returns:
139
+ -------
140
+ JQL query string
141
+
142
+ """
143
+ return f"project = {project_key} ORDER BY updated DESC"
144
+
145
+
146
+ def build_project_labels_jql(
147
+ project_key: str,
148
+ max_results: int = 500,
149
+ ) -> str:
150
+ """Build JQL query for listing all project labels.
151
+
152
+ Args:
153
+ ----
154
+ project_key: JIRA project key
155
+ max_results: Maximum number of issues to sample
156
+
157
+ Returns:
158
+ -------
159
+ JQL query string
160
+
161
+ """
162
+ return f"project = {project_key} ORDER BY updated DESC"
163
+
164
+
165
+ def get_search_params(
166
+ jql: str,
167
+ start_at: int = 0,
168
+ max_results: int = 50,
169
+ fields: str = "*all",
170
+ expand: str = "renderedFields",
171
+ ) -> dict[str, Any]:
172
+ """Get standard search query parameters.
173
+
174
+ Args:
175
+ ----
176
+ jql: JQL query string
177
+ start_at: Pagination offset
178
+ max_results: Maximum number of results
179
+ fields: Fields to include in response
180
+ expand: Additional data to expand
181
+
182
+ Returns:
183
+ -------
184
+ Dictionary of query parameters
185
+
186
+ """
187
+ return {
188
+ "jql": jql,
189
+ "startAt": start_at,
190
+ "maxResults": max_results,
191
+ "fields": fields,
192
+ "expand": expand,
193
+ }
194
+
195
+
196
+ def get_labels_search_params(
197
+ jql: str,
198
+ max_results: int = 100,
199
+ ) -> dict[str, Any]:
200
+ """Get search parameters for label listing.
201
+
202
+ Args:
203
+ ----
204
+ jql: JQL query string
205
+ max_results: Maximum number of results
206
+
207
+ Returns:
208
+ -------
209
+ Dictionary of query parameters
210
+
211
+ """
212
+ return {
213
+ "jql": jql,
214
+ "maxResults": max_results,
215
+ "fields": "labels",
216
+ }
@@ -0,0 +1,304 @@
1
+ """Type definitions and mappings for Jira adapter."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import re
7
+ from datetime import datetime
8
+ from enum import Enum
9
+ from typing import Any
10
+
11
+ from ...core.models import Priority, TicketState
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class JiraIssueType(str, Enum):
17
+ """Common JIRA issue types."""
18
+
19
+ EPIC = "Epic"
20
+ STORY = "Story"
21
+ TASK = "Task"
22
+ BUG = "Bug"
23
+ SUBTASK = "Sub-task"
24
+ IMPROVEMENT = "Improvement"
25
+ NEW_FEATURE = "New Feature"
26
+
27
+
28
+ class JiraPriority(str, Enum):
29
+ """Standard JIRA priority levels."""
30
+
31
+ HIGHEST = "Highest"
32
+ HIGH = "High"
33
+ MEDIUM = "Medium"
34
+ LOW = "Low"
35
+ LOWEST = "Lowest"
36
+
37
+
38
+ def get_state_mapping() -> dict[TicketState, str]:
39
+ """Map universal states to common JIRA workflow states.
40
+
41
+ Returns:
42
+ -------
43
+ Dictionary mapping TicketState enum values to JIRA status names
44
+
45
+ """
46
+ return {
47
+ TicketState.OPEN: "To Do",
48
+ TicketState.IN_PROGRESS: "In Progress",
49
+ TicketState.READY: "In Review",
50
+ TicketState.TESTED: "Testing",
51
+ TicketState.DONE: "Done",
52
+ TicketState.WAITING: "Waiting",
53
+ TicketState.BLOCKED: "Blocked",
54
+ TicketState.CLOSED: "Closed",
55
+ }
56
+
57
+
58
+ def map_priority_to_jira(priority: Priority) -> str:
59
+ """Map universal priority to JIRA priority.
60
+
61
+ Args:
62
+ ----
63
+ priority: Universal Priority enum value
64
+
65
+ Returns:
66
+ -------
67
+ JIRA priority string
68
+
69
+ """
70
+ mapping = {
71
+ Priority.CRITICAL: JiraPriority.HIGHEST,
72
+ Priority.HIGH: JiraPriority.HIGH,
73
+ Priority.MEDIUM: JiraPriority.MEDIUM,
74
+ Priority.LOW: JiraPriority.LOW,
75
+ }
76
+ return mapping.get(priority, JiraPriority.MEDIUM)
77
+
78
+
79
+ def map_priority_from_jira(jira_priority: dict[str, Any] | None) -> Priority:
80
+ """Map JIRA priority to universal priority.
81
+
82
+ Args:
83
+ ----
84
+ jira_priority: JIRA priority dictionary with 'name' field
85
+
86
+ Returns:
87
+ -------
88
+ Universal Priority enum value
89
+
90
+ """
91
+ if not jira_priority:
92
+ return Priority.MEDIUM
93
+
94
+ name = jira_priority.get("name", "").lower()
95
+
96
+ if "highest" in name or "urgent" in name or "critical" in name:
97
+ return Priority.CRITICAL
98
+ elif "high" in name:
99
+ return Priority.HIGH
100
+ elif "low" in name:
101
+ return Priority.LOW
102
+ else:
103
+ return Priority.MEDIUM
104
+
105
+
106
+ def map_state_from_jira(status: dict[str, Any]) -> TicketState:
107
+ """Map JIRA status to universal state.
108
+
109
+ Args:
110
+ ----
111
+ status: JIRA status dictionary with 'name' and 'statusCategory' fields
112
+
113
+ Returns:
114
+ -------
115
+ Universal TicketState enum value
116
+
117
+ """
118
+ if not status:
119
+ return TicketState.OPEN
120
+
121
+ name = status.get("name", "").lower()
122
+ category = status.get("statusCategory", {}).get("key", "").lower()
123
+
124
+ # Try to match by category first (more reliable)
125
+ if category == "new":
126
+ return TicketState.OPEN
127
+ elif category == "indeterminate":
128
+ return TicketState.IN_PROGRESS
129
+ elif category == "done":
130
+ return TicketState.DONE
131
+
132
+ # Fall back to name matching
133
+ if "block" in name:
134
+ return TicketState.BLOCKED
135
+ elif "wait" in name:
136
+ return TicketState.WAITING
137
+ elif "progress" in name or "doing" in name:
138
+ return TicketState.IN_PROGRESS
139
+ elif "review" in name:
140
+ return TicketState.READY
141
+ elif "test" in name:
142
+ return TicketState.TESTED
143
+ elif "done" in name or "resolved" in name:
144
+ return TicketState.DONE
145
+ elif "closed" in name:
146
+ return TicketState.CLOSED
147
+ else:
148
+ return TicketState.OPEN
149
+
150
+
151
+ def parse_jira_datetime(date_str: str) -> datetime | None:
152
+ """Parse JIRA datetime strings which can be in various formats.
153
+
154
+ JIRA can return dates in formats like:
155
+ - 2025-10-24T14:12:18.771-0400
156
+ - 2025-10-24T14:12:18.771Z
157
+ - 2025-10-24T14:12:18.771+00:00
158
+
159
+ Args:
160
+ ----
161
+ date_str: JIRA datetime string
162
+
163
+ Returns:
164
+ -------
165
+ datetime object or None if parsing fails
166
+
167
+ """
168
+ if not date_str:
169
+ return None
170
+
171
+ try:
172
+ # Handle Z timezone
173
+ if date_str.endswith("Z"):
174
+ return datetime.fromisoformat(date_str.replace("Z", "+00:00"))
175
+
176
+ # Handle timezone formats like -0400, +0500 (need to add colon)
177
+ if re.match(r".*[+-]\d{4}$", date_str):
178
+ # Insert colon in timezone: -0400 -> -04:00
179
+ date_str = re.sub(r"([+-]\d{2})(\d{2})$", r"\1:\2", date_str)
180
+
181
+ return datetime.fromisoformat(date_str)
182
+
183
+ except (ValueError, TypeError) as e:
184
+ logger.warning(f"Failed to parse JIRA datetime '{date_str}': {e}")
185
+ return None
186
+
187
+
188
+ def extract_text_from_adf(adf_content: str | dict[str, Any]) -> str:
189
+ """Extract plain text from Atlassian Document Format (ADF).
190
+
191
+ Args:
192
+ ----
193
+ adf_content: Either a string (already plain text) or ADF document dict
194
+
195
+ Returns:
196
+ -------
197
+ Plain text string extracted from the ADF content
198
+
199
+ """
200
+ if isinstance(adf_content, str):
201
+ return adf_content
202
+
203
+ if not isinstance(adf_content, dict):
204
+ return str(adf_content) if adf_content else ""
205
+
206
+ def extract_text_recursive(node: dict[str, Any]) -> str:
207
+ """Recursively extract text from ADF nodes."""
208
+ if not isinstance(node, dict):
209
+ return ""
210
+
211
+ # If this is a text node, return its text
212
+ if node.get("type") == "text":
213
+ return node.get("text", "")
214
+
215
+ # If this node has content, process it recursively
216
+ content = node.get("content", [])
217
+ if isinstance(content, list):
218
+ return "".join(extract_text_recursive(child) for child in content)
219
+
220
+ return ""
221
+
222
+ try:
223
+ return extract_text_recursive(adf_content)
224
+ except Exception as e:
225
+ logger.warning(f"Failed to extract text from ADF: {e}")
226
+ return str(adf_content) if adf_content else ""
227
+
228
+
229
+ def convert_to_adf(text: str) -> dict[str, Any]:
230
+ """Convert plain text to Atlassian Document Format (ADF).
231
+
232
+ ADF is required for JIRA Cloud description fields.
233
+ This creates a simple document with paragraphs for each line.
234
+
235
+ Args:
236
+ ----
237
+ text: Plain text to convert
238
+
239
+ Returns:
240
+ -------
241
+ ADF document dictionary
242
+
243
+ """
244
+ if not text:
245
+ return {"type": "doc", "version": 1, "content": []}
246
+
247
+ # Split text into lines and create paragraphs
248
+ lines = text.split("\n")
249
+ content = []
250
+
251
+ for line in lines:
252
+ if line.strip(): # Non-empty line
253
+ content.append(
254
+ {"type": "paragraph", "content": [{"type": "text", "text": line}]}
255
+ )
256
+ else: # Empty line becomes empty paragraph
257
+ content.append({"type": "paragraph", "content": []})
258
+
259
+ return {"type": "doc", "version": 1, "content": content}
260
+
261
+
262
+ def convert_from_adf(adf_content: Any) -> str:
263
+ """Convert Atlassian Document Format (ADF) to plain text.
264
+
265
+ This extracts text content from ADF structure for display.
266
+
267
+ Args:
268
+ ----
269
+ adf_content: ADF document or plain string
270
+
271
+ Returns:
272
+ -------
273
+ Plain text string
274
+
275
+ """
276
+ if not adf_content:
277
+ return ""
278
+
279
+ # If it's already a string, return it (JIRA Server)
280
+ if isinstance(adf_content, str):
281
+ return adf_content
282
+
283
+ # Handle ADF structure
284
+ if not isinstance(adf_content, dict):
285
+ return str(adf_content)
286
+
287
+ content_nodes = adf_content.get("content", [])
288
+ lines = []
289
+
290
+ for node in content_nodes:
291
+ if node.get("type") == "paragraph":
292
+ paragraph_text = ""
293
+ for content_item in node.get("content", []):
294
+ if content_item.get("type") == "text":
295
+ paragraph_text += content_item.get("text", "")
296
+ lines.append(paragraph_text)
297
+ elif node.get("type") == "heading":
298
+ heading_text = ""
299
+ for content_item in node.get("content", []):
300
+ if content_item.get("type") == "text":
301
+ heading_text += content_item.get("text", "")
302
+ lines.append(heading_text)
303
+
304
+ return "\n".join(lines)