mcp-ticketer 0.3.1__py3-none-any.whl → 0.3.2__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 (37) hide show
  1. mcp_ticketer/__version__.py +1 -1
  2. mcp_ticketer/adapters/aitrackdown.py +12 -15
  3. mcp_ticketer/adapters/github.py +7 -4
  4. mcp_ticketer/adapters/jira.py +23 -22
  5. mcp_ticketer/adapters/linear/__init__.py +1 -1
  6. mcp_ticketer/adapters/linear/adapter.py +88 -89
  7. mcp_ticketer/adapters/linear/client.py +71 -52
  8. mcp_ticketer/adapters/linear/mappers.py +88 -68
  9. mcp_ticketer/adapters/linear/queries.py +28 -7
  10. mcp_ticketer/adapters/linear/types.py +57 -50
  11. mcp_ticketer/adapters/linear.py +2 -2
  12. mcp_ticketer/cli/adapter_diagnostics.py +86 -51
  13. mcp_ticketer/cli/diagnostics.py +165 -72
  14. mcp_ticketer/cli/linear_commands.py +156 -113
  15. mcp_ticketer/cli/main.py +153 -82
  16. mcp_ticketer/cli/simple_health.py +73 -45
  17. mcp_ticketer/cli/utils.py +15 -10
  18. mcp_ticketer/core/config.py +23 -19
  19. mcp_ticketer/core/env_discovery.py +5 -4
  20. mcp_ticketer/core/env_loader.py +109 -86
  21. mcp_ticketer/core/exceptions.py +20 -18
  22. mcp_ticketer/core/models.py +9 -0
  23. mcp_ticketer/core/project_config.py +1 -1
  24. mcp_ticketer/mcp/server.py +294 -139
  25. mcp_ticketer/queue/health_monitor.py +152 -121
  26. mcp_ticketer/queue/manager.py +11 -4
  27. mcp_ticketer/queue/queue.py +15 -3
  28. mcp_ticketer/queue/run_worker.py +1 -1
  29. mcp_ticketer/queue/ticket_registry.py +190 -132
  30. mcp_ticketer/queue/worker.py +54 -25
  31. {mcp_ticketer-0.3.1.dist-info → mcp_ticketer-0.3.2.dist-info}/METADATA +1 -1
  32. mcp_ticketer-0.3.2.dist-info/RECORD +59 -0
  33. mcp_ticketer-0.3.1.dist-info/RECORD +0 -59
  34. {mcp_ticketer-0.3.1.dist-info → mcp_ticketer-0.3.2.dist-info}/WHEEL +0 -0
  35. {mcp_ticketer-0.3.1.dist-info → mcp_ticketer-0.3.2.dist-info}/entry_points.txt +0 -0
  36. {mcp_ticketer-0.3.1.dist-info → mcp_ticketer-0.3.2.dist-info}/licenses/LICENSE +0 -0
  37. {mcp_ticketer-0.3.1.dist-info → mcp_ticketer-0.3.2.dist-info}/top_level.txt +0 -0
@@ -4,10 +4,10 @@ import os
4
4
  from typing import Optional
5
5
 
6
6
  import typer
7
+ from gql import Client, gql
8
+ from gql.transport.httpx import HTTPXTransport
7
9
  from rich.console import Console
8
10
  from rich.table import Table
9
- from gql import gql, Client
10
- from gql.transport.httpx import HTTPXTransport
11
11
 
12
12
  app = typer.Typer(name="linear", help="Linear workspace and team management")
13
13
  console = Console()
@@ -20,10 +20,9 @@ def _create_linear_client() -> Client:
20
20
  console.print("[red]❌ LINEAR_API_KEY not found in environment[/red]")
21
21
  console.print("Set it in .env.local or environment variables")
22
22
  raise typer.Exit(1)
23
-
23
+
24
24
  transport = HTTPXTransport(
25
- url="https://api.linear.app/graphql",
26
- headers={"Authorization": api_key}
25
+ url="https://api.linear.app/graphql", headers={"Authorization": api_key}
27
26
  )
28
27
  return Client(transport=transport, fetch_schema_from_transport=False)
29
28
 
@@ -32,9 +31,10 @@ def _create_linear_client() -> Client:
32
31
  def list_workspaces():
33
32
  """List all accessible Linear workspaces."""
34
33
  console.print("🔍 Discovering Linear workspaces...")
35
-
34
+
36
35
  # Query for current organization and user info
37
- query = gql("""
36
+ query = gql(
37
+ """
38
38
  query GetWorkspaceInfo {
39
39
  viewer {
40
40
  id
@@ -48,26 +48,31 @@ def list_workspaces():
48
48
  }
49
49
  }
50
50
  }
51
- """)
52
-
51
+ """
52
+ )
53
+
53
54
  try:
54
55
  client = _create_linear_client()
55
56
  result = client.execute(query)
56
-
57
- viewer = result.get('viewer', {})
58
- organization = viewer.get('organization', {})
59
-
57
+
58
+ viewer = result.get("viewer", {})
59
+ organization = viewer.get("organization", {})
60
+
60
61
  console.print(f"\n👤 User: {viewer.get('name')} ({viewer.get('email')})")
61
- console.print(f"🏢 Current Workspace:")
62
+ console.print("🏢 Current Workspace:")
62
63
  console.print(f" Name: {organization.get('name')}")
63
64
  console.print(f" URL Key: {organization.get('urlKey')}")
64
65
  console.print(f" ID: {organization.get('id')}")
65
- if organization.get('createdAt'):
66
+ if organization.get("createdAt"):
66
67
  console.print(f" Created: {organization.get('createdAt')}")
67
-
68
- console.print(f"\n✅ API key has access to: {organization.get('name')} workspace")
69
- console.print(f"🌐 Workspace URL: https://linear.app/{organization.get('urlKey')}")
70
-
68
+
69
+ console.print(
70
+ f"\n✅ API key has access to: {organization.get('name')} workspace"
71
+ )
72
+ console.print(
73
+ f"🌐 Workspace URL: https://linear.app/{organization.get('urlKey')}"
74
+ )
75
+
71
76
  except Exception as e:
72
77
  console.print(f"[red]❌ Error fetching workspace info: {e}[/red]")
73
78
  raise typer.Exit(1)
@@ -75,17 +80,22 @@ def list_workspaces():
75
80
 
76
81
  @app.command("teams")
77
82
  def list_teams(
78
- workspace: Optional[str] = typer.Option(None, "--workspace", "-w", help="Workspace URL key (optional)"),
79
- all_teams: bool = typer.Option(False, "--all", "-a", help="Show all teams across all workspaces")
83
+ workspace: Optional[str] = typer.Option(
84
+ None, "--workspace", "-w", help="Workspace URL key (optional)"
85
+ ),
86
+ all_teams: bool = typer.Option(
87
+ False, "--all", "-a", help="Show all teams across all workspaces"
88
+ ),
80
89
  ):
81
90
  """List all teams in the current workspace or all accessible teams."""
82
91
  if all_teams:
83
92
  console.print("🔍 Discovering ALL accessible Linear teams across workspaces...")
84
93
  else:
85
94
  console.print("🔍 Discovering Linear teams...")
86
-
95
+
87
96
  # Query for all teams with pagination
88
- query = gql("""
97
+ query = gql(
98
+ """
89
99
  query GetTeams($first: Int, $after: String) {
90
100
  viewer {
91
101
  organization {
@@ -128,58 +138,70 @@ def list_teams(
128
138
  }
129
139
  }
130
140
  }
131
- """)
132
-
141
+ """
142
+ )
143
+
133
144
  try:
134
145
  client = _create_linear_client()
135
-
146
+
136
147
  # Fetch all teams with pagination
137
148
  all_teams = []
138
149
  has_next_page = True
139
150
  after_cursor = None
140
151
  current_workspace = None
141
-
152
+
142
153
  while has_next_page:
143
154
  variables = {"first": 50}
144
155
  if after_cursor:
145
156
  variables["after"] = after_cursor
146
-
157
+
147
158
  result = client.execute(query, variable_values=variables)
148
-
159
+
149
160
  # Get workspace info from first page
150
161
  if current_workspace is None:
151
- viewer = result.get('viewer', {})
152
- current_workspace = viewer.get('organization', {})
153
-
154
- teams_data = result.get('teams', {})
155
- page_teams = teams_data.get('nodes', [])
156
- page_info = teams_data.get('pageInfo', {})
157
-
162
+ viewer = result.get("viewer", {})
163
+ current_workspace = viewer.get("organization", {})
164
+
165
+ teams_data = result.get("teams", {})
166
+ page_teams = teams_data.get("nodes", [])
167
+ page_info = teams_data.get("pageInfo", {})
168
+
158
169
  all_teams.extend(page_teams)
159
- has_next_page = page_info.get('hasNextPage', False)
160
- after_cursor = page_info.get('endCursor')
161
-
170
+ has_next_page = page_info.get("hasNextPage", False)
171
+ after_cursor = page_info.get("endCursor")
172
+
162
173
  # Display workspace info
163
- console.print(f"\n🏢 Workspace: {current_workspace.get('name')} ({current_workspace.get('urlKey')})")
164
-
174
+ console.print(
175
+ f"\n🏢 Workspace: {current_workspace.get('name')} ({current_workspace.get('urlKey')})"
176
+ )
177
+
165
178
  # Filter teams by workspace if specified
166
179
  if workspace:
167
- filtered_teams = [team for team in all_teams
168
- if team.get('organization', {}).get('urlKey') == workspace]
180
+ filtered_teams = [
181
+ team
182
+ for team in all_teams
183
+ if team.get("organization", {}).get("urlKey") == workspace
184
+ ]
169
185
  if not filtered_teams:
170
- console.print(f"[yellow]No teams found in workspace '{workspace}'[/yellow]")
186
+ console.print(
187
+ f"[yellow]No teams found in workspace '{workspace}'[/yellow]"
188
+ )
171
189
  return
172
190
  all_teams = filtered_teams
173
191
  elif not all_teams and current_workspace:
174
192
  # If not showing all teams, filter to current workspace only
175
- filtered_teams = [team for team in all_teams
176
- if team.get('organization', {}).get('urlKey') == current_workspace.get('urlKey')]
193
+ filtered_teams = [
194
+ team
195
+ for team in all_teams
196
+ if team.get("organization", {}).get("urlKey")
197
+ == current_workspace.get("urlKey")
198
+ ]
177
199
  all_teams = filtered_teams
178
-
200
+
179
201
  if not all_teams:
180
202
  console.print("[yellow]No teams found[/yellow]")
181
203
  return
182
-
204
+
183
205
  # Create table
184
206
  title_suffix = " (all workspaces)" if all_teams else ""
185
207
  table = Table(title=f"Linear Teams ({len(all_teams)} found){title_suffix}")
@@ -191,36 +213,36 @@ def list_teams(
191
213
  table.add_column("Issues", justify="center")
192
214
  table.add_column("Projects", justify="center")
193
215
  table.add_column("Private", justify="center")
194
-
216
+
195
217
  for team in all_teams:
196
- member_count = len(team.get('members', {}).get('nodes', []))
197
- issue_count = len(team.get('issues', {}).get('nodes', []))
198
- project_count = len(team.get('projects', {}).get('nodes', []))
199
- is_private = "🔒" if team.get('private') else "🌐"
200
- workspace_key = team.get('organization', {}).get('urlKey', '')
218
+ member_count = len(team.get("members", {}).get("nodes", []))
219
+ issue_count = len(team.get("issues", {}).get("nodes", []))
220
+ project_count = len(team.get("projects", {}).get("nodes", []))
221
+ is_private = "🔒" if team.get("private") else "🌐"
222
+ workspace_key = team.get("organization", {}).get("urlKey", "")
201
223
 
202
224
  table.add_row(
203
- team.get('key', ''),
204
- team.get('name', ''),
225
+ team.get("key", ""),
226
+ team.get("name", ""),
205
227
  workspace_key,
206
- team.get('id', ''),
228
+ team.get("id", ""),
207
229
  str(member_count),
208
230
  str(issue_count),
209
231
  str(project_count),
210
- is_private
232
+ is_private,
211
233
  )
212
-
234
+
213
235
  console.print(table)
214
-
236
+
215
237
  # Show configuration suggestions
216
238
  if all_teams:
217
- console.print(f"\n💡 Configuration suggestions:")
239
+ console.print("\n💡 Configuration suggestions:")
218
240
  for team in all_teams[:3]: # Show first 3 teams
219
241
  console.print(f" Team '{team.get('name')}':")
220
242
  console.print(f" team_key: '{team.get('key')}'")
221
243
  console.print(f" team_id: '{team.get('id')}'")
222
244
  console.print()
223
-
245
+
224
246
  except Exception as e:
225
247
  console.print(f"[red]❌ Error fetching teams: {e}[/red]")
226
248
  raise typer.Exit(1)
@@ -228,9 +250,13 @@ def list_teams(
228
250
 
229
251
  @app.command("configure")
230
252
  def configure_team(
231
- team_key: Optional[str] = typer.Option(None, "--team-key", "-k", help="Team key (e.g., '1M')"),
253
+ team_key: Optional[str] = typer.Option(
254
+ None, "--team-key", "-k", help="Team key (e.g., '1M')"
255
+ ),
232
256
  team_id: Optional[str] = typer.Option(None, "--team-id", "-i", help="Team UUID"),
233
- workspace: Optional[str] = typer.Option(None, "--workspace", "-w", help="Workspace URL key"),
257
+ workspace: Optional[str] = typer.Option(
258
+ None, "--workspace", "-w", help="Workspace URL key"
259
+ ),
234
260
  ):
235
261
  """Configure Linear adapter with a specific team."""
236
262
  from ..cli.main import load_config, save_config
@@ -244,7 +270,8 @@ def configure_team(
244
270
  # Validate team exists
245
271
  if team_id:
246
272
  # Validate team by ID
247
- query = gql("""
273
+ query = gql(
274
+ """
248
275
  query GetTeamById($id: String!) {
249
276
  team(id: $id) {
250
277
  id
@@ -256,12 +283,13 @@ def configure_team(
256
283
  }
257
284
  }
258
285
  }
259
- """)
286
+ """
287
+ )
260
288
 
261
289
  try:
262
290
  client = _create_linear_client()
263
291
  result = client.execute(query, variable_values={"id": team_id})
264
- team = result.get('team')
292
+ team = result.get("team")
265
293
 
266
294
  if not team:
267
295
  console.print(f"[red]❌ Team with ID '{team_id}' not found[/red]")
@@ -273,7 +301,8 @@ def configure_team(
273
301
 
274
302
  elif team_key:
275
303
  # Validate team by key
276
- query = gql("""
304
+ query = gql(
305
+ """
277
306
  query GetTeamByKey($key: String!) {
278
307
  teams(filter: { key: { eq: $key } }) {
279
308
  nodes {
@@ -287,19 +316,20 @@ def configure_team(
287
316
  }
288
317
  }
289
318
  }
290
- """)
319
+ """
320
+ )
291
321
 
292
322
  try:
293
323
  client = _create_linear_client()
294
324
  result = client.execute(query, variable_values={"key": team_key})
295
- teams = result.get('teams', {}).get('nodes', [])
325
+ teams = result.get("teams", {}).get("nodes", [])
296
326
 
297
327
  if not teams:
298
328
  console.print(f"[red]❌ Team with key '{team_key}' not found[/red]")
299
329
  raise typer.Exit(1)
300
330
 
301
331
  team = teams[0]
302
- team_id = team['id'] # Use the found team ID
332
+ team_id = team["id"] # Use the found team ID
303
333
 
304
334
  except Exception as e:
305
335
  console.print(f"[red]❌ Error validating team: {e}[/red]")
@@ -313,10 +343,7 @@ def configure_team(
313
343
  config["adapters"] = {}
314
344
 
315
345
  # Update Linear adapter configuration
316
- linear_config = {
317
- "type": "linear",
318
- "team_id": team_id
319
- }
346
+ linear_config = {"type": "linear", "team_id": team_id}
320
347
 
321
348
  if team_key:
322
349
  linear_config["team_key"] = team_key
@@ -327,23 +354,27 @@ def configure_team(
327
354
 
328
355
  # Save configuration
329
356
  save_config(config)
330
-
331
- console.print(f"✅ Linear adapter configured successfully!")
357
+
358
+ console.print("✅ Linear adapter configured successfully!")
332
359
  console.print(f" Team: {team.get('name')} ({team.get('key')})")
333
360
  console.print(f" Team ID: {team_id}")
334
361
  console.print(f" Workspace: {team.get('organization', {}).get('name')}")
335
-
362
+
336
363
  # Test the configuration
337
364
  console.print("\n🧪 Testing configuration...")
338
365
  try:
339
366
  from ..adapters.linear import LinearAdapter
367
+
340
368
  adapter = LinearAdapter(linear_config)
341
-
369
+
342
370
  # Test by listing a few tickets
343
371
  import asyncio
372
+
344
373
  tickets = asyncio.run(adapter.list(limit=1))
345
- console.print(f"✅ Configuration test successful! Found {len(tickets)} ticket(s)")
346
-
374
+ console.print(
375
+ f"✅ Configuration test successful! Found {len(tickets)} ticket(s)"
376
+ )
377
+
347
378
  except Exception as e:
348
379
  console.print(f"[yellow]⚠️ Configuration saved but test failed: {e}[/yellow]")
349
380
  console.print("You may need to check your API key or team permissions")
@@ -351,17 +382,22 @@ def configure_team(
351
382
 
352
383
  @app.command("info")
353
384
  def show_info(
354
- team_key: Optional[str] = typer.Option(None, "--team-key", "-k", help="Team key to show info for"),
355
- team_id: Optional[str] = typer.Option(None, "--team-id", "-i", help="Team UUID to show info for"),
385
+ team_key: Optional[str] = typer.Option(
386
+ None, "--team-key", "-k", help="Team key to show info for"
387
+ ),
388
+ team_id: Optional[str] = typer.Option(
389
+ None, "--team-id", "-i", help="Team UUID to show info for"
390
+ ),
356
391
  ):
357
392
  """Show detailed information about a specific team."""
358
393
  if not team_key and not team_id:
359
394
  console.print("[red]❌ Either --team-key or --team-id is required[/red]")
360
395
  raise typer.Exit(1)
361
-
396
+
362
397
  # Query for detailed team information
363
398
  if team_id:
364
- query = gql("""
399
+ query = gql(
400
+ """
365
401
  query GetTeamInfo($id: String!) {
366
402
  team(id: $id) {
367
403
  id
@@ -392,10 +428,12 @@ def show_info(
392
428
  }
393
429
  }
394
430
  }
395
- """)
431
+ """
432
+ )
396
433
  variables = {"id": team_id}
397
434
  else:
398
- query = gql("""
435
+ query = gql(
436
+ """
399
437
  query GetTeamInfoByKey($key: String!) {
400
438
  teams(filter: { key: { eq: $key } }) {
401
439
  nodes {
@@ -428,63 +466,68 @@ def show_info(
428
466
  }
429
467
  }
430
468
  }
431
- """)
469
+ """
470
+ )
432
471
  variables = {"key": team_key}
433
-
472
+
434
473
  try:
435
474
  client = _create_linear_client()
436
475
  result = client.execute(query, variable_values=variables)
437
-
476
+
438
477
  if team_id:
439
- team = result.get('team')
478
+ team = result.get("team")
440
479
  else:
441
- teams = result.get('teams', {}).get('nodes', [])
480
+ teams = result.get("teams", {}).get("nodes", [])
442
481
  team = teams[0] if teams else None
443
-
482
+
444
483
  if not team:
445
484
  identifier = team_id or team_key
446
485
  console.print(f"[red]❌ Team '{identifier}' not found[/red]")
447
486
  raise typer.Exit(1)
448
-
487
+
449
488
  # Display team information
450
489
  console.print(f"\n🏷️ Team: {team.get('name')}")
451
490
  console.print(f" Key: {team.get('key')}")
452
491
  console.print(f" ID: {team.get('id')}")
453
- console.print(f" Workspace: {team.get('organization', {}).get('name')} ({team.get('organization', {}).get('urlKey')})")
454
- console.print(f" Privacy: {'🔒 Private' if team.get('private') else '🌐 Public'}")
455
-
456
- if team.get('description'):
492
+ console.print(
493
+ f" Workspace: {team.get('organization', {}).get('name')} ({team.get('organization', {}).get('urlKey')})"
494
+ )
495
+ console.print(
496
+ f" Privacy: {'🔒 Private' if team.get('private') else '🌐 Public'}"
497
+ )
498
+
499
+ if team.get("description"):
457
500
  console.print(f" Description: {team.get('description')}")
458
-
501
+
459
502
  console.print(f" Created: {team.get('createdAt')}")
460
-
503
+
461
504
  # Statistics
462
- member_count = len(team.get('members', {}).get('nodes', []))
463
- state_count = len(team.get('states', {}).get('nodes', []))
464
-
465
- console.print(f"\n📊 Statistics:")
505
+ member_count = len(team.get("members", {}).get("nodes", []))
506
+ state_count = len(team.get("states", {}).get("nodes", []))
507
+
508
+ console.print("\n📊 Statistics:")
466
509
  console.print(f" Members: {member_count}")
467
510
  console.print(f" Workflow States: {state_count}")
468
-
511
+
469
512
  # Show members
470
- members = team.get('members', {}).get('nodes', [])
513
+ members = team.get("members", {}).get("nodes", [])
471
514
  if members:
472
515
  console.print(f"\n👥 Members ({len(members)}):")
473
516
  for member in members:
474
- status = "✅" if member.get('active') else "❌"
517
+ status = "✅" if member.get("active") else "❌"
475
518
  console.print(f" {status} {member.get('name')}")
476
519
  if len(members) == 10:
477
- console.print(f" ... (showing first 10 members)")
478
-
520
+ console.print(" ... (showing first 10 members)")
521
+
479
522
  # Show workflow states
480
- states = team.get('states', {}).get('nodes', [])
523
+ states = team.get("states", {}).get("nodes", [])
481
524
  if states:
482
525
  console.print(f"\n🔄 Workflow States ({len(states)}):")
483
- for state in sorted(states, key=lambda s: s.get('position', 0)):
526
+ for state in sorted(states, key=lambda s: s.get("position", 0)):
484
527
  console.print(f" {state.get('name')} ({state.get('type')})")
485
528
  if len(states) == 20:
486
- console.print(f" ... (showing first 20 states)")
487
-
529
+ console.print(" ... (showing first 20 states)")
530
+
488
531
  except Exception as e:
489
532
  console.print(f"[red]❌ Error fetching team info: {e}[/red]")
490
533
  raise typer.Exit(1)