mcp-ticketer 0.1.30__py3-none-any.whl → 1.2.11__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 (109) hide show
  1. mcp_ticketer/__init__.py +10 -10
  2. mcp_ticketer/__version__.py +3 -3
  3. mcp_ticketer/adapters/__init__.py +2 -0
  4. mcp_ticketer/adapters/aitrackdown.py +796 -46
  5. mcp_ticketer/adapters/asana/__init__.py +15 -0
  6. mcp_ticketer/adapters/asana/adapter.py +1416 -0
  7. mcp_ticketer/adapters/asana/client.py +292 -0
  8. mcp_ticketer/adapters/asana/mappers.py +348 -0
  9. mcp_ticketer/adapters/asana/types.py +146 -0
  10. mcp_ticketer/adapters/github.py +879 -129
  11. mcp_ticketer/adapters/hybrid.py +11 -11
  12. mcp_ticketer/adapters/jira.py +973 -73
  13. mcp_ticketer/adapters/linear/__init__.py +24 -0
  14. mcp_ticketer/adapters/linear/adapter.py +2732 -0
  15. mcp_ticketer/adapters/linear/client.py +344 -0
  16. mcp_ticketer/adapters/linear/mappers.py +420 -0
  17. mcp_ticketer/adapters/linear/queries.py +479 -0
  18. mcp_ticketer/adapters/linear/types.py +360 -0
  19. mcp_ticketer/adapters/linear.py +10 -2315
  20. mcp_ticketer/analysis/__init__.py +23 -0
  21. mcp_ticketer/analysis/orphaned.py +218 -0
  22. mcp_ticketer/analysis/similarity.py +224 -0
  23. mcp_ticketer/analysis/staleness.py +266 -0
  24. mcp_ticketer/cache/memory.py +9 -8
  25. mcp_ticketer/cli/adapter_diagnostics.py +421 -0
  26. mcp_ticketer/cli/auggie_configure.py +116 -15
  27. mcp_ticketer/cli/codex_configure.py +274 -82
  28. mcp_ticketer/cli/configure.py +888 -151
  29. mcp_ticketer/cli/diagnostics.py +400 -157
  30. mcp_ticketer/cli/discover.py +297 -26
  31. mcp_ticketer/cli/gemini_configure.py +119 -26
  32. mcp_ticketer/cli/init_command.py +880 -0
  33. mcp_ticketer/cli/instruction_commands.py +435 -0
  34. mcp_ticketer/cli/linear_commands.py +616 -0
  35. mcp_ticketer/cli/main.py +203 -1165
  36. mcp_ticketer/cli/mcp_configure.py +474 -90
  37. mcp_ticketer/cli/mcp_server_commands.py +415 -0
  38. mcp_ticketer/cli/migrate_config.py +12 -8
  39. mcp_ticketer/cli/platform_commands.py +123 -0
  40. mcp_ticketer/cli/platform_detection.py +418 -0
  41. mcp_ticketer/cli/platform_installer.py +513 -0
  42. mcp_ticketer/cli/python_detection.py +126 -0
  43. mcp_ticketer/cli/queue_commands.py +15 -15
  44. mcp_ticketer/cli/setup_command.py +639 -0
  45. mcp_ticketer/cli/simple_health.py +90 -65
  46. mcp_ticketer/cli/ticket_commands.py +1013 -0
  47. mcp_ticketer/cli/update_checker.py +313 -0
  48. mcp_ticketer/cli/utils.py +114 -66
  49. mcp_ticketer/core/__init__.py +24 -1
  50. mcp_ticketer/core/adapter.py +250 -16
  51. mcp_ticketer/core/config.py +145 -37
  52. mcp_ticketer/core/env_discovery.py +101 -22
  53. mcp_ticketer/core/env_loader.py +349 -0
  54. mcp_ticketer/core/exceptions.py +160 -0
  55. mcp_ticketer/core/http_client.py +26 -26
  56. mcp_ticketer/core/instructions.py +405 -0
  57. mcp_ticketer/core/label_manager.py +732 -0
  58. mcp_ticketer/core/mappers.py +42 -30
  59. mcp_ticketer/core/models.py +280 -28
  60. mcp_ticketer/core/onepassword_secrets.py +379 -0
  61. mcp_ticketer/core/project_config.py +183 -49
  62. mcp_ticketer/core/registry.py +3 -3
  63. mcp_ticketer/core/session_state.py +171 -0
  64. mcp_ticketer/core/state_matcher.py +592 -0
  65. mcp_ticketer/core/url_parser.py +425 -0
  66. mcp_ticketer/core/validators.py +69 -0
  67. mcp_ticketer/defaults/ticket_instructions.md +644 -0
  68. mcp_ticketer/mcp/__init__.py +29 -1
  69. mcp_ticketer/mcp/__main__.py +60 -0
  70. mcp_ticketer/mcp/server/__init__.py +25 -0
  71. mcp_ticketer/mcp/server/__main__.py +60 -0
  72. mcp_ticketer/mcp/server/constants.py +58 -0
  73. mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
  74. mcp_ticketer/mcp/server/dto.py +195 -0
  75. mcp_ticketer/mcp/server/main.py +1343 -0
  76. mcp_ticketer/mcp/server/response_builder.py +206 -0
  77. mcp_ticketer/mcp/server/routing.py +655 -0
  78. mcp_ticketer/mcp/server/server_sdk.py +151 -0
  79. mcp_ticketer/mcp/server/tools/__init__.py +56 -0
  80. mcp_ticketer/mcp/server/tools/analysis_tools.py +495 -0
  81. mcp_ticketer/mcp/server/tools/attachment_tools.py +226 -0
  82. mcp_ticketer/mcp/server/tools/bulk_tools.py +273 -0
  83. mcp_ticketer/mcp/server/tools/comment_tools.py +152 -0
  84. mcp_ticketer/mcp/server/tools/config_tools.py +1439 -0
  85. mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
  86. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +921 -0
  87. mcp_ticketer/mcp/server/tools/instruction_tools.py +300 -0
  88. mcp_ticketer/mcp/server/tools/label_tools.py +948 -0
  89. mcp_ticketer/mcp/server/tools/pr_tools.py +152 -0
  90. mcp_ticketer/mcp/server/tools/search_tools.py +215 -0
  91. mcp_ticketer/mcp/server/tools/session_tools.py +170 -0
  92. mcp_ticketer/mcp/server/tools/ticket_tools.py +1268 -0
  93. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +547 -0
  94. mcp_ticketer/queue/__init__.py +1 -0
  95. mcp_ticketer/queue/health_monitor.py +168 -136
  96. mcp_ticketer/queue/manager.py +95 -25
  97. mcp_ticketer/queue/queue.py +40 -21
  98. mcp_ticketer/queue/run_worker.py +6 -1
  99. mcp_ticketer/queue/ticket_registry.py +213 -155
  100. mcp_ticketer/queue/worker.py +109 -49
  101. mcp_ticketer-1.2.11.dist-info/METADATA +792 -0
  102. mcp_ticketer-1.2.11.dist-info/RECORD +110 -0
  103. mcp_ticketer/mcp/server.py +0 -1895
  104. mcp_ticketer-0.1.30.dist-info/METADATA +0 -413
  105. mcp_ticketer-0.1.30.dist-info/RECORD +0 -49
  106. {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/WHEEL +0 -0
  107. {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/entry_points.txt +0 -0
  108. {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/licenses/LICENSE +0 -0
  109. {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,420 @@
1
+ """Data transformation mappers for Linear API responses."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime
6
+ from typing import Any
7
+
8
+ from ...core.models import Attachment, Comment, Epic, Priority, Task, TicketState
9
+ from .types import extract_linear_metadata, get_universal_priority, get_universal_state
10
+
11
+
12
+ def map_linear_issue_to_task(issue_data: dict[str, Any]) -> Task:
13
+ """Convert Linear issue or sub-issue data to universal Task model.
14
+
15
+ Handles both top-level issues (no parent) and sub-issues (child items
16
+ with a parent issue).
17
+
18
+ Args:
19
+ ----
20
+ issue_data: Raw Linear issue data from GraphQL
21
+
22
+ Returns:
23
+ -------
24
+ Universal Task model
25
+
26
+ """
27
+ # Extract basic fields
28
+ task_id = issue_data["identifier"]
29
+ title = issue_data["title"]
30
+ description = issue_data.get("description", "")
31
+
32
+ # Map priority
33
+ linear_priority = issue_data.get("priority", 3)
34
+ priority = get_universal_priority(linear_priority)
35
+
36
+ # Map state with synonym matching (1M-164)
37
+ state_data = issue_data.get("state", {})
38
+ state_type = state_data.get("type", "unstarted")
39
+ state_name = state_data.get("name") # Extract state name for synonym matching
40
+ state = get_universal_state(state_type, state_name)
41
+
42
+ # Extract assignee
43
+ assignee = None
44
+ if issue_data.get("assignee"):
45
+ assignee_data = issue_data["assignee"]
46
+ assignee = assignee_data.get("email") or assignee_data.get("displayName")
47
+
48
+ # Extract creator
49
+ creator = None
50
+ if issue_data.get("creator"):
51
+ creator_data = issue_data["creator"]
52
+ creator = creator_data.get("email") or creator_data.get("displayName")
53
+
54
+ # Extract tags (labels)
55
+ tags = []
56
+ if issue_data.get("labels", {}).get("nodes"):
57
+ tags = [label["name"] for label in issue_data["labels"]["nodes"]]
58
+
59
+ # Extract parent epic (project)
60
+ parent_epic = None
61
+ if issue_data.get("project"):
62
+ parent_epic = issue_data["project"]["id"]
63
+
64
+ # Extract parent issue
65
+ parent_issue = None
66
+ if issue_data.get("parent"):
67
+ parent_issue = issue_data["parent"]["identifier"]
68
+
69
+ # Extract dates
70
+ created_at = None
71
+ if issue_data.get("createdAt"):
72
+ created_at = datetime.fromisoformat(
73
+ issue_data["createdAt"].replace("Z", "+00:00")
74
+ )
75
+
76
+ updated_at = None
77
+ if issue_data.get("updatedAt"):
78
+ updated_at = datetime.fromisoformat(
79
+ issue_data["updatedAt"].replace("Z", "+00:00")
80
+ )
81
+
82
+ # Extract child issue IDs
83
+ children = extract_child_issue_ids(issue_data)
84
+
85
+ # Extract Linear-specific metadata
86
+ linear_metadata = extract_linear_metadata(issue_data)
87
+ metadata = {"linear": linear_metadata} if linear_metadata else {}
88
+
89
+ return Task(
90
+ id=task_id,
91
+ title=title,
92
+ description=description,
93
+ state=state,
94
+ priority=priority,
95
+ assignee=assignee,
96
+ creator=creator,
97
+ tags=tags,
98
+ parent_epic=parent_epic,
99
+ parent_issue=parent_issue,
100
+ children=children,
101
+ created_at=created_at,
102
+ updated_at=updated_at,
103
+ metadata=metadata,
104
+ )
105
+
106
+
107
+ def map_linear_project_to_epic(project_data: dict[str, Any]) -> Epic:
108
+ """Convert Linear project data to universal Epic model.
109
+
110
+ Args:
111
+ ----
112
+ project_data: Raw Linear project data from GraphQL
113
+
114
+ Returns:
115
+ -------
116
+ Universal Epic model
117
+
118
+ """
119
+ # Extract basic fields
120
+ epic_id = project_data["id"]
121
+ title = project_data["name"]
122
+ description = project_data.get("description", "")
123
+
124
+ # Map state based on project state
125
+ project_state = project_data.get("state", "planned")
126
+ if project_state == "completed":
127
+ state = TicketState.DONE
128
+ elif project_state == "started":
129
+ state = TicketState.IN_PROGRESS
130
+ elif project_state == "canceled":
131
+ state = TicketState.CLOSED
132
+ else:
133
+ state = TicketState.OPEN
134
+
135
+ # Extract dates
136
+ created_at = None
137
+ if project_data.get("createdAt"):
138
+ created_at = datetime.fromisoformat(
139
+ project_data["createdAt"].replace("Z", "+00:00")
140
+ )
141
+
142
+ updated_at = None
143
+ if project_data.get("updatedAt"):
144
+ updated_at = datetime.fromisoformat(
145
+ project_data["updatedAt"].replace("Z", "+00:00")
146
+ )
147
+
148
+ # Extract Linear-specific metadata
149
+ metadata: dict[str, Any] = {"linear": {}}
150
+ if project_data.get("url"):
151
+ metadata["linear"]["linear_url"] = project_data["url"]
152
+ if project_data.get("icon"):
153
+ metadata["linear"]["icon"] = project_data["icon"]
154
+ if project_data.get("color"):
155
+ metadata["linear"]["color"] = project_data["color"]
156
+ if project_data.get("targetDate"):
157
+ metadata["linear"]["target_date"] = project_data["targetDate"]
158
+
159
+ return Epic(
160
+ id=epic_id,
161
+ title=title,
162
+ description=description,
163
+ state=state,
164
+ priority=Priority.MEDIUM, # Projects don't have priority in Linear
165
+ created_at=created_at,
166
+ updated_at=updated_at,
167
+ metadata=metadata if metadata["linear"] else {},
168
+ )
169
+
170
+
171
+ def map_linear_comment_to_comment(
172
+ comment_data: dict[str, Any], ticket_id: str
173
+ ) -> Comment:
174
+ """Convert Linear comment data to universal Comment model.
175
+
176
+ Args:
177
+ ----
178
+ comment_data: Raw Linear comment data from GraphQL
179
+ ticket_id: ID of the ticket this comment belongs to
180
+
181
+ Returns:
182
+ -------
183
+ Universal Comment model
184
+
185
+ """
186
+ # Extract basic fields
187
+ comment_id = comment_data["id"]
188
+ body = comment_data.get("body", "")
189
+
190
+ # Extract author
191
+ author = None
192
+ if comment_data.get("user"):
193
+ user_data = comment_data["user"]
194
+ author = user_data.get("email") or user_data.get("displayName")
195
+
196
+ # Extract dates
197
+ created_at = None
198
+ if comment_data.get("createdAt"):
199
+ created_at = datetime.fromisoformat(
200
+ comment_data["createdAt"].replace("Z", "+00:00")
201
+ )
202
+
203
+ # Note: Comment model doesn't have updated_at field
204
+ # Store it in metadata if needed
205
+ metadata = {}
206
+ if comment_data.get("updatedAt"):
207
+ metadata["updated_at"] = comment_data["updatedAt"]
208
+
209
+ return Comment(
210
+ id=comment_id,
211
+ ticket_id=ticket_id,
212
+ content=body,
213
+ author=author,
214
+ created_at=created_at,
215
+ metadata=metadata,
216
+ )
217
+
218
+
219
+ def build_linear_issue_input(task: Task, team_id: str) -> dict[str, Any]:
220
+ """Build Linear issue or sub-issue input from universal Task model.
221
+
222
+ Creates input for a top-level issue when task.parent_issue is not set,
223
+ or for a sub-issue when task.parent_issue is provided.
224
+
225
+ Args:
226
+ ----
227
+ task: Universal Task model
228
+ team_id: Linear team ID
229
+
230
+ Returns:
231
+ -------
232
+ Linear issue input dictionary
233
+
234
+ """
235
+ from .types import get_linear_priority
236
+
237
+ issue_input: dict[str, Any] = {
238
+ "title": task.title,
239
+ "teamId": team_id,
240
+ }
241
+
242
+ # Add description if provided
243
+ if task.description:
244
+ issue_input["description"] = task.description
245
+
246
+ # Add priority
247
+ if task.priority:
248
+ issue_input["priority"] = get_linear_priority(task.priority)
249
+
250
+ # Add assignee if provided (assumes it's a user ID)
251
+ if task.assignee:
252
+ issue_input["assigneeId"] = task.assignee
253
+
254
+ # Add parent issue if provided
255
+ if task.parent_issue:
256
+ issue_input["parentId"] = task.parent_issue
257
+
258
+ # Add project (epic) if provided
259
+ if task.parent_epic:
260
+ issue_input["projectId"] = task.parent_epic
261
+
262
+ # DO NOT set labelIds here - adapter will handle label resolution
263
+ # Labels are resolved from names to UUIDs in LinearAdapter._create_task()
264
+ # If we set it here, we create a type mismatch (names vs UUIDs)
265
+ # The adapter's label resolution logic (lines 982-988) handles this properly
266
+ #
267
+ # Bug Fix (v1.1.1): Previously, setting labelIds to tag names here caused
268
+ # "Argument Validation Error" from Linear's GraphQL API. The API requires
269
+ # UUIDs (e.g., "uuid-1"), not names (e.g., "bug"). This was fixed by:
270
+ # 1. Removing labelIds assignment in mapper (this file)
271
+ # 2. Adding UUID validation in adapter._create_task() (adapter.py:1047-1060)
272
+ # 3. Changing GraphQL labelIds type to [String!]! (queries.py)
273
+ #
274
+ # See: docs/TROUBLESHOOTING.md#issue-argument-validation-error-when-creating-issues-with-labels
275
+
276
+ # Add Linear-specific metadata
277
+ if task.metadata and "linear" in task.metadata:
278
+ linear_meta = task.metadata["linear"]
279
+ if "due_date" in linear_meta:
280
+ issue_input["dueDate"] = linear_meta["due_date"]
281
+ if "cycle_id" in linear_meta:
282
+ issue_input["cycleId"] = linear_meta["cycle_id"]
283
+ if "estimate" in linear_meta:
284
+ issue_input["estimate"] = linear_meta["estimate"]
285
+
286
+ return issue_input
287
+
288
+
289
+ def build_linear_issue_update_input(updates: dict[str, Any]) -> dict[str, Any]:
290
+ """Build Linear issue update input from update dictionary.
291
+
292
+ Args:
293
+ ----
294
+ updates: Dictionary of fields to update
295
+
296
+ Returns:
297
+ -------
298
+ Linear issue update input dictionary
299
+
300
+ """
301
+ from .types import get_linear_priority
302
+
303
+ update_input = {}
304
+
305
+ # Map standard fields
306
+ if "title" in updates:
307
+ update_input["title"] = updates["title"]
308
+
309
+ if "description" in updates:
310
+ update_input["description"] = updates["description"]
311
+
312
+ if "priority" in updates:
313
+ priority = (
314
+ Priority(updates["priority"])
315
+ if isinstance(updates["priority"], str)
316
+ else updates["priority"]
317
+ )
318
+ update_input["priority"] = get_linear_priority(priority)
319
+
320
+ if "assignee" in updates:
321
+ update_input["assigneeId"] = updates["assignee"]
322
+
323
+ # Handle state transitions (would need workflow state mapping)
324
+ if "state" in updates:
325
+ # This would need to be handled by the adapter with proper state mapping
326
+ pass
327
+
328
+ # Handle metadata updates
329
+ if "metadata" in updates and "linear" in updates["metadata"]:
330
+ linear_meta = updates["metadata"]["linear"]
331
+ if "due_date" in linear_meta:
332
+ update_input["dueDate"] = linear_meta["due_date"]
333
+ if "cycle_id" in linear_meta:
334
+ update_input["cycleId"] = linear_meta["cycle_id"]
335
+ if "project_id" in linear_meta:
336
+ update_input["projectId"] = linear_meta["project_id"]
337
+ if "estimate" in linear_meta:
338
+ update_input["estimate"] = linear_meta["estimate"]
339
+
340
+ return update_input
341
+
342
+
343
+ def extract_child_issue_ids(issue_data: dict[str, Any]) -> list[str]:
344
+ """Extract child issue IDs from Linear issue data.
345
+
346
+ Args:
347
+ ----
348
+ issue_data: Raw Linear issue data from GraphQL
349
+
350
+ Returns:
351
+ -------
352
+ List of child issue identifiers
353
+
354
+ """
355
+ child_ids = []
356
+ if issue_data.get("children", {}).get("nodes"):
357
+ child_ids = [child["identifier"] for child in issue_data["children"]["nodes"]]
358
+ return child_ids
359
+
360
+
361
+ def map_linear_attachment_to_attachment(
362
+ attachment_data: dict[str, Any], ticket_id: str
363
+ ) -> Attachment:
364
+ """Convert Linear attachment data to universal Attachment model.
365
+
366
+ Args:
367
+ ----
368
+ attachment_data: Raw Linear attachment data from GraphQL
369
+ ticket_id: ID of the ticket this attachment belongs to
370
+
371
+ Returns:
372
+ -------
373
+ Universal Attachment model
374
+
375
+ Note:
376
+ ----
377
+ Linear attachment URLs require authentication with API key.
378
+ URLs are in format: https://files.linear.app/workspace/attachment-id/filename
379
+ Authentication header: Authorization: Bearer {api_key}
380
+
381
+ """
382
+ # Extract basic fields
383
+ attachment_id = attachment_data.get("id")
384
+ title = attachment_data.get("title", "Untitled")
385
+ url = attachment_data.get("url")
386
+ subtitle = attachment_data.get("subtitle")
387
+
388
+ # Extract dates
389
+ created_at = None
390
+ if attachment_data.get("createdAt"):
391
+ created_at = datetime.fromisoformat(
392
+ attachment_data["createdAt"].replace("Z", "+00:00")
393
+ )
394
+
395
+ if attachment_data.get("updatedAt"):
396
+ datetime.fromisoformat(attachment_data["updatedAt"].replace("Z", "+00:00"))
397
+
398
+ # Build metadata with Linear-specific fields
399
+ metadata = {
400
+ "linear": {
401
+ "id": attachment_id,
402
+ "title": title,
403
+ "subtitle": subtitle,
404
+ "metadata": attachment_data.get("metadata"),
405
+ "updated_at": attachment_data.get("updatedAt"),
406
+ }
407
+ }
408
+
409
+ return Attachment(
410
+ id=attachment_id,
411
+ ticket_id=ticket_id,
412
+ filename=title, # Linear uses 'title' as filename
413
+ url=url,
414
+ content_type=None, # Linear doesn't provide MIME type in GraphQL
415
+ size_bytes=None, # Linear doesn't provide size in GraphQL
416
+ created_at=created_at,
417
+ created_by=None, # Not included in standard fragment
418
+ description=subtitle,
419
+ metadata=metadata,
420
+ )