mcp-ticketer 0.12.0__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 (129) hide show
  1. mcp_ticketer/__init__.py +10 -10
  2. mcp_ticketer/__version__.py +1 -1
  3. mcp_ticketer/_version_scm.py +1 -0
  4. mcp_ticketer/adapters/aitrackdown.py +507 -6
  5. mcp_ticketer/adapters/asana/adapter.py +229 -0
  6. mcp_ticketer/adapters/asana/mappers.py +14 -0
  7. mcp_ticketer/adapters/github/__init__.py +26 -0
  8. mcp_ticketer/adapters/github/adapter.py +3229 -0
  9. mcp_ticketer/adapters/github/client.py +335 -0
  10. mcp_ticketer/adapters/github/mappers.py +797 -0
  11. mcp_ticketer/adapters/github/queries.py +692 -0
  12. mcp_ticketer/adapters/github/types.py +460 -0
  13. mcp_ticketer/adapters/hybrid.py +47 -5
  14. mcp_ticketer/adapters/jira/__init__.py +35 -0
  15. mcp_ticketer/adapters/jira/adapter.py +1351 -0
  16. mcp_ticketer/adapters/jira/client.py +271 -0
  17. mcp_ticketer/adapters/jira/mappers.py +246 -0
  18. mcp_ticketer/adapters/jira/queries.py +216 -0
  19. mcp_ticketer/adapters/jira/types.py +304 -0
  20. mcp_ticketer/adapters/linear/adapter.py +2730 -139
  21. mcp_ticketer/adapters/linear/client.py +175 -3
  22. mcp_ticketer/adapters/linear/mappers.py +203 -8
  23. mcp_ticketer/adapters/linear/queries.py +280 -3
  24. mcp_ticketer/adapters/linear/types.py +120 -4
  25. mcp_ticketer/analysis/__init__.py +56 -0
  26. mcp_ticketer/analysis/dependency_graph.py +255 -0
  27. mcp_ticketer/analysis/health_assessment.py +304 -0
  28. mcp_ticketer/analysis/orphaned.py +218 -0
  29. mcp_ticketer/analysis/project_status.py +594 -0
  30. mcp_ticketer/analysis/similarity.py +224 -0
  31. mcp_ticketer/analysis/staleness.py +266 -0
  32. mcp_ticketer/automation/__init__.py +11 -0
  33. mcp_ticketer/automation/project_updates.py +378 -0
  34. mcp_ticketer/cli/adapter_diagnostics.py +3 -1
  35. mcp_ticketer/cli/auggie_configure.py +17 -5
  36. mcp_ticketer/cli/codex_configure.py +97 -61
  37. mcp_ticketer/cli/configure.py +1288 -105
  38. mcp_ticketer/cli/cursor_configure.py +314 -0
  39. mcp_ticketer/cli/diagnostics.py +13 -12
  40. mcp_ticketer/cli/discover.py +5 -0
  41. mcp_ticketer/cli/gemini_configure.py +17 -5
  42. mcp_ticketer/cli/init_command.py +880 -0
  43. mcp_ticketer/cli/install_mcp_server.py +418 -0
  44. mcp_ticketer/cli/instruction_commands.py +6 -0
  45. mcp_ticketer/cli/main.py +267 -3175
  46. mcp_ticketer/cli/mcp_configure.py +821 -119
  47. mcp_ticketer/cli/mcp_server_commands.py +415 -0
  48. mcp_ticketer/cli/platform_detection.py +77 -12
  49. mcp_ticketer/cli/platform_installer.py +545 -0
  50. mcp_ticketer/cli/project_update_commands.py +350 -0
  51. mcp_ticketer/cli/setup_command.py +795 -0
  52. mcp_ticketer/cli/simple_health.py +12 -10
  53. mcp_ticketer/cli/ticket_commands.py +705 -103
  54. mcp_ticketer/cli/utils.py +113 -0
  55. mcp_ticketer/core/__init__.py +56 -6
  56. mcp_ticketer/core/adapter.py +533 -2
  57. mcp_ticketer/core/config.py +21 -21
  58. mcp_ticketer/core/exceptions.py +7 -1
  59. mcp_ticketer/core/label_manager.py +732 -0
  60. mcp_ticketer/core/mappers.py +31 -19
  61. mcp_ticketer/core/milestone_manager.py +252 -0
  62. mcp_ticketer/core/models.py +480 -0
  63. mcp_ticketer/core/onepassword_secrets.py +1 -1
  64. mcp_ticketer/core/priority_matcher.py +463 -0
  65. mcp_ticketer/core/project_config.py +132 -14
  66. mcp_ticketer/core/project_utils.py +281 -0
  67. mcp_ticketer/core/project_validator.py +376 -0
  68. mcp_ticketer/core/session_state.py +176 -0
  69. mcp_ticketer/core/state_matcher.py +625 -0
  70. mcp_ticketer/core/url_parser.py +425 -0
  71. mcp_ticketer/core/validators.py +69 -0
  72. mcp_ticketer/mcp/server/__main__.py +2 -1
  73. mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
  74. mcp_ticketer/mcp/server/main.py +106 -25
  75. mcp_ticketer/mcp/server/routing.py +723 -0
  76. mcp_ticketer/mcp/server/server_sdk.py +58 -0
  77. mcp_ticketer/mcp/server/tools/__init__.py +33 -11
  78. mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
  79. mcp_ticketer/mcp/server/tools/attachment_tools.py +5 -5
  80. mcp_ticketer/mcp/server/tools/bulk_tools.py +259 -202
  81. mcp_ticketer/mcp/server/tools/comment_tools.py +74 -12
  82. mcp_ticketer/mcp/server/tools/config_tools.py +1391 -145
  83. mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
  84. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +870 -460
  85. mcp_ticketer/mcp/server/tools/instruction_tools.py +7 -5
  86. mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
  87. mcp_ticketer/mcp/server/tools/milestone_tools.py +338 -0
  88. mcp_ticketer/mcp/server/tools/pr_tools.py +3 -7
  89. mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
  90. mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
  91. mcp_ticketer/mcp/server/tools/search_tools.py +209 -97
  92. mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
  93. mcp_ticketer/mcp/server/tools/ticket_tools.py +1107 -124
  94. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +218 -236
  95. mcp_ticketer/queue/queue.py +68 -0
  96. mcp_ticketer/queue/worker.py +1 -1
  97. mcp_ticketer/utils/__init__.py +5 -0
  98. mcp_ticketer/utils/token_utils.py +246 -0
  99. mcp_ticketer-2.2.13.dist-info/METADATA +1396 -0
  100. mcp_ticketer-2.2.13.dist-info/RECORD +158 -0
  101. mcp_ticketer-2.2.13.dist-info/top_level.txt +2 -0
  102. py_mcp_installer/examples/phase3_demo.py +178 -0
  103. py_mcp_installer/scripts/manage_version.py +54 -0
  104. py_mcp_installer/setup.py +6 -0
  105. py_mcp_installer/src/py_mcp_installer/__init__.py +153 -0
  106. py_mcp_installer/src/py_mcp_installer/command_builder.py +445 -0
  107. py_mcp_installer/src/py_mcp_installer/config_manager.py +541 -0
  108. py_mcp_installer/src/py_mcp_installer/exceptions.py +243 -0
  109. py_mcp_installer/src/py_mcp_installer/installation_strategy.py +617 -0
  110. py_mcp_installer/src/py_mcp_installer/installer.py +656 -0
  111. py_mcp_installer/src/py_mcp_installer/mcp_inspector.py +750 -0
  112. py_mcp_installer/src/py_mcp_installer/platform_detector.py +451 -0
  113. py_mcp_installer/src/py_mcp_installer/platforms/__init__.py +26 -0
  114. py_mcp_installer/src/py_mcp_installer/platforms/claude_code.py +225 -0
  115. py_mcp_installer/src/py_mcp_installer/platforms/codex.py +181 -0
  116. py_mcp_installer/src/py_mcp_installer/platforms/cursor.py +191 -0
  117. py_mcp_installer/src/py_mcp_installer/types.py +222 -0
  118. py_mcp_installer/src/py_mcp_installer/utils.py +463 -0
  119. py_mcp_installer/tests/__init__.py +0 -0
  120. py_mcp_installer/tests/platforms/__init__.py +0 -0
  121. py_mcp_installer/tests/test_platform_detector.py +17 -0
  122. mcp_ticketer/adapters/github.py +0 -1574
  123. mcp_ticketer/adapters/jira.py +0 -1258
  124. mcp_ticketer-0.12.0.dist-info/METADATA +0 -550
  125. mcp_ticketer-0.12.0.dist-info/RECORD +0 -91
  126. mcp_ticketer-0.12.0.dist-info/top_level.txt +0 -1
  127. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.2.13.dist-info}/WHEEL +0 -0
  128. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.2.13.dist-info}/entry_points.txt +0 -0
  129. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.2.13.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,460 @@
1
+ """GitHub-specific type definitions and conversion utilities.
2
+
3
+ This module contains:
4
+ - State and priority mappings between GitHub and universal models
5
+ - Type conversion helper functions
6
+ - GitHub-specific constants and enums
7
+ - TypedDict definitions for GitHub API responses
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Any, TypedDict
13
+
14
+ from ...core.models import Priority, TicketState
15
+
16
+
17
+ class GitHubStateMapping:
18
+ """GitHub issue states and label-based extended states.
19
+
20
+ Design Decision: GitHub's two-state model (open/closed)
21
+
22
+ GitHub natively only supports two states: 'open' and 'closed'.
23
+ To support richer workflow states, we use labels to extend the state model.
24
+
25
+ Rationale:
26
+ - Maintains compatibility with GitHub's API limitations
27
+ - Allows flexible workflow states through labeling
28
+ - Enables state transitions without closing issues
29
+
30
+ Trade-offs:
31
+ - State changes require label management (more API calls)
32
+ - Labels are user-visible and can be manually modified
33
+ - No built-in state transition validation in GitHub
34
+
35
+ Extension Point: Custom state labels can be configured per repository
36
+ through adapter configuration.
37
+ """
38
+
39
+ # GitHub native states
40
+ OPEN = "open"
41
+ CLOSED = "closed"
42
+
43
+ # Extended states via labels
44
+ # These labels represent workflow states beyond GitHub's native open/closed
45
+ STATE_LABELS = {
46
+ TicketState.IN_PROGRESS: "in-progress",
47
+ TicketState.READY: "ready",
48
+ TicketState.TESTED: "tested",
49
+ TicketState.WAITING: "waiting",
50
+ TicketState.BLOCKED: "blocked",
51
+ }
52
+
53
+ # Priority labels mapping
54
+ # Multiple label patterns support different team conventions
55
+ PRIORITY_LABELS = {
56
+ Priority.CRITICAL: ["P0", "critical", "urgent"],
57
+ Priority.HIGH: ["P1", "high"],
58
+ Priority.MEDIUM: ["P2", "medium"],
59
+ Priority.LOW: ["P3", "low"],
60
+ }
61
+
62
+
63
+ def get_universal_state(
64
+ github_state: str,
65
+ labels: list[str],
66
+ ) -> TicketState:
67
+ """Convert GitHub state + labels to universal TicketState.
68
+
69
+ GitHub has only two states (open/closed), so we use labels to infer
70
+ the extended workflow state.
71
+
72
+ Args:
73
+ ----
74
+ github_state: GitHub issue state ('open' or 'closed')
75
+ labels: List of label names attached to the issue
76
+
77
+ Returns:
78
+ -------
79
+ Universal ticket state enum value
80
+
81
+ Performance:
82
+ -----------
83
+ Time Complexity: O(n*m) where n=number of labels, m=state labels to check
84
+ Worst case: ~5 state labels * ~20 issue labels = 100 comparisons
85
+
86
+ Example:
87
+ -------
88
+ >>> get_universal_state("open", ["in-progress", "bug"])
89
+ TicketState.IN_PROGRESS
90
+ >>> get_universal_state("closed", [])
91
+ TicketState.CLOSED
92
+ """
93
+ # Closed issues are always CLOSED state
94
+ if github_state == "closed":
95
+ return TicketState.CLOSED
96
+
97
+ # Normalize labels for comparison
98
+ label_names = [label.lower() for label in labels]
99
+
100
+ # Check for extended state labels
101
+ for state, label_name in GitHubStateMapping.STATE_LABELS.items():
102
+ if label_name.lower() in label_names:
103
+ return state
104
+
105
+ # Default to OPEN if no state label found
106
+ return TicketState.OPEN
107
+
108
+
109
+ def extract_state_from_issue(issue: dict[str, Any]) -> TicketState:
110
+ """Extract ticket state from GitHub issue data.
111
+
112
+ Handles multiple GitHub API response formats:
113
+ - REST API v3: labels as array of objects
114
+ - GraphQL API v4: labels.nodes as array
115
+ - Legacy formats: labels as array of strings
116
+
117
+ Args:
118
+ ----
119
+ issue: GitHub issue data from REST or GraphQL API
120
+
121
+ Returns:
122
+ -------
123
+ Universal ticket state
124
+
125
+ Example:
126
+ -------
127
+ >>> issue = {"state": "open", "labels": [{"name": "ready"}]}
128
+ >>> extract_state_from_issue(issue)
129
+ TicketState.READY
130
+ """
131
+ # Extract labels from various formats
132
+ labels = []
133
+ if "labels" in issue:
134
+ if isinstance(issue["labels"], list):
135
+ # REST API format: array of objects or strings
136
+ labels = [
137
+ label.get("name", "") if isinstance(label, dict) else str(label)
138
+ for label in issue["labels"]
139
+ ]
140
+ elif isinstance(issue["labels"], dict) and "nodes" in issue["labels"]:
141
+ # GraphQL format: labels.nodes array
142
+ labels = [label["name"] for label in issue["labels"]["nodes"]]
143
+
144
+ return get_universal_state(issue["state"], labels)
145
+
146
+
147
+ def get_priority_from_labels(
148
+ labels: list[str],
149
+ custom_priority_scheme: dict[str, list[str]] | None = None,
150
+ ) -> Priority:
151
+ """Extract priority from GitHub issue labels.
152
+
153
+ Priority is inferred from labels since GitHub has no native priority field.
154
+ Supports custom priority label schemes for team-specific conventions.
155
+
156
+ Args:
157
+ ----
158
+ labels: List of label names
159
+ custom_priority_scheme: Optional custom mapping of priority -> label patterns
160
+
161
+ Returns:
162
+ -------
163
+ Priority enum value (defaults to MEDIUM if not found)
164
+
165
+ Performance:
166
+ -----------
167
+ Time Complexity: O(n*m) where n=labels, m=priority patterns
168
+ Expected: ~20 labels * ~12 priority patterns = 240 comparisons worst case
169
+
170
+ Example:
171
+ -------
172
+ >>> get_priority_from_labels(["P0", "bug"])
173
+ Priority.CRITICAL
174
+ >>> get_priority_from_labels(["enhancement"])
175
+ Priority.MEDIUM # default
176
+ """
177
+ label_names = [label.lower() for label in labels]
178
+
179
+ # Check custom priority scheme first
180
+ if custom_priority_scheme:
181
+ for priority_str, label_patterns in custom_priority_scheme.items():
182
+ for pattern in label_patterns:
183
+ if any(pattern.lower() in label for label in label_names):
184
+ return Priority(priority_str)
185
+
186
+ # Check default priority labels
187
+ for priority, priority_labels in GitHubStateMapping.PRIORITY_LABELS.items():
188
+ for priority_label in priority_labels:
189
+ if priority_label.lower() in label_names:
190
+ return priority
191
+
192
+ return Priority.MEDIUM
193
+
194
+
195
+ def get_priority_label(
196
+ priority: Priority,
197
+ custom_priority_scheme: dict[str, list[str]] | None = None,
198
+ ) -> str:
199
+ """Get label name for a priority level.
200
+
201
+ Returns the first matching label from custom scheme or default labels.
202
+ Falls back to P0/P1/P2/P3 notation if no match found.
203
+
204
+ Args:
205
+ ----
206
+ priority: Universal priority enum
207
+ custom_priority_scheme: Optional custom priority label mapping
208
+
209
+ Returns:
210
+ -------
211
+ Label name to apply to issue
212
+
213
+ Example:
214
+ -------
215
+ >>> get_priority_label(Priority.CRITICAL)
216
+ 'P0'
217
+ >>> get_priority_label(Priority.HIGH, {"high": ["urgent", "high-priority"]})
218
+ 'urgent'
219
+ """
220
+ # Check custom scheme first
221
+ if custom_priority_scheme:
222
+ labels = custom_priority_scheme.get(priority.value, [])
223
+ if labels:
224
+ return labels[0]
225
+
226
+ # Use default labels
227
+ labels = GitHubStateMapping.PRIORITY_LABELS.get(priority, [])
228
+ if labels:
229
+ return labels[0]
230
+
231
+ # Fallback to P0-P3 notation
232
+ priority_index = list(Priority).index(priority)
233
+ return f"P{priority_index}"
234
+
235
+
236
+ def get_state_label(state: TicketState) -> str | None:
237
+ """Get the label name for extended workflow states.
238
+
239
+ Args:
240
+ ----
241
+ state: Universal ticket state
242
+
243
+ Returns:
244
+ -------
245
+ Label name if state requires a label, None for native GitHub states
246
+
247
+ Example:
248
+ -------
249
+ >>> get_state_label(TicketState.IN_PROGRESS)
250
+ 'in-progress'
251
+ >>> get_state_label(TicketState.OPEN)
252
+ None # Native GitHub state, no label needed
253
+ """
254
+ return GitHubStateMapping.STATE_LABELS.get(state)
255
+
256
+
257
+ def get_github_state(state: TicketState) -> str:
258
+ """Map universal state to GitHub native state.
259
+
260
+ Only two valid values: 'open' or 'closed'.
261
+ Extended states map to 'open' with additional labels.
262
+
263
+ Args:
264
+ ----
265
+ state: Universal ticket state
266
+
267
+ Returns:
268
+ -------
269
+ GitHub state string ('open' or 'closed')
270
+
271
+ Example:
272
+ -------
273
+ >>> get_github_state(TicketState.IN_PROGRESS)
274
+ 'open'
275
+ >>> get_github_state(TicketState.CLOSED)
276
+ 'closed'
277
+ """
278
+ if state in (TicketState.DONE, TicketState.CLOSED):
279
+ return GitHubStateMapping.CLOSED
280
+ return GitHubStateMapping.OPEN
281
+
282
+
283
+ # =============================================================================
284
+ # GitHub Projects V2 Type Definitions
285
+ # =============================================================================
286
+
287
+
288
+ class ProjectV2Owner(TypedDict, total=False):
289
+ """GitHub ProjectV2 owner (Organization or User).
290
+
291
+ Attributes:
292
+ __typename: Type discriminator ("Organization" or "User")
293
+ login: Owner login name
294
+ id: Owner node ID
295
+ """
296
+
297
+ __typename: str
298
+ login: str
299
+ id: str
300
+
301
+
302
+ class ProjectV2PageInfo(TypedDict, total=False):
303
+ """GraphQL pagination info for ProjectV2 queries.
304
+
305
+ Attributes:
306
+ hasNextPage: Whether more results exist
307
+ endCursor: Cursor for next page
308
+ """
309
+
310
+ hasNextPage: bool
311
+ endCursor: str | None
312
+
313
+
314
+ class ProjectV2ItemsConnection(TypedDict, total=False):
315
+ """ProjectV2 items connection.
316
+
317
+ Attributes:
318
+ totalCount: Total number of items in project
319
+ """
320
+
321
+ totalCount: int
322
+
323
+
324
+ class ProjectV2Node(TypedDict, total=False):
325
+ """GitHub ProjectV2 GraphQL node.
326
+
327
+ Represents a single GitHub Projects V2 project from the GraphQL API.
328
+ This type matches the structure returned by PROJECT_V2_FRAGMENT.
329
+
330
+ Design Decision: Total vs Required Fields
331
+ -----------------------------------------
332
+ Using total=False allows optional fields to be omitted, matching the
333
+ GraphQL API where many fields are nullable or may not be queried.
334
+
335
+ Required fields (id, number, title) are still enforced at the Pydantic
336
+ model level in map_github_projectv2_to_project().
337
+
338
+ Attributes:
339
+ id: GitHub node ID (e.g., "PVT_kwDOABcdefgh")
340
+ number: Project number (e.g., 5)
341
+ title: Project title
342
+ shortDescription: Brief description (max 256 chars)
343
+ readme: Markdown readme content
344
+ public: Whether project is publicly visible
345
+ closed: Whether project is closed
346
+ url: Direct URL to project
347
+ createdAt: ISO timestamp of creation
348
+ updatedAt: ISO timestamp of last update
349
+ closedAt: ISO timestamp of closure (if closed)
350
+ owner: Owner (Organization or User)
351
+ items: Items connection with totalCount
352
+ """
353
+
354
+ id: str
355
+ number: int
356
+ title: str
357
+ shortDescription: str | None
358
+ readme: str | None
359
+ public: bool
360
+ closed: bool
361
+ url: str
362
+ createdAt: str
363
+ updatedAt: str
364
+ closedAt: str | None
365
+ owner: ProjectV2Owner
366
+ items: ProjectV2ItemsConnection | None
367
+
368
+
369
+ class ProjectV2Response(TypedDict, total=False):
370
+ """Response from GET_PROJECT_QUERY or GET_PROJECT_BY_ID_QUERY.
371
+
372
+ Single project query response wrapping the project node.
373
+
374
+ Attributes:
375
+ organization: Organization containing projectV2 field
376
+ node: Direct node lookup result
377
+ """
378
+
379
+ organization: dict[str, ProjectV2Node | None]
380
+ node: ProjectV2Node | None
381
+
382
+
383
+ class ProjectV2Connection(TypedDict, total=False):
384
+ """Connection of ProjectV2 nodes with pagination.
385
+
386
+ Attributes:
387
+ totalCount: Total number of projects
388
+ pageInfo: Pagination information
389
+ nodes: List of project nodes
390
+ """
391
+
392
+ totalCount: int
393
+ pageInfo: ProjectV2PageInfo
394
+ nodes: list[ProjectV2Node]
395
+
396
+
397
+ class ProjectListResponse(TypedDict, total=False):
398
+ """Response from LIST_PROJECTS_QUERY.
399
+
400
+ Attributes:
401
+ organization: Organization containing projectsV2 connection
402
+ """
403
+
404
+ organization: dict[str, ProjectV2Connection]
405
+
406
+
407
+ class ProjectItemContent(TypedDict, total=False):
408
+ """Content of a project item (Issue, PR, or DraftIssue).
409
+
410
+ Attributes:
411
+ __typename: Content type discriminator
412
+ id: Content node ID
413
+ number: Issue/PR number (not present for DraftIssue)
414
+ title: Content title
415
+ state: Content state (OPEN/CLOSED for issues, etc.)
416
+ labels: Labels connection (issues only)
417
+ """
418
+
419
+ __typename: str
420
+ id: str
421
+ number: int | None
422
+ title: str
423
+ state: str | None
424
+ labels: dict[str, list[dict[str, str]]] | None
425
+
426
+
427
+ class ProjectItemNode(TypedDict, total=False):
428
+ """Single project item node.
429
+
430
+ Attributes:
431
+ id: Project item ID (not the same as content ID)
432
+ content: The actual content (Issue, PR, or DraftIssue)
433
+ """
434
+
435
+ id: str
436
+ content: ProjectItemContent
437
+
438
+
439
+ class ProjectItemsConnection(TypedDict, total=False):
440
+ """Connection of project items with pagination.
441
+
442
+ Attributes:
443
+ totalCount: Total items in project
444
+ pageInfo: Pagination info
445
+ nodes: List of project item nodes
446
+ """
447
+
448
+ totalCount: int
449
+ pageInfo: ProjectV2PageInfo
450
+ nodes: list[ProjectItemNode]
451
+
452
+
453
+ class ProjectItemsResponse(TypedDict, total=False):
454
+ """Response from PROJECT_ITEMS_QUERY.
455
+
456
+ Attributes:
457
+ node: ProjectV2 node containing items connection
458
+ """
459
+
460
+ node: dict[str, ProjectItemsConnection]
@@ -73,6 +73,9 @@ class HybridAdapter(BaseAdapter):
73
73
 
74
74
  def _get_state_mapping(self) -> dict[TicketState, str]:
75
75
  """Get state mapping from primary adapter."""
76
+ # Type narrowing: primary_adapter_name is validated in __init__
77
+ if self.primary_adapter_name is None:
78
+ raise ValueError("Primary adapter name is not set")
76
79
  primary = self.adapters[self.primary_adapter_name]
77
80
  return primary._get_state_mapping()
78
81
 
@@ -163,8 +166,11 @@ class HybridAdapter(BaseAdapter):
163
166
  Created ticket with universal ID
164
167
 
165
168
  """
169
+ if self.primary_adapter_name is None:
170
+ raise ValueError("Primary adapter name is not set")
171
+
166
172
  universal_id = self._generate_universal_id()
167
- results = []
173
+ results: list[tuple[str, Task | Epic]] = []
168
174
 
169
175
  # Create in primary adapter first
170
176
  primary = self.adapters[self.primary_adapter_name]
@@ -236,6 +242,9 @@ class HybridAdapter(BaseAdapter):
236
242
  Ticket if found, None otherwise
237
243
 
238
244
  """
245
+ if self.primary_adapter_name is None:
246
+ raise ValueError("Primary adapter name is not set")
247
+
239
248
  # Check if this is a universal ID
240
249
  if ticket_id.startswith("hybrid-"):
241
250
  # Get primary adapter ticket ID
@@ -266,7 +275,10 @@ class HybridAdapter(BaseAdapter):
266
275
  Updated ticket from primary adapter
267
276
 
268
277
  """
269
- universal_id = ticket_id
278
+ if self.primary_adapter_name is None:
279
+ raise ValueError("Primary adapter name is not set")
280
+
281
+ universal_id: str | None = ticket_id
270
282
  if not ticket_id.startswith("hybrid-"):
271
283
  # Try to find universal ID by searching mapping
272
284
  universal_id = self._find_universal_id(ticket_id)
@@ -279,6 +291,9 @@ class HybridAdapter(BaseAdapter):
279
291
  # Update in all adapters
280
292
  results = []
281
293
  for adapter_name, adapter in self.adapters.items():
294
+ if universal_id is None:
295
+ logger.warning(f"No universal ID available for ticket: {ticket_id}")
296
+ continue
282
297
  adapter_ticket_id = self._get_adapter_ticket_id(universal_id, adapter_name)
283
298
  if not adapter_ticket_id:
284
299
  logger.warning(f"No ticket ID for adapter {adapter_name}")
@@ -325,7 +340,10 @@ class HybridAdapter(BaseAdapter):
325
340
  True if deleted from at least one adapter
326
341
 
327
342
  """
328
- universal_id = ticket_id
343
+ if self.primary_adapter_name is None:
344
+ raise ValueError("Primary adapter name is not set")
345
+
346
+ universal_id: str | None = ticket_id
329
347
  if not ticket_id.startswith("hybrid-"):
330
348
  universal_id = self._find_universal_id(ticket_id)
331
349
  if not universal_id:
@@ -336,6 +354,9 @@ class HybridAdapter(BaseAdapter):
336
354
  # Delete from all adapters
337
355
  success_count = 0
338
356
  for adapter_name, adapter in self.adapters.items():
357
+ if universal_id is None:
358
+ logger.warning(f"No universal ID available for ticket: {ticket_id}")
359
+ continue
339
360
  adapter_ticket_id = self._get_adapter_ticket_id(universal_id, adapter_name)
340
361
  if not adapter_ticket_id:
341
362
  continue
@@ -372,6 +393,8 @@ class HybridAdapter(BaseAdapter):
372
393
  List of tickets from primary adapter
373
394
 
374
395
  """
396
+ if self.primary_adapter_name is None:
397
+ raise ValueError("Primary adapter name is not set")
375
398
  primary = self.adapters[self.primary_adapter_name]
376
399
  return await primary.list(limit, offset, filters)
377
400
 
@@ -385,6 +408,8 @@ class HybridAdapter(BaseAdapter):
385
408
  List of tickets matching search criteria
386
409
 
387
410
  """
411
+ if self.primary_adapter_name is None:
412
+ raise ValueError("Primary adapter name is not set")
388
413
  primary = self.adapters[self.primary_adapter_name]
389
414
  return await primary.search(query)
390
415
 
@@ -401,7 +426,10 @@ class HybridAdapter(BaseAdapter):
401
426
  Updated ticket from primary adapter
402
427
 
403
428
  """
404
- universal_id = ticket_id
429
+ if self.primary_adapter_name is None:
430
+ raise ValueError("Primary adapter name is not set")
431
+
432
+ universal_id: str | None = ticket_id
405
433
  if not ticket_id.startswith("hybrid-"):
406
434
  universal_id = self._find_universal_id(ticket_id)
407
435
  if not universal_id:
@@ -412,6 +440,9 @@ class HybridAdapter(BaseAdapter):
412
440
  # Transition in all adapters
413
441
  results = []
414
442
  for adapter_name, adapter in self.adapters.items():
443
+ if universal_id is None:
444
+ logger.warning(f"No universal ID available for ticket: {ticket_id}")
445
+ continue
415
446
  adapter_ticket_id = self._get_adapter_ticket_id(universal_id, adapter_name)
416
447
  if not adapter_ticket_id:
417
448
  continue
@@ -446,7 +477,10 @@ class HybridAdapter(BaseAdapter):
446
477
  Created comment from primary adapter
447
478
 
448
479
  """
449
- universal_id = comment.ticket_id
480
+ if self.primary_adapter_name is None:
481
+ raise ValueError("Primary adapter name is not set")
482
+
483
+ universal_id: str | None = comment.ticket_id
450
484
  if not comment.ticket_id.startswith("hybrid-"):
451
485
  universal_id = self._find_universal_id(comment.ticket_id)
452
486
  if not universal_id:
@@ -457,6 +491,11 @@ class HybridAdapter(BaseAdapter):
457
491
  # Add comment to all adapters
458
492
  results = []
459
493
  for adapter_name, adapter in self.adapters.items():
494
+ if universal_id is None:
495
+ logger.warning(
496
+ f"No universal ID available for ticket: {comment.ticket_id}"
497
+ )
498
+ continue
460
499
  adapter_ticket_id = self._get_adapter_ticket_id(universal_id, adapter_name)
461
500
  if not adapter_ticket_id:
462
501
  continue
@@ -501,6 +540,9 @@ class HybridAdapter(BaseAdapter):
501
540
  List of comments from primary adapter
502
541
 
503
542
  """
543
+ if self.primary_adapter_name is None:
544
+ raise ValueError("Primary adapter name is not set")
545
+
504
546
  if ticket_id.startswith("hybrid-"):
505
547
  # Get primary adapter ticket ID
506
548
  primary_id = self._get_adapter_ticket_id(
@@ -0,0 +1,35 @@
1
+ """JIRA adapter for universal ticket management.
2
+
3
+ This module provides a unified interface to JIRA REST API v3, supporting both
4
+ JIRA Cloud and JIRA Server/Data Center.
5
+
6
+ Public API:
7
+ -----------
8
+ JiraAdapter: Main adapter class for JIRA operations
9
+ JiraIssueType: Enum of common JIRA issue types
10
+ JiraPriority: Enum of standard JIRA priority levels
11
+
12
+ Usage:
13
+ ------
14
+ from mcp_ticketer.adapters.jira import JiraAdapter
15
+
16
+ config = {
17
+ "server": "https://company.atlassian.net",
18
+ "email": "user@example.com",
19
+ "api_token": "your-token",
20
+ "project_key": "PROJ",
21
+ }
22
+
23
+ adapter = JiraAdapter(config)
24
+ tickets = await adapter.list(limit=10)
25
+
26
+ """
27
+
28
+ from .adapter import JiraAdapter
29
+ from .types import JiraIssueType, JiraPriority
30
+
31
+ __all__ = [
32
+ "JiraAdapter",
33
+ "JiraIssueType",
34
+ "JiraPriority",
35
+ ]