mcp-ticketer 0.1.39__py3-none-any.whl → 0.3.0__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.
- mcp_ticketer/__version__.py +1 -1
- mcp_ticketer/adapters/linear/__init__.py +24 -0
- mcp_ticketer/adapters/linear/adapter.py +813 -0
- mcp_ticketer/adapters/linear/client.py +257 -0
- mcp_ticketer/adapters/linear/mappers.py +307 -0
- mcp_ticketer/adapters/linear/queries.py +366 -0
- mcp_ticketer/adapters/linear/types.py +277 -0
- mcp_ticketer/adapters/linear.py +10 -2384
- mcp_ticketer/cli/adapter_diagnostics.py +384 -0
- mcp_ticketer/cli/main.py +327 -67
- mcp_ticketer/core/exceptions.py +152 -0
- mcp_ticketer/mcp/server.py +172 -37
- {mcp_ticketer-0.1.39.dist-info → mcp_ticketer-0.3.0.dist-info}/METADATA +1 -1
- {mcp_ticketer-0.1.39.dist-info → mcp_ticketer-0.3.0.dist-info}/RECORD +18 -10
- {mcp_ticketer-0.1.39.dist-info → mcp_ticketer-0.3.0.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.1.39.dist-info → mcp_ticketer-0.3.0.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.1.39.dist-info → mcp_ticketer-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.1.39.dist-info → mcp_ticketer-0.3.0.dist-info}/top_level.txt +0 -0
mcp_ticketer/cli/main.py
CHANGED
|
@@ -317,13 +317,161 @@ def get_adapter(
|
|
|
317
317
|
return AdapterRegistry.get_adapter(adapter_type, adapter_config)
|
|
318
318
|
|
|
319
319
|
|
|
320
|
+
def _prompt_for_adapter_selection(console: Console) -> str:
|
|
321
|
+
"""Interactive prompt for adapter selection.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
console: Rich console for output
|
|
325
|
+
|
|
326
|
+
Returns:
|
|
327
|
+
Selected adapter type
|
|
328
|
+
"""
|
|
329
|
+
console.print("\n[bold blue]🚀 MCP Ticketer Setup[/bold blue]")
|
|
330
|
+
console.print("Choose which ticket system you want to connect to:\n")
|
|
331
|
+
|
|
332
|
+
# Define adapter options with descriptions
|
|
333
|
+
adapters = [
|
|
334
|
+
{
|
|
335
|
+
"name": "linear",
|
|
336
|
+
"title": "Linear",
|
|
337
|
+
"description": "Modern project management (linear.app)",
|
|
338
|
+
"requirements": "API key and team ID"
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
"name": "github",
|
|
342
|
+
"title": "GitHub Issues",
|
|
343
|
+
"description": "GitHub repository issues",
|
|
344
|
+
"requirements": "Personal access token, owner, and repo"
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
"name": "jira",
|
|
348
|
+
"title": "JIRA",
|
|
349
|
+
"description": "Atlassian JIRA project management",
|
|
350
|
+
"requirements": "Server URL, email, and API token"
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
"name": "aitrackdown",
|
|
354
|
+
"title": "Local Files (AITrackdown)",
|
|
355
|
+
"description": "Store tickets in local files (no external service)",
|
|
356
|
+
"requirements": "None - works offline"
|
|
357
|
+
}
|
|
358
|
+
]
|
|
359
|
+
|
|
360
|
+
# Display options
|
|
361
|
+
for i, adapter in enumerate(adapters, 1):
|
|
362
|
+
console.print(f"[cyan]{i}.[/cyan] [bold]{adapter['title']}[/bold]")
|
|
363
|
+
console.print(f" {adapter['description']}")
|
|
364
|
+
console.print(f" [dim]Requirements: {adapter['requirements']}[/dim]\n")
|
|
365
|
+
|
|
366
|
+
# Get user selection
|
|
367
|
+
while True:
|
|
368
|
+
try:
|
|
369
|
+
choice = typer.prompt(
|
|
370
|
+
"Select adapter (1-4)",
|
|
371
|
+
type=int,
|
|
372
|
+
default=1
|
|
373
|
+
)
|
|
374
|
+
if 1 <= choice <= len(adapters):
|
|
375
|
+
selected_adapter = adapters[choice - 1]
|
|
376
|
+
console.print(f"\n[green]✓ Selected: {selected_adapter['title']}[/green]")
|
|
377
|
+
return selected_adapter["name"]
|
|
378
|
+
else:
|
|
379
|
+
console.print(f"[red]Please enter a number between 1 and {len(adapters)}[/red]")
|
|
380
|
+
except (ValueError, typer.Abort):
|
|
381
|
+
console.print("[yellow]Setup cancelled.[/yellow]")
|
|
382
|
+
raise typer.Exit(0)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
@app.command()
|
|
386
|
+
def setup(
|
|
387
|
+
adapter: Optional[str] = typer.Option(
|
|
388
|
+
None,
|
|
389
|
+
"--adapter",
|
|
390
|
+
"-a",
|
|
391
|
+
help="Adapter type to use (interactive prompt if not specified)",
|
|
392
|
+
),
|
|
393
|
+
project_path: Optional[str] = typer.Option(
|
|
394
|
+
None, "--path", help="Project path (default: current directory)"
|
|
395
|
+
),
|
|
396
|
+
global_config: bool = typer.Option(
|
|
397
|
+
False,
|
|
398
|
+
"--global",
|
|
399
|
+
"-g",
|
|
400
|
+
help="Save to global config instead of project-specific",
|
|
401
|
+
),
|
|
402
|
+
base_path: Optional[str] = typer.Option(
|
|
403
|
+
None,
|
|
404
|
+
"--base-path",
|
|
405
|
+
"-p",
|
|
406
|
+
help="Base path for ticket storage (AITrackdown only)",
|
|
407
|
+
),
|
|
408
|
+
api_key: Optional[str] = typer.Option(
|
|
409
|
+
None, "--api-key", help="API key for Linear or API token for JIRA"
|
|
410
|
+
),
|
|
411
|
+
team_id: Optional[str] = typer.Option(
|
|
412
|
+
None, "--team-id", help="Linear team ID (required for Linear adapter)"
|
|
413
|
+
),
|
|
414
|
+
jira_server: Optional[str] = typer.Option(
|
|
415
|
+
None,
|
|
416
|
+
"--jira-server",
|
|
417
|
+
help="JIRA server URL (e.g., https://company.atlassian.net)",
|
|
418
|
+
),
|
|
419
|
+
jira_email: Optional[str] = typer.Option(
|
|
420
|
+
None, "--jira-email", help="JIRA user email for authentication"
|
|
421
|
+
),
|
|
422
|
+
jira_project: Optional[str] = typer.Option(
|
|
423
|
+
None, "--jira-project", help="Default JIRA project key"
|
|
424
|
+
),
|
|
425
|
+
github_owner: Optional[str] = typer.Option(
|
|
426
|
+
None, "--github-owner", help="GitHub repository owner"
|
|
427
|
+
),
|
|
428
|
+
github_repo: Optional[str] = typer.Option(
|
|
429
|
+
None, "--github-repo", help="GitHub repository name"
|
|
430
|
+
),
|
|
431
|
+
github_token: Optional[str] = typer.Option(
|
|
432
|
+
None, "--github-token", help="GitHub Personal Access Token"
|
|
433
|
+
),
|
|
434
|
+
) -> None:
|
|
435
|
+
"""Interactive setup wizard for MCP Ticketer (alias for init).
|
|
436
|
+
|
|
437
|
+
This command provides a user-friendly setup experience with prompts
|
|
438
|
+
to guide you through configuring MCP Ticketer for your preferred
|
|
439
|
+
ticket management system. It's identical to 'init' and 'install'.
|
|
440
|
+
|
|
441
|
+
Examples:
|
|
442
|
+
# Run interactive setup
|
|
443
|
+
mcp-ticketer setup
|
|
444
|
+
|
|
445
|
+
# Setup with specific adapter
|
|
446
|
+
mcp-ticketer setup --adapter linear
|
|
447
|
+
|
|
448
|
+
# Setup for different project
|
|
449
|
+
mcp-ticketer setup --path /path/to/project
|
|
450
|
+
"""
|
|
451
|
+
# Call init with all parameters
|
|
452
|
+
init(
|
|
453
|
+
adapter=adapter,
|
|
454
|
+
project_path=project_path,
|
|
455
|
+
global_config=global_config,
|
|
456
|
+
base_path=base_path,
|
|
457
|
+
api_key=api_key,
|
|
458
|
+
team_id=team_id,
|
|
459
|
+
jira_server=jira_server,
|
|
460
|
+
jira_email=jira_email,
|
|
461
|
+
jira_project=jira_project,
|
|
462
|
+
github_owner=github_owner,
|
|
463
|
+
github_repo=github_repo,
|
|
464
|
+
github_token=github_token,
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
|
|
320
468
|
@app.command()
|
|
321
469
|
def init(
|
|
322
470
|
adapter: Optional[str] = typer.Option(
|
|
323
471
|
None,
|
|
324
472
|
"--adapter",
|
|
325
473
|
"-a",
|
|
326
|
-
help="Adapter type to use (
|
|
474
|
+
help="Adapter type to use (interactive prompt if not specified)",
|
|
327
475
|
),
|
|
328
476
|
project_path: Optional[str] = typer.Option(
|
|
329
477
|
None, "--path", help="Project path (default: current directory)"
|
|
@@ -369,11 +517,17 @@ def init(
|
|
|
369
517
|
) -> None:
|
|
370
518
|
"""Initialize mcp-ticketer for the current project.
|
|
371
519
|
|
|
520
|
+
This command sets up MCP Ticketer configuration with interactive prompts
|
|
521
|
+
to guide you through the process. It auto-detects adapter configuration
|
|
522
|
+
from .env files or prompts for interactive setup if no configuration is found.
|
|
523
|
+
|
|
372
524
|
Creates .mcp-ticketer/config.json in the current directory with
|
|
373
525
|
auto-detected or specified adapter configuration.
|
|
374
526
|
|
|
527
|
+
Note: 'setup' and 'install' are synonyms for this command.
|
|
528
|
+
|
|
375
529
|
Examples:
|
|
376
|
-
#
|
|
530
|
+
# Interactive setup (same as 'setup' and 'install')
|
|
377
531
|
mcp-ticketer init
|
|
378
532
|
|
|
379
533
|
# Force specific adapter
|
|
@@ -429,16 +583,21 @@ def init(
|
|
|
429
583
|
f"\n[dim]Configuration found in: {primary.found_in}[/dim]"
|
|
430
584
|
)
|
|
431
585
|
console.print(f"[dim]Confidence: {primary.confidence:.0%}[/dim]")
|
|
586
|
+
|
|
587
|
+
# Ask user to confirm auto-detected adapter
|
|
588
|
+
if not typer.confirm(
|
|
589
|
+
f"Use detected {adapter_type} adapter?",
|
|
590
|
+
default=True,
|
|
591
|
+
):
|
|
592
|
+
adapter_type = None # Will trigger interactive selection
|
|
432
593
|
else:
|
|
433
|
-
adapter_type =
|
|
434
|
-
console.print(
|
|
435
|
-
"[yellow]⚠ No credentials found, defaulting to aitrackdown[/yellow]"
|
|
436
|
-
)
|
|
594
|
+
adapter_type = None # Will trigger interactive selection
|
|
437
595
|
else:
|
|
438
|
-
adapter_type =
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
596
|
+
adapter_type = None # Will trigger interactive selection
|
|
597
|
+
|
|
598
|
+
# If no adapter determined, show interactive selection
|
|
599
|
+
if not adapter_type:
|
|
600
|
+
adapter_type = _prompt_for_adapter_selection(console)
|
|
442
601
|
|
|
443
602
|
# 2. Create configuration based on adapter type
|
|
444
603
|
config = {"default_adapter": adapter_type, "adapters": {}}
|
|
@@ -462,59 +621,99 @@ def init(
|
|
|
462
621
|
}
|
|
463
622
|
|
|
464
623
|
elif adapter_type == "linear":
|
|
465
|
-
# If not auto-discovered, build from CLI params
|
|
624
|
+
# If not auto-discovered, build from CLI params or prompt
|
|
466
625
|
if adapter_type not in config["adapters"]:
|
|
467
626
|
linear_config = {}
|
|
468
627
|
|
|
469
|
-
# Team ID
|
|
470
|
-
if team_id:
|
|
471
|
-
linear_config["team_id"] = team_id
|
|
472
|
-
|
|
473
628
|
# API Key
|
|
474
629
|
linear_api_key = api_key or os.getenv("LINEAR_API_KEY")
|
|
630
|
+
if not linear_api_key and not discovered:
|
|
631
|
+
console.print("\n[bold]Linear Configuration[/bold]")
|
|
632
|
+
console.print("You need a Linear API key to connect to Linear.")
|
|
633
|
+
console.print("[dim]Get your API key at: https://linear.app/settings/api[/dim]\n")
|
|
634
|
+
|
|
635
|
+
linear_api_key = typer.prompt(
|
|
636
|
+
"Enter your Linear API key",
|
|
637
|
+
hide_input=True
|
|
638
|
+
)
|
|
639
|
+
|
|
475
640
|
if linear_api_key:
|
|
476
641
|
linear_config["api_key"] = linear_api_key
|
|
477
|
-
elif not discovered:
|
|
478
|
-
console.print("[yellow]Warning:[/yellow] No Linear API key provided.")
|
|
479
|
-
console.print(
|
|
480
|
-
"Set LINEAR_API_KEY environment variable or use --api-key option"
|
|
481
|
-
)
|
|
482
642
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
643
|
+
# Team ID
|
|
644
|
+
linear_team_id = team_id or os.getenv("LINEAR_TEAM_ID")
|
|
645
|
+
if not linear_team_id and not discovered:
|
|
646
|
+
console.print("\nYou need your Linear team ID.")
|
|
647
|
+
console.print("[dim]Find it in Linear settings or team URL[/dim]\n")
|
|
648
|
+
|
|
649
|
+
linear_team_id = typer.prompt("Enter your Linear team ID")
|
|
650
|
+
|
|
651
|
+
if linear_team_id:
|
|
652
|
+
linear_config["team_id"] = linear_team_id
|
|
653
|
+
|
|
654
|
+
if not linear_config.get("api_key") or not linear_config.get("team_id"):
|
|
655
|
+
console.print("[red]Error:[/red] Linear requires both API key and team ID")
|
|
656
|
+
console.print("Run 'mcp-ticketer init --adapter linear' with proper credentials")
|
|
657
|
+
raise typer.Exit(1)
|
|
658
|
+
|
|
659
|
+
linear_config["type"] = "linear"
|
|
660
|
+
config["adapters"]["linear"] = linear_config
|
|
486
661
|
|
|
487
662
|
elif adapter_type == "jira":
|
|
488
|
-
# If not auto-discovered, build from CLI params
|
|
663
|
+
# If not auto-discovered, build from CLI params or prompt
|
|
489
664
|
if adapter_type not in config["adapters"]:
|
|
490
665
|
server = jira_server or os.getenv("JIRA_SERVER")
|
|
491
666
|
email = jira_email or os.getenv("JIRA_EMAIL")
|
|
492
667
|
token = api_key or os.getenv("JIRA_API_TOKEN")
|
|
493
668
|
project = jira_project or os.getenv("JIRA_PROJECT_KEY")
|
|
494
669
|
|
|
670
|
+
# Interactive prompts for missing values
|
|
671
|
+
if not server and not discovered:
|
|
672
|
+
console.print("\n[bold]JIRA Configuration[/bold]")
|
|
673
|
+
console.print("Enter your JIRA server details.\n")
|
|
674
|
+
|
|
675
|
+
server = typer.prompt(
|
|
676
|
+
"JIRA server URL (e.g., https://company.atlassian.net)"
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
if not email and not discovered:
|
|
680
|
+
email = typer.prompt("Your JIRA email address")
|
|
681
|
+
|
|
682
|
+
if not token and not discovered:
|
|
683
|
+
console.print("\nYou need a JIRA API token.")
|
|
684
|
+
console.print("[dim]Generate one at: https://id.atlassian.com/manage/api-tokens[/dim]\n")
|
|
685
|
+
|
|
686
|
+
token = typer.prompt(
|
|
687
|
+
"Enter your JIRA API token",
|
|
688
|
+
hide_input=True
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
if not project and not discovered:
|
|
692
|
+
project = typer.prompt(
|
|
693
|
+
"Default JIRA project key (optional, press Enter to skip)",
|
|
694
|
+
default="",
|
|
695
|
+
show_default=False
|
|
696
|
+
)
|
|
697
|
+
|
|
698
|
+
# Validate required fields
|
|
495
699
|
if not server:
|
|
496
700
|
console.print("[red]Error:[/red] JIRA server URL is required")
|
|
497
|
-
console.print(
|
|
498
|
-
"Use --jira-server or set JIRA_SERVER environment variable"
|
|
499
|
-
)
|
|
500
701
|
raise typer.Exit(1)
|
|
501
702
|
|
|
502
703
|
if not email:
|
|
503
704
|
console.print("[red]Error:[/red] JIRA email is required")
|
|
504
|
-
console.print("Use --jira-email or set JIRA_EMAIL environment variable")
|
|
505
705
|
raise typer.Exit(1)
|
|
506
706
|
|
|
507
707
|
if not token:
|
|
508
708
|
console.print("[red]Error:[/red] JIRA API token is required")
|
|
509
|
-
console.print(
|
|
510
|
-
"Use --api-key or set JIRA_API_TOKEN environment variable"
|
|
511
|
-
)
|
|
512
|
-
console.print(
|
|
513
|
-
"[dim]Generate token at: https://id.atlassian.com/manage/api-tokens[/dim]"
|
|
514
|
-
)
|
|
515
709
|
raise typer.Exit(1)
|
|
516
710
|
|
|
517
|
-
jira_config = {
|
|
711
|
+
jira_config = {
|
|
712
|
+
"server": server,
|
|
713
|
+
"email": email,
|
|
714
|
+
"api_token": token,
|
|
715
|
+
"type": "jira"
|
|
716
|
+
}
|
|
518
717
|
|
|
519
718
|
if project:
|
|
520
719
|
jira_config["project_key"] = project
|
|
@@ -522,45 +721,50 @@ def init(
|
|
|
522
721
|
config["adapters"]["jira"] = jira_config
|
|
523
722
|
|
|
524
723
|
elif adapter_type == "github":
|
|
525
|
-
# If not auto-discovered, build from CLI params
|
|
724
|
+
# If not auto-discovered, build from CLI params or prompt
|
|
526
725
|
if adapter_type not in config["adapters"]:
|
|
527
726
|
owner = github_owner or os.getenv("GITHUB_OWNER")
|
|
528
727
|
repo = github_repo or os.getenv("GITHUB_REPO")
|
|
529
728
|
token = github_token or os.getenv("GITHUB_TOKEN")
|
|
530
729
|
|
|
730
|
+
# Interactive prompts for missing values
|
|
731
|
+
if not owner and not discovered:
|
|
732
|
+
console.print("\n[bold]GitHub Configuration[/bold]")
|
|
733
|
+
console.print("Enter your GitHub repository details.\n")
|
|
734
|
+
|
|
735
|
+
owner = typer.prompt("GitHub repository owner (username or organization)")
|
|
736
|
+
|
|
737
|
+
if not repo and not discovered:
|
|
738
|
+
repo = typer.prompt("GitHub repository name")
|
|
739
|
+
|
|
740
|
+
if not token and not discovered:
|
|
741
|
+
console.print("\nYou need a GitHub Personal Access Token.")
|
|
742
|
+
console.print("[dim]Create one at: https://github.com/settings/tokens/new[/dim]")
|
|
743
|
+
console.print("[dim]Required scopes: repo (for private repos) or public_repo (for public repos)[/dim]\n")
|
|
744
|
+
|
|
745
|
+
token = typer.prompt(
|
|
746
|
+
"Enter your GitHub Personal Access Token",
|
|
747
|
+
hide_input=True
|
|
748
|
+
)
|
|
749
|
+
|
|
750
|
+
# Validate required fields
|
|
531
751
|
if not owner:
|
|
532
752
|
console.print("[red]Error:[/red] GitHub repository owner is required")
|
|
533
|
-
console.print(
|
|
534
|
-
"Use --github-owner or set GITHUB_OWNER environment variable"
|
|
535
|
-
)
|
|
536
753
|
raise typer.Exit(1)
|
|
537
754
|
|
|
538
755
|
if not repo:
|
|
539
756
|
console.print("[red]Error:[/red] GitHub repository name is required")
|
|
540
|
-
console.print(
|
|
541
|
-
"Use --github-repo or set GITHUB_REPO environment variable"
|
|
542
|
-
)
|
|
543
757
|
raise typer.Exit(1)
|
|
544
758
|
|
|
545
759
|
if not token:
|
|
546
|
-
console.print(
|
|
547
|
-
"[red]Error:[/red] GitHub Personal Access Token is required"
|
|
548
|
-
)
|
|
549
|
-
console.print(
|
|
550
|
-
"Use --github-token or set GITHUB_TOKEN environment variable"
|
|
551
|
-
)
|
|
552
|
-
console.print(
|
|
553
|
-
"[dim]Create token at: https://github.com/settings/tokens/new[/dim]"
|
|
554
|
-
)
|
|
555
|
-
console.print(
|
|
556
|
-
"[dim]Required scopes: repo (for private repos) or public_repo (for public repos)[/dim]"
|
|
557
|
-
)
|
|
760
|
+
console.print("[red]Error:[/red] GitHub Personal Access Token is required")
|
|
558
761
|
raise typer.Exit(1)
|
|
559
762
|
|
|
560
763
|
config["adapters"]["github"] = {
|
|
561
764
|
"owner": owner,
|
|
562
765
|
"repo": repo,
|
|
563
766
|
"token": token,
|
|
767
|
+
"type": "github"
|
|
564
768
|
}
|
|
565
769
|
|
|
566
770
|
# 5. Save to appropriate location
|
|
@@ -600,6 +804,48 @@ def init(
|
|
|
600
804
|
f.write("# MCP Ticketer\n.mcp-ticketer/\n")
|
|
601
805
|
console.print("[dim]✓ Created .gitignore with .mcp-ticketer/[/dim]")
|
|
602
806
|
|
|
807
|
+
# Show next steps
|
|
808
|
+
_show_next_steps(console, adapter_type, config_file_path)
|
|
809
|
+
|
|
810
|
+
|
|
811
|
+
def _show_next_steps(console: Console, adapter_type: str, config_file_path: Path) -> None:
|
|
812
|
+
"""Show helpful next steps after initialization.
|
|
813
|
+
|
|
814
|
+
Args:
|
|
815
|
+
console: Rich console for output
|
|
816
|
+
adapter_type: Type of adapter that was configured
|
|
817
|
+
config_file_path: Path to the configuration file
|
|
818
|
+
"""
|
|
819
|
+
console.print("\n[bold green]🎉 Setup Complete![/bold green]")
|
|
820
|
+
console.print(f"MCP Ticketer is now configured to use {adapter_type.title()}.\n")
|
|
821
|
+
|
|
822
|
+
console.print("[bold]Next Steps:[/bold]")
|
|
823
|
+
console.print("1. [cyan]Test your configuration:[/cyan]")
|
|
824
|
+
console.print(" mcp-ticketer diagnose")
|
|
825
|
+
console.print("\n2. [cyan]Create a test ticket:[/cyan]")
|
|
826
|
+
console.print(" mcp-ticketer create 'Test ticket from MCP Ticketer'")
|
|
827
|
+
|
|
828
|
+
if adapter_type != "aitrackdown":
|
|
829
|
+
console.print(f"\n3. [cyan]Verify the ticket appears in {adapter_type.title()}[/cyan]")
|
|
830
|
+
|
|
831
|
+
if adapter_type == "linear":
|
|
832
|
+
console.print(" Check your Linear workspace for the new ticket")
|
|
833
|
+
elif adapter_type == "github":
|
|
834
|
+
console.print(" Check your GitHub repository's Issues tab")
|
|
835
|
+
elif adapter_type == "jira":
|
|
836
|
+
console.print(" Check your JIRA project for the new ticket")
|
|
837
|
+
else:
|
|
838
|
+
console.print("\n3. [cyan]Check local ticket storage:[/cyan]")
|
|
839
|
+
console.print(" ls .aitrackdown/")
|
|
840
|
+
|
|
841
|
+
console.print("\n4. [cyan]Configure MCP clients (optional):[/cyan]")
|
|
842
|
+
console.print(" mcp-ticketer mcp claude # For Claude Code")
|
|
843
|
+
console.print(" mcp-ticketer mcp auggie # For Auggie")
|
|
844
|
+
console.print(" mcp-ticketer mcp gemini # For Gemini CLI")
|
|
845
|
+
|
|
846
|
+
console.print(f"\n[dim]Configuration saved to: {config_file_path}[/dim]")
|
|
847
|
+
console.print("[dim]Run 'mcp-ticketer --help' for more commands[/dim]")
|
|
848
|
+
|
|
603
849
|
|
|
604
850
|
@app.command()
|
|
605
851
|
def install(
|
|
@@ -653,12 +899,12 @@ def install(
|
|
|
653
899
|
) -> None:
|
|
654
900
|
"""Initialize mcp-ticketer for the current project (alias for init).
|
|
655
901
|
|
|
656
|
-
This command is synonymous with 'init' and
|
|
657
|
-
|
|
658
|
-
|
|
902
|
+
This command is synonymous with 'init' and 'setup' - all three provide
|
|
903
|
+
identical functionality with interactive prompts to guide you through
|
|
904
|
+
configuring MCP Ticketer for your preferred ticket management system.
|
|
659
905
|
|
|
660
906
|
Examples:
|
|
661
|
-
#
|
|
907
|
+
# Interactive setup (same as 'init' and 'setup')
|
|
662
908
|
mcp-ticketer install
|
|
663
909
|
|
|
664
910
|
# Force specific adapter
|
|
@@ -1548,6 +1794,9 @@ def check(queue_id: str = typer.Argument(..., help="Queue ID to check")):
|
|
|
1548
1794
|
console.print(f"\nRetry Count: {item.retry_count}")
|
|
1549
1795
|
|
|
1550
1796
|
|
|
1797
|
+
|
|
1798
|
+
|
|
1799
|
+
|
|
1551
1800
|
@app.command()
|
|
1552
1801
|
def serve(
|
|
1553
1802
|
adapter: Optional[AdapterType] = typer.Option(
|
|
@@ -1575,16 +1824,27 @@ def serve(
|
|
|
1575
1824
|
# Load configuration (respects project-specific config in cwd)
|
|
1576
1825
|
config = load_config()
|
|
1577
1826
|
|
|
1578
|
-
# Determine adapter type
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1827
|
+
# Determine adapter type with priority: CLI arg > .env files > config > default
|
|
1828
|
+
if adapter:
|
|
1829
|
+
# Priority 1: Command line argument
|
|
1830
|
+
adapter_type = adapter.value
|
|
1831
|
+
# Get base config from config file
|
|
1832
|
+
adapters_config = config.get("adapters", {})
|
|
1833
|
+
adapter_config = adapters_config.get(adapter_type, {})
|
|
1834
|
+
else:
|
|
1835
|
+
# Priority 2: .env files
|
|
1836
|
+
from ..mcp.server import _load_env_configuration
|
|
1837
|
+
env_config = _load_env_configuration()
|
|
1838
|
+
if env_config:
|
|
1839
|
+
adapter_type = env_config["adapter_type"]
|
|
1840
|
+
adapter_config = env_config["adapter_config"]
|
|
1841
|
+
else:
|
|
1842
|
+
# Priority 3: Configuration file
|
|
1843
|
+
adapter_type = config.get("default_adapter", "aitrackdown")
|
|
1844
|
+
adapters_config = config.get("adapters", {})
|
|
1845
|
+
adapter_config = adapters_config.get(adapter_type, {})
|
|
1586
1846
|
|
|
1587
|
-
# Override with command line options if provided
|
|
1847
|
+
# Override with command line options if provided (highest priority)
|
|
1588
1848
|
if base_path and adapter_type == "aitrackdown":
|
|
1589
1849
|
adapter_config["base_path"] = base_path
|
|
1590
1850
|
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Exception classes for MCP Ticketer."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
from .models import TicketState
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MCPTicketerError(Exception):
|
|
11
|
+
"""Base exception for MCP Ticketer."""
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AdapterError(MCPTicketerError):
|
|
16
|
+
"""Base adapter error."""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
message: str,
|
|
21
|
+
adapter_name: str,
|
|
22
|
+
original_error: Optional[Exception] = None
|
|
23
|
+
):
|
|
24
|
+
"""Initialize adapter error.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
message: Error message
|
|
28
|
+
adapter_name: Name of the adapter that raised the error
|
|
29
|
+
original_error: Original exception that caused this error
|
|
30
|
+
"""
|
|
31
|
+
super().__init__(message)
|
|
32
|
+
self.adapter_name = adapter_name
|
|
33
|
+
self.original_error = original_error
|
|
34
|
+
|
|
35
|
+
def __str__(self) -> str:
|
|
36
|
+
"""String representation of the error."""
|
|
37
|
+
base_msg = f"[{self.adapter_name}] {super().__str__()}"
|
|
38
|
+
if self.original_error:
|
|
39
|
+
base_msg += f" (caused by: {self.original_error})"
|
|
40
|
+
return base_msg
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class AuthenticationError(AdapterError):
|
|
44
|
+
"""Authentication failed with external service."""
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class RateLimitError(AdapterError):
|
|
49
|
+
"""Rate limit exceeded."""
|
|
50
|
+
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
message: str,
|
|
54
|
+
adapter_name: str,
|
|
55
|
+
retry_after: Optional[int] = None,
|
|
56
|
+
original_error: Optional[Exception] = None
|
|
57
|
+
):
|
|
58
|
+
"""Initialize rate limit error.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
message: Error message
|
|
62
|
+
adapter_name: Name of the adapter
|
|
63
|
+
retry_after: Seconds to wait before retrying
|
|
64
|
+
original_error: Original exception
|
|
65
|
+
"""
|
|
66
|
+
super().__init__(message, adapter_name, original_error)
|
|
67
|
+
self.retry_after = retry_after
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class ValidationError(MCPTicketerError):
|
|
71
|
+
"""Data validation error."""
|
|
72
|
+
|
|
73
|
+
def __init__(
|
|
74
|
+
self,
|
|
75
|
+
message: str,
|
|
76
|
+
field: Optional[str] = None,
|
|
77
|
+
value: Any = None
|
|
78
|
+
):
|
|
79
|
+
"""Initialize validation error.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
message: Error message
|
|
83
|
+
field: Field that failed validation
|
|
84
|
+
value: Value that failed validation
|
|
85
|
+
"""
|
|
86
|
+
super().__init__(message)
|
|
87
|
+
self.field = field
|
|
88
|
+
self.value = value
|
|
89
|
+
|
|
90
|
+
def __str__(self) -> str:
|
|
91
|
+
"""String representation of the error."""
|
|
92
|
+
base_msg = super().__str__()
|
|
93
|
+
if self.field:
|
|
94
|
+
base_msg += f" (field: {self.field})"
|
|
95
|
+
if self.value is not None:
|
|
96
|
+
base_msg += f" (value: {self.value})"
|
|
97
|
+
return base_msg
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class ConfigurationError(MCPTicketerError):
|
|
101
|
+
"""Configuration error."""
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class CacheError(MCPTicketerError):
|
|
106
|
+
"""Cache operation error."""
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class StateTransitionError(MCPTicketerError):
|
|
111
|
+
"""Invalid state transition."""
|
|
112
|
+
|
|
113
|
+
def __init__(
|
|
114
|
+
self,
|
|
115
|
+
message: str,
|
|
116
|
+
from_state: TicketState,
|
|
117
|
+
to_state: TicketState
|
|
118
|
+
):
|
|
119
|
+
"""Initialize state transition error.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
message: Error message
|
|
123
|
+
from_state: Current state
|
|
124
|
+
to_state: Target state
|
|
125
|
+
"""
|
|
126
|
+
super().__init__(message)
|
|
127
|
+
self.from_state = from_state
|
|
128
|
+
self.to_state = to_state
|
|
129
|
+
|
|
130
|
+
def __str__(self) -> str:
|
|
131
|
+
"""String representation of the error."""
|
|
132
|
+
return f"{super().__str__()} ({self.from_state} -> {self.to_state})"
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class NetworkError(AdapterError):
|
|
136
|
+
"""Network-related error."""
|
|
137
|
+
pass
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class TimeoutError(AdapterError):
|
|
141
|
+
"""Request timeout error."""
|
|
142
|
+
pass
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class NotFoundError(AdapterError):
|
|
146
|
+
"""Resource not found error."""
|
|
147
|
+
pass
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class PermissionError(AdapterError):
|
|
151
|
+
"""Permission denied error."""
|
|
152
|
+
pass
|