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,344 @@
1
+ """GraphQL client management for Linear API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from typing import Any
7
+
8
+ try:
9
+ from gql import Client, gql
10
+ from gql.transport.exceptions import TransportError
11
+ from gql.transport.httpx import HTTPXAsyncTransport
12
+ except ImportError:
13
+ # Handle missing gql dependency gracefully
14
+ Client = None
15
+ gql = None
16
+ HTTPXAsyncTransport = None
17
+ TransportError = Exception
18
+
19
+ from ...core.exceptions import AdapterError, AuthenticationError, RateLimitError
20
+
21
+
22
+ class LinearGraphQLClient:
23
+ """GraphQL client for Linear API with error handling and retry logic."""
24
+
25
+ def __init__(self, api_key: str, timeout: int = 30):
26
+ """Initialize the Linear GraphQL client.
27
+
28
+ Args:
29
+ ----
30
+ api_key: Linear API key
31
+ timeout: Request timeout in seconds
32
+
33
+ """
34
+ self.api_key = api_key
35
+ self.timeout = timeout
36
+ self._base_url = "https://api.linear.app/graphql"
37
+
38
+ def create_client(self) -> Client:
39
+ """Create a new GraphQL client instance.
40
+
41
+ Returns:
42
+ -------
43
+ Configured GraphQL client
44
+
45
+ Raises:
46
+ ------
47
+ AuthenticationError: If API key is invalid
48
+ AdapterError: If client creation fails
49
+
50
+ """
51
+ if Client is None:
52
+ raise AdapterError(
53
+ "gql library not installed. Install with: pip install gql[httpx]",
54
+ "linear",
55
+ )
56
+
57
+ if not self.api_key:
58
+ raise AuthenticationError("Linear API key is required", "linear")
59
+
60
+ try:
61
+ # Create transport with authentication
62
+ # Linear API keys are passed directly (no Bearer prefix)
63
+ # Only OAuth tokens use Bearer scheme
64
+ transport = HTTPXAsyncTransport(
65
+ url=self._base_url,
66
+ headers={"Authorization": self.api_key},
67
+ timeout=self.timeout,
68
+ )
69
+
70
+ # Create client
71
+ client = Client(transport=transport, fetch_schema_from_transport=False)
72
+ return client
73
+
74
+ except Exception as e:
75
+ raise AdapterError(f"Failed to create Linear client: {e}", "linear") from e
76
+
77
+ async def execute_query(
78
+ self,
79
+ query_string: str,
80
+ variables: dict[str, Any] | None = None,
81
+ retries: int = 3,
82
+ ) -> dict[str, Any]:
83
+ """Execute a GraphQL query with error handling and retries.
84
+
85
+ Args:
86
+ ----
87
+ query_string: GraphQL query string
88
+ variables: Query variables
89
+ retries: Number of retry attempts
90
+
91
+ Returns:
92
+ -------
93
+ Query result data
94
+
95
+ Raises:
96
+ ------
97
+ AuthenticationError: If authentication fails
98
+ RateLimitError: If rate limit is exceeded
99
+ AdapterError: If query execution fails
100
+
101
+ """
102
+ query = gql(query_string)
103
+
104
+ for attempt in range(retries + 1):
105
+ try:
106
+ client = self.create_client()
107
+ async with client as session:
108
+ result = await session.execute(
109
+ query, variable_values=variables or {}
110
+ )
111
+ return result
112
+
113
+ except TransportError as e:
114
+ # Handle HTTP errors
115
+ if hasattr(e, "response") and e.response:
116
+ status_code = e.response.status
117
+
118
+ if status_code == 401:
119
+ raise AuthenticationError(
120
+ "Invalid Linear API key", "linear"
121
+ ) from e
122
+ elif status_code == 403:
123
+ raise AuthenticationError(
124
+ "Insufficient permissions", "linear"
125
+ ) from e
126
+ elif status_code == 429:
127
+ # Rate limit exceeded
128
+ retry_after = e.response.headers.get("Retry-After", "60")
129
+ raise RateLimitError(
130
+ "Linear API rate limit exceeded", "linear", retry_after
131
+ ) from e
132
+ elif status_code >= 500:
133
+ # Server error - retry
134
+ if attempt < retries:
135
+ await asyncio.sleep(2**attempt) # Exponential backoff
136
+ continue
137
+ raise AdapterError(
138
+ f"Linear API server error: {status_code}", "linear"
139
+ ) from e
140
+
141
+ # Network or other transport error
142
+ if attempt < retries:
143
+ await asyncio.sleep(2**attempt)
144
+ continue
145
+ raise AdapterError(f"Linear API transport error: {e}", "linear") from e
146
+
147
+ except Exception as e:
148
+ # GraphQL or other errors
149
+ error_msg = str(e)
150
+
151
+ # Check for specific GraphQL errors
152
+ if (
153
+ "authentication" in error_msg.lower()
154
+ or "unauthorized" in error_msg.lower()
155
+ ):
156
+ raise AuthenticationError(
157
+ f"Linear authentication failed: {error_msg}", "linear"
158
+ ) from e
159
+ elif "rate limit" in error_msg.lower():
160
+ raise RateLimitError(
161
+ "Linear API rate limit exceeded", "linear"
162
+ ) from e
163
+
164
+ # Generic error
165
+ if attempt < retries:
166
+ await asyncio.sleep(2**attempt)
167
+ continue
168
+ raise AdapterError(
169
+ f"Linear GraphQL error: {error_msg}", "linear"
170
+ ) from e
171
+
172
+ # Should never reach here
173
+ raise AdapterError("Maximum retries exceeded", "linear")
174
+
175
+ async def execute_mutation(
176
+ self,
177
+ mutation_string: str,
178
+ variables: dict[str, Any] | None = None,
179
+ retries: int = 3,
180
+ ) -> dict[str, Any]:
181
+ """Execute a GraphQL mutation with error handling.
182
+
183
+ Args:
184
+ ----
185
+ mutation_string: GraphQL mutation string
186
+ variables: Mutation variables
187
+ retries: Number of retry attempts
188
+
189
+ Returns:
190
+ -------
191
+ Mutation result data
192
+
193
+ Raises:
194
+ ------
195
+ AuthenticationError: If authentication fails
196
+ RateLimitError: If rate limit is exceeded
197
+ AdapterError: If mutation execution fails
198
+
199
+ """
200
+ return await self.execute_query(mutation_string, variables, retries)
201
+
202
+ async def test_connection(self) -> bool:
203
+ """Test the connection to Linear API.
204
+
205
+ Returns:
206
+ -------
207
+ True if connection is successful, False otherwise
208
+
209
+ """
210
+ try:
211
+ # Simple query to test authentication
212
+ test_query = """
213
+ query TestConnection {
214
+ viewer {
215
+ id
216
+ name
217
+ }
218
+ }
219
+ """
220
+
221
+ result = await self.execute_query(test_query)
222
+ return bool(result.get("viewer"))
223
+
224
+ except Exception:
225
+ return False
226
+
227
+ async def get_team_info(self, team_id: str) -> dict[str, Any] | None:
228
+ """Get team information by ID.
229
+
230
+ Args:
231
+ ----
232
+ team_id: Linear team ID
233
+
234
+ Returns:
235
+ -------
236
+ Team information or None if not found
237
+
238
+ """
239
+ try:
240
+ query = """
241
+ query GetTeam($teamId: String!) {
242
+ team(id: $teamId) {
243
+ id
244
+ name
245
+ key
246
+ description
247
+ }
248
+ }
249
+ """
250
+
251
+ result = await self.execute_query(query, {"teamId": team_id})
252
+ return result.get("team")
253
+
254
+ except Exception:
255
+ return None
256
+
257
+ async def get_user_by_email(self, email: str) -> dict[str, Any] | None:
258
+ """Get user information by email.
259
+
260
+ Args:
261
+ ----
262
+ email: User email address
263
+
264
+ Returns:
265
+ -------
266
+ User information or None if not found
267
+
268
+ """
269
+ try:
270
+ query = """
271
+ query GetUserByEmail($email: String!) {
272
+ users(filter: { email: { eq: $email } }) {
273
+ nodes {
274
+ id
275
+ name
276
+ email
277
+ displayName
278
+ avatarUrl
279
+ }
280
+ }
281
+ }
282
+ """
283
+
284
+ result = await self.execute_query(query, {"email": email})
285
+ users = result.get("users", {}).get("nodes", [])
286
+ return users[0] if users else None
287
+
288
+ except Exception:
289
+ return None
290
+
291
+ async def get_users_by_name(self, name: str) -> list[dict[str, Any]]:
292
+ """Search users by display name or full name.
293
+
294
+ Args:
295
+ ----
296
+ name: Display name or full name to search for
297
+
298
+ Returns:
299
+ -------
300
+ List of matching users (may be empty)
301
+
302
+ """
303
+ import logging
304
+
305
+ try:
306
+ query = """
307
+ query SearchUsers($nameFilter: String!) {
308
+ users(
309
+ filter: {
310
+ or: [
311
+ { displayName: { containsIgnoreCase: $nameFilter } }
312
+ { name: { containsIgnoreCase: $nameFilter } }
313
+ ]
314
+ }
315
+ first: 10
316
+ ) {
317
+ nodes {
318
+ id
319
+ name
320
+ email
321
+ displayName
322
+ avatarUrl
323
+ active
324
+ }
325
+ }
326
+ }
327
+ """
328
+
329
+ result = await self.execute_query(query, {"nameFilter": name})
330
+ users = result.get("users", {}).get("nodes", [])
331
+ return [u for u in users if u.get("active", True)] # Filter active users
332
+
333
+ except Exception as e:
334
+ logging.getLogger(__name__).warning(f"Failed to search users by name: {e}")
335
+ return []
336
+
337
+ async def close(self) -> None:
338
+ """Close the client connection.
339
+
340
+ Since we create fresh clients for each operation, there's no persistent
341
+ connection to close. Each client's transport is automatically closed when
342
+ the async context manager exits.
343
+ """
344
+ pass