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
@@ -3,6 +3,8 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
+ import json
7
+ import logging
6
8
  from typing import Any
7
9
 
8
10
  try:
@@ -19,6 +21,8 @@ except ImportError:
19
21
 
20
22
  from ...core.exceptions import AdapterError, AuthenticationError, RateLimitError
21
23
 
24
+ logger = logging.getLogger(__name__)
25
+
22
26
 
23
27
  class LinearGraphQLClient:
24
28
  """GraphQL client for Linear API with error handling and retry logic."""
@@ -102,13 +106,42 @@ class LinearGraphQLClient:
102
106
  """
103
107
  query = gql(query_string)
104
108
 
109
+ # Extract operation name from query for logging
110
+ operation_name = "unknown"
111
+ try:
112
+ # Simple extraction - look for 'query' or 'mutation' keyword
113
+ query_lower = query_string.strip().lower()
114
+ if query_lower.startswith("mutation"):
115
+ operation_name = (
116
+ query_string.split("{")[0].strip().replace("mutation", "").strip()
117
+ )
118
+ elif query_lower.startswith("query"):
119
+ operation_name = (
120
+ query_string.split("{")[0].strip().replace("query", "").strip()
121
+ )
122
+ except Exception:
123
+ pass # Use default 'unknown' if extraction fails
124
+
105
125
  for attempt in range(retries + 1):
106
126
  try:
127
+ # Log request details before execution
128
+ logger.debug(
129
+ f"[Linear GraphQL] Executing operation: {operation_name}\n"
130
+ f"Variables:\n{json.dumps(variables or {}, indent=2, default=str)}"
131
+ )
132
+
107
133
  client = self.create_client()
108
134
  async with client as session:
109
135
  result = await session.execute(
110
136
  query, variable_values=variables or {}
111
137
  )
138
+
139
+ # Log successful response
140
+ logger.debug(
141
+ f"[Linear GraphQL] Operation successful: {operation_name}\n"
142
+ f"Response:\n{json.dumps(result, indent=2, default=str)}"
143
+ )
144
+
112
145
  return result
113
146
 
114
147
  except TransportQueryError as e:
@@ -118,8 +151,46 @@ class LinearGraphQLClient:
118
151
 
119
152
  Related: 1M-398 - Label duplicate error handling
120
153
  """
154
+ # Log detailed error information
155
+ logger.error(
156
+ f"[Linear GraphQL] TransportQueryError occurred\n"
157
+ f"Operation: {operation_name}\n"
158
+ f"Variables:\n{json.dumps(variables or {}, indent=2, default=str)}\n"
159
+ f"Error: {e}\n"
160
+ f"Error details: {e.errors if hasattr(e, 'errors') else 'No error details'}"
161
+ )
162
+
121
163
  if e.errors:
122
- error_msg = e.errors[0].get("message", "Unknown GraphQL error")
164
+ error = e.errors[0]
165
+ error_msg = error.get("message", "Unknown GraphQL error")
166
+
167
+ # Parse extensions for field-specific details (enhanced debugging)
168
+ extensions = error.get("extensions", {})
169
+
170
+ # Check for user-presentable message (clearer error for users)
171
+ user_message = extensions.get("userPresentableMessage")
172
+ if user_message:
173
+ error_msg = user_message
174
+
175
+ # Check for argument path (which field failed validation)
176
+ arg_path = extensions.get("argumentPath")
177
+ if arg_path:
178
+ field_path = ".".join(str(p) for p in arg_path)
179
+ error_msg = f"{error_msg} (field: {field_path})"
180
+
181
+ # Check for validation errors (additional context)
182
+ validation_errors = extensions.get("validationErrors")
183
+ if validation_errors:
184
+ error_msg = (
185
+ f"{error_msg}\nValidation errors: {validation_errors}"
186
+ )
187
+
188
+ # Log full error context for debugging
189
+ logger.error(
190
+ "Linear GraphQL error: %s (extensions: %s)",
191
+ error_msg,
192
+ extensions,
193
+ )
123
194
 
124
195
  # Check for duplicate label errors specifically
125
196
  if (
@@ -139,6 +210,15 @@ class LinearGraphQLClient:
139
210
  raise AdapterError(f"Linear GraphQL error: {e}", "linear") from e
140
211
 
141
212
  except TransportError as e:
213
+ # Log transport error details
214
+ logger.error(
215
+ f"[Linear GraphQL] TransportError occurred\n"
216
+ f"Operation: {operation_name}\n"
217
+ f"Variables:\n{json.dumps(variables or {}, indent=2, default=str)}\n"
218
+ f"Error: {e}\n"
219
+ f"Status code: {e.response.status if hasattr(e, 'response') and e.response else 'N/A'}"
220
+ )
221
+
142
222
  # Handle HTTP errors
143
223
  if hasattr(e, "response") and e.response:
144
224
  status_code = e.response.status
@@ -173,6 +253,16 @@ class LinearGraphQLClient:
173
253
  raise AdapterError(f"Linear API transport error: {e}", "linear") from e
174
254
 
175
255
  except Exception as e:
256
+ # Log generic error details
257
+ logger.error(
258
+ f"[Linear GraphQL] Unexpected error occurred\n"
259
+ f"Operation: {operation_name}\n"
260
+ f"Variables:\n{json.dumps(variables or {}, indent=2, default=str)}\n"
261
+ f"Error type: {type(e).__name__}\n"
262
+ f"Error: {e}",
263
+ exc_info=True,
264
+ )
265
+
176
266
  # GraphQL or other errors
177
267
  error_msg = str(e)
178
268
 
@@ -418,3 +418,110 @@ def map_linear_attachment_to_attachment(
418
418
  description=subtitle,
419
419
  metadata=metadata,
420
420
  )
421
+
422
+
423
+ def task_to_compact_format(task: Task) -> dict[str, Any]:
424
+ """Convert Task to compact format for efficient token usage.
425
+
426
+ Compact format includes only essential fields, reducing token usage by ~70-80%
427
+ compared to full Task serialization.
428
+
429
+ Args:
430
+ ----
431
+ task: Universal Task model
432
+
433
+ Returns:
434
+ -------
435
+ Compact dictionary with minimal essential fields
436
+
437
+ Design Decision: Compact Format Fields (1M-554)
438
+ -----------------------------------------------
439
+ Rationale: Selected fields based on token efficiency analysis and user needs.
440
+ Full Task serialization averages ~600 tokens per item. Compact format targets
441
+ ~120 tokens per item (80% reduction).
442
+
443
+ Essential fields included:
444
+ - id: Required for all operations (identifier)
445
+ - title: Primary user-facing information
446
+ - state: Critical for workflow understanding
447
+ - priority: Important for triage and filtering
448
+ - assignee: Key for task assignment visibility
449
+
450
+ Fields excluded:
451
+ - description: Often large (100-500 tokens), available via get()
452
+ - creator: Less critical for list views
453
+ - tags: Available in full format, not essential for scanning
454
+ - children: Hierarchy details available via dedicated queries
455
+ - created_at/updated_at: Not essential for list scanning
456
+ - metadata: Platform-specific, not needed in compact view
457
+
458
+ Performance: Reduces typical 50-item list from ~30,000 to ~6,000 tokens.
459
+
460
+ """
461
+ # Handle state - can be TicketState enum or string
462
+ state_value = None
463
+ if task.state:
464
+ state_value = task.state.value if hasattr(task.state, "value") else task.state
465
+
466
+ # Handle priority - can be Priority enum or string
467
+ priority_value = None
468
+ if task.priority:
469
+ priority_value = (
470
+ task.priority.value if hasattr(task.priority, "value") else task.priority
471
+ )
472
+
473
+ return {
474
+ "id": task.id,
475
+ "title": task.title,
476
+ "state": state_value,
477
+ "priority": priority_value,
478
+ "assignee": task.assignee,
479
+ }
480
+
481
+
482
+ def epic_to_compact_format(epic: Epic) -> dict[str, Any]:
483
+ """Convert Epic to compact format for efficient token usage.
484
+
485
+ Compact format includes only essential fields, reducing token usage by ~70-80%
486
+ compared to full Epic serialization.
487
+
488
+ Args:
489
+ ----
490
+ epic: Universal Epic model
491
+
492
+ Returns:
493
+ -------
494
+ Compact dictionary with minimal essential fields
495
+
496
+ Design Decision: Epic Compact Format (1M-554)
497
+ ---------------------------------------------
498
+ Rationale: Epics typically have less metadata than tasks, but descriptions
499
+ can still be large. Compact format focuses on overview information.
500
+
501
+ Essential fields:
502
+ - id: Required for all operations
503
+ - title: Primary identifier
504
+ - state: Project status
505
+ - child_count: Useful for project overview (if available)
506
+
507
+ Performance: Similar token reduction to task compact format.
508
+
509
+ """
510
+ # Handle state - can be TicketState enum or string
511
+ state_value = None
512
+ if epic.state:
513
+ state_value = epic.state.value if hasattr(epic.state, "value") else epic.state
514
+
515
+ compact = {
516
+ "id": epic.id,
517
+ "title": epic.title,
518
+ "state": state_value,
519
+ }
520
+
521
+ # Include child count if available in metadata
522
+ if epic.metadata and "linear" in epic.metadata:
523
+ linear_meta = epic.metadata["linear"]
524
+ if "issue_count" in linear_meta:
525
+ compact["child_count"] = linear_meta["issue_count"]
526
+
527
+ return compact
@@ -354,11 +354,15 @@ LIST_PROJECTS_QUERY = (
354
354
  TEAM_FRAGMENT # Required by PROJECT_FRAGMENT which uses ...TeamFields
355
355
  + PROJECT_FRAGMENT
356
356
  + """
357
- query ListProjects($filter: ProjectFilter, $first: Int!) {
358
- projects(filter: $filter, first: $first, orderBy: updatedAt) {
357
+ query ListProjects($filter: ProjectFilter, $first: Int!, $after: String) {
358
+ projects(filter: $filter, first: $first, after: $after, orderBy: updatedAt) {
359
359
  nodes {
360
360
  ...ProjectFields
361
361
  }
362
+ pageInfo {
363
+ hasNextPage
364
+ endCursor
365
+ }
362
366
  }
363
367
  }
364
368
  """
@@ -554,3 +558,109 @@ GET_PROJECT_UPDATE_QUERY = (
554
558
  }
555
559
  """
556
560
  )
561
+
562
+ # Milestone/Cycle Operations (1M-607 Phase 2)
563
+
564
+ CREATE_CYCLE_MUTATION = """
565
+ mutation CycleCreate($input: CycleCreateInput!) {
566
+ cycleCreate(input: $input) {
567
+ success
568
+ cycle {
569
+ id
570
+ name
571
+ description
572
+ startsAt
573
+ endsAt
574
+ completedAt
575
+ progress
576
+ completedIssueCount
577
+ issueCount
578
+ team {
579
+ id
580
+ name
581
+ }
582
+ }
583
+ }
584
+ }
585
+ """
586
+
587
+ GET_CYCLE_QUERY = """
588
+ query Cycle($id: String!) {
589
+ cycle(id: $id) {
590
+ id
591
+ name
592
+ description
593
+ startsAt
594
+ endsAt
595
+ completedAt
596
+ progress
597
+ completedIssueCount
598
+ issueCount
599
+ team {
600
+ id
601
+ name
602
+ }
603
+ }
604
+ }
605
+ """
606
+
607
+ UPDATE_CYCLE_MUTATION = """
608
+ mutation CycleUpdate($id: String!, $input: CycleUpdateInput!) {
609
+ cycleUpdate(id: $id, input: $input) {
610
+ success
611
+ cycle {
612
+ id
613
+ name
614
+ description
615
+ startsAt
616
+ endsAt
617
+ completedAt
618
+ progress
619
+ completedIssueCount
620
+ issueCount
621
+ }
622
+ }
623
+ }
624
+ """
625
+
626
+ ARCHIVE_CYCLE_MUTATION = """
627
+ mutation CycleArchive($id: String!) {
628
+ cycleArchive(id: $id) {
629
+ success
630
+ }
631
+ }
632
+ """
633
+
634
+ GET_CYCLE_ISSUES_QUERY = """
635
+ query CycleIssues($cycleId: String!, $first: Int!) {
636
+ cycle(id: $cycleId) {
637
+ issues(first: $first) {
638
+ nodes {
639
+ id
640
+ identifier
641
+ title
642
+ description
643
+ state {
644
+ id
645
+ name
646
+ type
647
+ }
648
+ priority
649
+ assignee {
650
+ id
651
+ email
652
+ name
653
+ }
654
+ labels {
655
+ nodes {
656
+ id
657
+ name
658
+ }
659
+ }
660
+ createdAt
661
+ updatedAt
662
+ }
663
+ }
664
+ }
665
+ }
666
+ """
@@ -51,6 +51,34 @@ class LinearStateMapping:
51
51
  "canceled": TicketState.CLOSED,
52
52
  }
53
53
 
54
+ # Semantic state name mappings for flexible workflow matching (1M-552)
55
+ # Maps universal states to common Linear state names (case-insensitive)
56
+ SEMANTIC_NAMES: dict[TicketState, list[str]] = {
57
+ TicketState.OPEN: ["todo", "to do", "open", "new", "backlog"],
58
+ TicketState.READY: ["ready", "triage", "ready for dev", "ready to start"],
59
+ TicketState.TESTED: [
60
+ "tested",
61
+ "in review",
62
+ "review",
63
+ "qa",
64
+ "testing",
65
+ "ready for review",
66
+ ],
67
+ TicketState.WAITING: ["waiting", "on hold", "paused"],
68
+ TicketState.BLOCKED: ["blocked"],
69
+ TicketState.IN_PROGRESS: [
70
+ "in progress",
71
+ "in-progress",
72
+ "started",
73
+ "doing",
74
+ "active",
75
+ "in development",
76
+ "in dev",
77
+ ],
78
+ TicketState.DONE: ["done", "completed", "finished"],
79
+ TicketState.CLOSED: ["closed", "canceled", "cancelled", "won't do", "wont do"],
80
+ }
81
+
54
82
 
55
83
  class LinearWorkflowStateType(Enum):
56
84
  """Linear workflow state types."""
@@ -138,8 +166,9 @@ def get_universal_state(
138
166
  2. Try synonym matching on state name (ToDo, In Review, Testing, etc.)
139
167
  3. Default to OPEN for unknown states
140
168
 
141
- Synonym Matching Rules (ticket 1M-164):
142
- - "Done", "Closed", "Cancelled", "Completed", "Won't Do" CLOSED
169
+ Synonym Matching Rules (ticket 1M-164, fixed in v2.0.4):
170
+ - "done", "completed", "finished", "resolved" → DONE
171
+ - "closed", "canceled", "cancelled", "won't do" → CLOSED
143
172
  - Everything else → OPEN
144
173
 
145
174
  Args:
@@ -158,24 +187,35 @@ def get_universal_state(
158
187
  if state_name:
159
188
  state_name_lower = state_name.lower().strip()
160
189
 
161
- # Check for "done/closed" synonyms - these become CLOSED
162
- closed_synonyms = [
190
+ # DONE states: Work successfully completed
191
+ # - User finished the work
192
+ # - Requirements met
193
+ # - Quality verified
194
+ done_synonyms = [
163
195
  "done",
196
+ "completed",
197
+ "finished",
198
+ "resolved",
199
+ ]
200
+
201
+ if any(synonym in state_name_lower for synonym in done_synonyms):
202
+ return TicketState.DONE
203
+
204
+ # CLOSED states: Work terminated without completion
205
+ # - User decided not to do it
206
+ # - Requirements changed
207
+ # - Duplicate/invalid ticket
208
+ closed_synonyms = [
164
209
  "closed",
165
210
  "cancelled",
166
211
  "canceled",
167
- "completed",
168
212
  "won't do",
169
213
  "wont do",
170
214
  "rejected",
171
- "resolved",
172
- "finished",
173
215
  ]
174
216
 
175
217
  if any(synonym in state_name_lower for synonym in closed_synonyms):
176
- return (
177
- TicketState.DONE if state_name_lower == "done" else TicketState.CLOSED
178
- )
218
+ return TicketState.CLOSED
179
219
 
180
220
  # Check for "in progress" synonyms
181
221
  in_progress_synonyms = [