mcp-ticketer 0.3.5__py3-none-any.whl → 0.12.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.
Potentially problematic release.
This version of mcp-ticketer might be problematic. Click here for more details.
- mcp_ticketer/__version__.py +3 -3
- mcp_ticketer/adapters/__init__.py +2 -0
- mcp_ticketer/adapters/aitrackdown.py +263 -14
- mcp_ticketer/adapters/asana/__init__.py +15 -0
- mcp_ticketer/adapters/asana/adapter.py +1308 -0
- mcp_ticketer/adapters/asana/client.py +292 -0
- mcp_ticketer/adapters/asana/mappers.py +334 -0
- mcp_ticketer/adapters/asana/types.py +146 -0
- mcp_ticketer/adapters/github.py +326 -109
- mcp_ticketer/adapters/hybrid.py +11 -11
- mcp_ticketer/adapters/jira.py +271 -25
- mcp_ticketer/adapters/linear/adapter.py +693 -39
- mcp_ticketer/adapters/linear/client.py +61 -9
- mcp_ticketer/adapters/linear/mappers.py +9 -3
- mcp_ticketer/adapters/linear/queries.py +9 -7
- mcp_ticketer/cache/memory.py +9 -8
- mcp_ticketer/cli/adapter_diagnostics.py +1 -1
- mcp_ticketer/cli/auggie_configure.py +104 -15
- mcp_ticketer/cli/codex_configure.py +188 -32
- mcp_ticketer/cli/configure.py +37 -48
- mcp_ticketer/cli/diagnostics.py +20 -18
- mcp_ticketer/cli/discover.py +292 -26
- mcp_ticketer/cli/gemini_configure.py +107 -26
- mcp_ticketer/cli/instruction_commands.py +429 -0
- mcp_ticketer/cli/linear_commands.py +105 -22
- mcp_ticketer/cli/main.py +1830 -435
- mcp_ticketer/cli/mcp_configure.py +296 -89
- mcp_ticketer/cli/migrate_config.py +12 -8
- mcp_ticketer/cli/platform_commands.py +123 -0
- mcp_ticketer/cli/platform_detection.py +412 -0
- mcp_ticketer/cli/python_detection.py +126 -0
- mcp_ticketer/cli/queue_commands.py +15 -15
- mcp_ticketer/cli/simple_health.py +1 -1
- mcp_ticketer/cli/ticket_commands.py +773 -0
- mcp_ticketer/cli/update_checker.py +313 -0
- mcp_ticketer/cli/utils.py +67 -62
- mcp_ticketer/core/__init__.py +14 -1
- mcp_ticketer/core/adapter.py +84 -15
- mcp_ticketer/core/config.py +44 -39
- mcp_ticketer/core/env_discovery.py +42 -12
- mcp_ticketer/core/env_loader.py +15 -14
- mcp_ticketer/core/exceptions.py +3 -3
- mcp_ticketer/core/http_client.py +26 -26
- mcp_ticketer/core/instructions.py +405 -0
- mcp_ticketer/core/mappers.py +11 -11
- mcp_ticketer/core/models.py +50 -20
- mcp_ticketer/core/onepassword_secrets.py +379 -0
- mcp_ticketer/core/project_config.py +57 -35
- mcp_ticketer/core/registry.py +3 -3
- mcp_ticketer/defaults/ticket_instructions.md +644 -0
- mcp_ticketer/mcp/__init__.py +29 -1
- mcp_ticketer/mcp/__main__.py +60 -0
- mcp_ticketer/mcp/server/__init__.py +25 -0
- mcp_ticketer/mcp/server/__main__.py +60 -0
- mcp_ticketer/mcp/{dto.py → server/dto.py} +32 -32
- mcp_ticketer/mcp/{server.py → server/main.py} +127 -74
- mcp_ticketer/mcp/{response_builder.py → server/response_builder.py} +2 -2
- mcp_ticketer/mcp/server/server_sdk.py +93 -0
- mcp_ticketer/mcp/server/tools/__init__.py +47 -0
- mcp_ticketer/mcp/server/tools/attachment_tools.py +226 -0
- mcp_ticketer/mcp/server/tools/bulk_tools.py +273 -0
- mcp_ticketer/mcp/server/tools/comment_tools.py +90 -0
- mcp_ticketer/mcp/server/tools/config_tools.py +381 -0
- mcp_ticketer/mcp/server/tools/hierarchy_tools.py +532 -0
- mcp_ticketer/mcp/server/tools/instruction_tools.py +293 -0
- mcp_ticketer/mcp/server/tools/pr_tools.py +154 -0
- mcp_ticketer/mcp/server/tools/search_tools.py +206 -0
- mcp_ticketer/mcp/server/tools/ticket_tools.py +430 -0
- mcp_ticketer/mcp/server/tools/user_ticket_tools.py +382 -0
- mcp_ticketer/queue/__init__.py +1 -0
- mcp_ticketer/queue/health_monitor.py +5 -4
- mcp_ticketer/queue/manager.py +15 -51
- mcp_ticketer/queue/queue.py +19 -19
- mcp_ticketer/queue/run_worker.py +1 -1
- mcp_ticketer/queue/ticket_registry.py +14 -14
- mcp_ticketer/queue/worker.py +16 -14
- {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/METADATA +168 -32
- mcp_ticketer-0.12.0.dist-info/RECORD +91 -0
- mcp_ticketer-0.3.5.dist-info/RECORD +0 -62
- /mcp_ticketer/mcp/{constants.py → server/constants.py} +0 -0
- {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/top_level.txt +0 -0
mcp_ticketer/cli/main.py
CHANGED
|
@@ -5,7 +5,7 @@ import json
|
|
|
5
5
|
import os
|
|
6
6
|
from enum import Enum
|
|
7
7
|
from pathlib import Path
|
|
8
|
-
from typing import
|
|
8
|
+
from typing import Any
|
|
9
9
|
|
|
10
10
|
import typer
|
|
11
11
|
from dotenv import load_dotenv
|
|
@@ -24,9 +24,11 @@ from ..queue.ticket_registry import TicketRegistry
|
|
|
24
24
|
from .configure import configure_wizard, set_adapter_config, show_current_config
|
|
25
25
|
from .diagnostics import run_diagnostics
|
|
26
26
|
from .discover import app as discover_app
|
|
27
|
-
from .
|
|
27
|
+
from .instruction_commands import app as instruction_app
|
|
28
28
|
from .migrate_config import migrate_config_command
|
|
29
|
+
from .platform_commands import app as platform_app
|
|
29
30
|
from .queue_commands import app as queue_app
|
|
31
|
+
from .ticket_commands import app as ticket_app
|
|
30
32
|
|
|
31
33
|
# Load environment variables from .env files
|
|
32
34
|
# Priority: .env.local (highest) > .env (base)
|
|
@@ -48,11 +50,11 @@ app = typer.Typer(
|
|
|
48
50
|
console = Console()
|
|
49
51
|
|
|
50
52
|
|
|
51
|
-
def version_callback(value: bool):
|
|
53
|
+
def version_callback(value: bool) -> None:
|
|
52
54
|
"""Print version and exit."""
|
|
53
55
|
if value:
|
|
54
56
|
console.print(f"mcp-ticketer version {__version__}")
|
|
55
|
-
raise typer.Exit()
|
|
57
|
+
raise typer.Exit() from None
|
|
56
58
|
|
|
57
59
|
|
|
58
60
|
@app.callback()
|
|
@@ -65,7 +67,7 @@ def main_callback(
|
|
|
65
67
|
is_eager=True,
|
|
66
68
|
help="Show version and exit",
|
|
67
69
|
),
|
|
68
|
-
):
|
|
70
|
+
) -> None:
|
|
69
71
|
"""MCP Ticketer - Universal ticket management interface."""
|
|
70
72
|
pass
|
|
71
73
|
|
|
@@ -83,7 +85,7 @@ class AdapterType(str, Enum):
|
|
|
83
85
|
GITHUB = "github"
|
|
84
86
|
|
|
85
87
|
|
|
86
|
-
def load_config(project_dir:
|
|
88
|
+
def load_config(project_dir: Path | None = None) -> dict:
|
|
87
89
|
"""Load configuration from project-local config file ONLY.
|
|
88
90
|
|
|
89
91
|
SECURITY: This method ONLY reads from the current project directory
|
|
@@ -145,7 +147,7 @@ def load_config(project_dir: Optional[Path] = None) -> dict:
|
|
|
145
147
|
return {"adapter": "aitrackdown", "config": {"base_path": ".aitrackdown"}}
|
|
146
148
|
|
|
147
149
|
|
|
148
|
-
def _discover_from_env_files() ->
|
|
150
|
+
def _discover_from_env_files() -> str | None:
|
|
149
151
|
"""Discover adapter configuration from .env or .env.local files.
|
|
150
152
|
|
|
151
153
|
Returns:
|
|
@@ -269,8 +271,8 @@ def merge_config(updates: dict) -> dict:
|
|
|
269
271
|
|
|
270
272
|
|
|
271
273
|
def get_adapter(
|
|
272
|
-
override_adapter:
|
|
273
|
-
):
|
|
274
|
+
override_adapter: str | None = None, override_config: dict | None = None
|
|
275
|
+
) -> Any:
|
|
274
276
|
"""Get configured adapter instance.
|
|
275
277
|
|
|
276
278
|
Args:
|
|
@@ -318,6 +320,416 @@ def get_adapter(
|
|
|
318
320
|
return AdapterRegistry.get_adapter(adapter_type, adapter_config)
|
|
319
321
|
|
|
320
322
|
|
|
323
|
+
async def _validate_adapter_credentials(
|
|
324
|
+
adapter_type: str, config_file_path: Path
|
|
325
|
+
) -> list[str]:
|
|
326
|
+
"""Validate adapter credentials by performing real connectivity tests.
|
|
327
|
+
|
|
328
|
+
Args:
|
|
329
|
+
adapter_type: Type of adapter to validate
|
|
330
|
+
config_file_path: Path to config file
|
|
331
|
+
|
|
332
|
+
Returns:
|
|
333
|
+
List of validation issues (empty if valid)
|
|
334
|
+
|
|
335
|
+
"""
|
|
336
|
+
import json
|
|
337
|
+
|
|
338
|
+
issues = []
|
|
339
|
+
|
|
340
|
+
try:
|
|
341
|
+
# Load config
|
|
342
|
+
with open(config_file_path) as f:
|
|
343
|
+
config = json.load(f)
|
|
344
|
+
|
|
345
|
+
adapter_config = config.get("adapters", {}).get(adapter_type, {})
|
|
346
|
+
|
|
347
|
+
if not adapter_config:
|
|
348
|
+
issues.append(f"No configuration found for {adapter_type}")
|
|
349
|
+
return issues
|
|
350
|
+
|
|
351
|
+
# Validate based on adapter type
|
|
352
|
+
if adapter_type == "linear":
|
|
353
|
+
api_key = adapter_config.get("api_key")
|
|
354
|
+
|
|
355
|
+
# Check API key format
|
|
356
|
+
if not api_key:
|
|
357
|
+
issues.append("Linear API key is missing")
|
|
358
|
+
return issues
|
|
359
|
+
|
|
360
|
+
if not api_key.startswith("lin_api_"):
|
|
361
|
+
issues.append(
|
|
362
|
+
"Invalid Linear API key format (should start with 'lin_api_')"
|
|
363
|
+
)
|
|
364
|
+
return issues
|
|
365
|
+
|
|
366
|
+
# Test actual connectivity
|
|
367
|
+
try:
|
|
368
|
+
from ..adapters.linear import LinearAdapter
|
|
369
|
+
|
|
370
|
+
adapter = LinearAdapter(adapter_config)
|
|
371
|
+
# Try to list one ticket to verify connectivity
|
|
372
|
+
await adapter.list(limit=1)
|
|
373
|
+
except Exception as e:
|
|
374
|
+
error_msg = str(e)
|
|
375
|
+
if "401" in error_msg or "Unauthorized" in error_msg:
|
|
376
|
+
issues.append(
|
|
377
|
+
"Failed to authenticate with Linear API - invalid API key"
|
|
378
|
+
)
|
|
379
|
+
elif "403" in error_msg or "Forbidden" in error_msg:
|
|
380
|
+
issues.append("Linear API key lacks required permissions")
|
|
381
|
+
elif "team" in error_msg.lower():
|
|
382
|
+
issues.append(f"Linear team configuration error: {error_msg}")
|
|
383
|
+
else:
|
|
384
|
+
issues.append(f"Failed to connect to Linear API: {error_msg}")
|
|
385
|
+
|
|
386
|
+
elif adapter_type == "jira":
|
|
387
|
+
server = adapter_config.get("server")
|
|
388
|
+
email = adapter_config.get("email")
|
|
389
|
+
api_token = adapter_config.get("api_token")
|
|
390
|
+
|
|
391
|
+
# Check required fields
|
|
392
|
+
if not server:
|
|
393
|
+
issues.append("JIRA server URL is missing")
|
|
394
|
+
if not email:
|
|
395
|
+
issues.append("JIRA email is missing")
|
|
396
|
+
if not api_token:
|
|
397
|
+
issues.append("JIRA API token is missing")
|
|
398
|
+
|
|
399
|
+
if issues:
|
|
400
|
+
return issues
|
|
401
|
+
|
|
402
|
+
# Test actual connectivity
|
|
403
|
+
try:
|
|
404
|
+
from ..adapters.jira import JiraAdapter
|
|
405
|
+
|
|
406
|
+
adapter = JiraAdapter(adapter_config)
|
|
407
|
+
await adapter.list(limit=1)
|
|
408
|
+
except Exception as e:
|
|
409
|
+
error_msg = str(e)
|
|
410
|
+
if "401" in error_msg or "Unauthorized" in error_msg:
|
|
411
|
+
issues.append(
|
|
412
|
+
"Failed to authenticate with JIRA - invalid credentials"
|
|
413
|
+
)
|
|
414
|
+
elif "403" in error_msg or "Forbidden" in error_msg:
|
|
415
|
+
issues.append("JIRA credentials lack required permissions")
|
|
416
|
+
else:
|
|
417
|
+
issues.append(f"Failed to connect to JIRA: {error_msg}")
|
|
418
|
+
|
|
419
|
+
elif adapter_type == "github":
|
|
420
|
+
token = adapter_config.get("token") or adapter_config.get("api_key")
|
|
421
|
+
owner = adapter_config.get("owner")
|
|
422
|
+
repo = adapter_config.get("repo")
|
|
423
|
+
|
|
424
|
+
# Check required fields
|
|
425
|
+
if not token:
|
|
426
|
+
issues.append("GitHub token is missing")
|
|
427
|
+
if not owner:
|
|
428
|
+
issues.append("GitHub owner is missing")
|
|
429
|
+
if not repo:
|
|
430
|
+
issues.append("GitHub repo is missing")
|
|
431
|
+
|
|
432
|
+
if issues:
|
|
433
|
+
return issues
|
|
434
|
+
|
|
435
|
+
# Test actual connectivity
|
|
436
|
+
try:
|
|
437
|
+
from ..adapters.github import GitHubAdapter
|
|
438
|
+
|
|
439
|
+
adapter = GitHubAdapter(adapter_config)
|
|
440
|
+
await adapter.list(limit=1)
|
|
441
|
+
except Exception as e:
|
|
442
|
+
error_msg = str(e)
|
|
443
|
+
if (
|
|
444
|
+
"401" in error_msg
|
|
445
|
+
or "Unauthorized" in error_msg
|
|
446
|
+
or "Bad credentials" in error_msg
|
|
447
|
+
):
|
|
448
|
+
issues.append("Failed to authenticate with GitHub - invalid token")
|
|
449
|
+
elif "404" in error_msg or "Not Found" in error_msg:
|
|
450
|
+
issues.append(f"GitHub repository not found: {owner}/{repo}")
|
|
451
|
+
elif "403" in error_msg or "Forbidden" in error_msg:
|
|
452
|
+
issues.append("GitHub token lacks required permissions")
|
|
453
|
+
else:
|
|
454
|
+
issues.append(f"Failed to connect to GitHub: {error_msg}")
|
|
455
|
+
|
|
456
|
+
elif adapter_type == "aitrackdown":
|
|
457
|
+
# AITrackdown doesn't require credentials, just check base_path is set
|
|
458
|
+
base_path = adapter_config.get("base_path")
|
|
459
|
+
if not base_path:
|
|
460
|
+
issues.append("AITrackdown base_path is missing")
|
|
461
|
+
|
|
462
|
+
except Exception as e:
|
|
463
|
+
issues.append(f"Validation error: {str(e)}")
|
|
464
|
+
|
|
465
|
+
return issues
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
async def _validate_configuration_with_retry(
|
|
469
|
+
console: Console, adapter_type: str, config_file_path: Path, proj_path: Path
|
|
470
|
+
) -> bool:
|
|
471
|
+
"""Validate configuration with retry loop for corrections.
|
|
472
|
+
|
|
473
|
+
Args:
|
|
474
|
+
console: Rich console for output
|
|
475
|
+
adapter_type: Type of adapter configured
|
|
476
|
+
config_file_path: Path to config file
|
|
477
|
+
proj_path: Project path
|
|
478
|
+
|
|
479
|
+
Returns:
|
|
480
|
+
True if validation passed or user chose to continue, False if user chose to exit
|
|
481
|
+
|
|
482
|
+
"""
|
|
483
|
+
max_retries = 3
|
|
484
|
+
retry_count = 0
|
|
485
|
+
|
|
486
|
+
while retry_count < max_retries:
|
|
487
|
+
console.print("\n[cyan]🔍 Validating configuration...[/cyan]")
|
|
488
|
+
|
|
489
|
+
# Run real adapter validation (suppress verbose output)
|
|
490
|
+
import io
|
|
491
|
+
import sys
|
|
492
|
+
|
|
493
|
+
# Capture output to suppress verbose diagnostics output
|
|
494
|
+
old_stdout = sys.stdout
|
|
495
|
+
old_stderr = sys.stderr
|
|
496
|
+
sys.stdout = io.StringIO()
|
|
497
|
+
sys.stderr = io.StringIO()
|
|
498
|
+
|
|
499
|
+
try:
|
|
500
|
+
# Perform real adapter validation using diagnostics
|
|
501
|
+
validation_issues = await _validate_adapter_credentials(
|
|
502
|
+
adapter_type, config_file_path
|
|
503
|
+
)
|
|
504
|
+
finally:
|
|
505
|
+
# Restore stdout/stderr
|
|
506
|
+
sys.stdout = old_stdout
|
|
507
|
+
sys.stderr = old_stderr
|
|
508
|
+
|
|
509
|
+
# Check if there are issues
|
|
510
|
+
if not validation_issues:
|
|
511
|
+
console.print("[green]✓ Configuration validated successfully![/green]")
|
|
512
|
+
return True
|
|
513
|
+
|
|
514
|
+
# Display issues found
|
|
515
|
+
console.print("[yellow]⚠️ Configuration validation found issues:[/yellow]")
|
|
516
|
+
for issue in validation_issues:
|
|
517
|
+
console.print(f" [red]❌[/red] {issue}")
|
|
518
|
+
|
|
519
|
+
# Offer user options
|
|
520
|
+
console.print("\n[bold]What would you like to do?[/bold]")
|
|
521
|
+
console.print("1. [cyan]Re-enter configuration values[/cyan] (fix issues)")
|
|
522
|
+
console.print("2. [yellow]Continue anyway[/yellow] (skip validation)")
|
|
523
|
+
console.print("3. [red]Exit[/red] (fix manually later)")
|
|
524
|
+
|
|
525
|
+
try:
|
|
526
|
+
choice = typer.prompt("\nSelect option (1-3)", type=int, default=1)
|
|
527
|
+
except typer.Abort:
|
|
528
|
+
console.print("[yellow]Cancelled.[/yellow]")
|
|
529
|
+
return False
|
|
530
|
+
|
|
531
|
+
if choice == 1:
|
|
532
|
+
# Re-enter configuration
|
|
533
|
+
# Check BEFORE increment to fix off-by-one error
|
|
534
|
+
if retry_count >= max_retries:
|
|
535
|
+
console.print(
|
|
536
|
+
f"[red]Maximum retry attempts ({max_retries}) reached.[/red]"
|
|
537
|
+
)
|
|
538
|
+
console.print(
|
|
539
|
+
"[yellow]Please fix configuration manually and run 'mcp-ticketer doctor'[/yellow]"
|
|
540
|
+
)
|
|
541
|
+
return False
|
|
542
|
+
retry_count += 1
|
|
543
|
+
|
|
544
|
+
console.print(
|
|
545
|
+
f"\n[cyan]Retry {retry_count}/{max_retries} - Re-entering configuration...[/cyan]"
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
# Reload current config to get values
|
|
549
|
+
import json
|
|
550
|
+
|
|
551
|
+
with open(config_file_path) as f:
|
|
552
|
+
current_config = json.load(f)
|
|
553
|
+
|
|
554
|
+
# Re-prompt for adapter-specific configuration
|
|
555
|
+
if adapter_type == "linear":
|
|
556
|
+
console.print("\n[bold]Linear Configuration[/bold]")
|
|
557
|
+
console.print(
|
|
558
|
+
"[dim]Get your API key at: https://linear.app/settings/api[/dim]\n"
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
linear_api_key = typer.prompt(
|
|
562
|
+
"Enter your Linear API key", hide_input=True
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
console.print("\n[bold]Linear Team Configuration[/bold]")
|
|
566
|
+
console.print("You can provide either:")
|
|
567
|
+
console.print(
|
|
568
|
+
" 1. Team URL (e.g., https://linear.app/workspace/team/TEAMKEY/active)"
|
|
569
|
+
)
|
|
570
|
+
console.print(" 2. Team key (e.g., 'ENG', 'DESIGN', 'PRODUCT')")
|
|
571
|
+
console.print(" 3. Team ID (UUID)")
|
|
572
|
+
console.print(
|
|
573
|
+
"[dim]Find team URL or key in: Linear → Your Team → Team Issues Page[/dim]\n"
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
team_input = typer.prompt("Team URL, key, or ID")
|
|
577
|
+
|
|
578
|
+
# Check if input is a URL
|
|
579
|
+
linear_team_id = None
|
|
580
|
+
linear_team_key = None
|
|
581
|
+
|
|
582
|
+
if team_input.startswith("https://linear.app/"):
|
|
583
|
+
console.print("[cyan]Detected team URL, deriving team ID...[/cyan]")
|
|
584
|
+
from .linear_commands import derive_team_from_url
|
|
585
|
+
|
|
586
|
+
derived_team_id, error = await derive_team_from_url(
|
|
587
|
+
linear_api_key, team_input
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
if derived_team_id:
|
|
591
|
+
linear_team_id = derived_team_id
|
|
592
|
+
console.print(
|
|
593
|
+
"[green]✓[/green] Successfully derived team ID from URL"
|
|
594
|
+
)
|
|
595
|
+
else:
|
|
596
|
+
console.print(f"[red]Error:[/red] {error}")
|
|
597
|
+
console.print("Please provide team key or ID manually instead.")
|
|
598
|
+
team_input = typer.prompt("Team key or ID")
|
|
599
|
+
|
|
600
|
+
if len(team_input) > 20: # Likely a UUID
|
|
601
|
+
linear_team_id = team_input
|
|
602
|
+
else:
|
|
603
|
+
linear_team_key = team_input
|
|
604
|
+
else:
|
|
605
|
+
# Input is team key or ID
|
|
606
|
+
if len(team_input) > 20: # Likely a UUID
|
|
607
|
+
linear_team_id = team_input
|
|
608
|
+
else:
|
|
609
|
+
linear_team_key = team_input
|
|
610
|
+
|
|
611
|
+
# Update config
|
|
612
|
+
linear_config = {
|
|
613
|
+
"api_key": linear_api_key,
|
|
614
|
+
"type": "linear",
|
|
615
|
+
}
|
|
616
|
+
if linear_team_key:
|
|
617
|
+
linear_config["team_key"] = linear_team_key
|
|
618
|
+
if linear_team_id:
|
|
619
|
+
linear_config["team_id"] = linear_team_id
|
|
620
|
+
|
|
621
|
+
current_config["adapters"]["linear"] = linear_config
|
|
622
|
+
|
|
623
|
+
elif adapter_type == "jira":
|
|
624
|
+
console.print("\n[bold]JIRA Configuration[/bold]")
|
|
625
|
+
console.print("Enter your JIRA server details.\n")
|
|
626
|
+
|
|
627
|
+
server = typer.prompt(
|
|
628
|
+
"JIRA server URL (e.g., https://company.atlassian.net)"
|
|
629
|
+
)
|
|
630
|
+
email = typer.prompt("Your JIRA email address")
|
|
631
|
+
|
|
632
|
+
console.print("\nYou need a JIRA API token.")
|
|
633
|
+
console.print(
|
|
634
|
+
"[dim]Generate one at: https://id.atlassian.com/manage/api-tokens[/dim]\n"
|
|
635
|
+
)
|
|
636
|
+
|
|
637
|
+
token = typer.prompt("Enter your JIRA API token", hide_input=True)
|
|
638
|
+
|
|
639
|
+
project = typer.prompt(
|
|
640
|
+
"Default JIRA project key (optional, press Enter to skip)",
|
|
641
|
+
default="",
|
|
642
|
+
show_default=False,
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
# Update config
|
|
646
|
+
jira_config = {
|
|
647
|
+
"server": server,
|
|
648
|
+
"email": email,
|
|
649
|
+
"api_token": token,
|
|
650
|
+
"type": "jira",
|
|
651
|
+
}
|
|
652
|
+
if project:
|
|
653
|
+
jira_config["project_key"] = project
|
|
654
|
+
|
|
655
|
+
current_config["adapters"]["jira"] = jira_config
|
|
656
|
+
|
|
657
|
+
elif adapter_type == "github":
|
|
658
|
+
console.print("\n[bold]GitHub Configuration[/bold]")
|
|
659
|
+
console.print("Enter your GitHub repository details.\n")
|
|
660
|
+
|
|
661
|
+
owner = typer.prompt(
|
|
662
|
+
"GitHub repository owner (username or organization)"
|
|
663
|
+
)
|
|
664
|
+
repo = typer.prompt("GitHub repository name")
|
|
665
|
+
|
|
666
|
+
console.print("\nYou need a GitHub Personal Access Token.")
|
|
667
|
+
console.print(
|
|
668
|
+
"[dim]Create one at: https://github.com/settings/tokens/new[/dim]"
|
|
669
|
+
)
|
|
670
|
+
console.print(
|
|
671
|
+
"[dim]Required scopes: repo (for private repos) or public_repo (for public repos)[/dim]\n"
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
token = typer.prompt(
|
|
675
|
+
"Enter your GitHub Personal Access Token", hide_input=True
|
|
676
|
+
)
|
|
677
|
+
|
|
678
|
+
# Update config
|
|
679
|
+
current_config["adapters"]["github"] = {
|
|
680
|
+
"owner": owner,
|
|
681
|
+
"repo": repo,
|
|
682
|
+
"token": token,
|
|
683
|
+
"type": "github",
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
elif adapter_type == "aitrackdown":
|
|
687
|
+
# AITrackdown doesn't need credentials, but save config before returning
|
|
688
|
+
# Save updated configuration
|
|
689
|
+
with open(config_file_path, "w") as f:
|
|
690
|
+
json.dump(current_config, f, indent=2)
|
|
691
|
+
|
|
692
|
+
console.print(
|
|
693
|
+
"[yellow]AITrackdown doesn't require credentials. Continuing...[/yellow]"
|
|
694
|
+
)
|
|
695
|
+
console.print("[dim]✓ Configuration updated[/dim]")
|
|
696
|
+
return True
|
|
697
|
+
|
|
698
|
+
else:
|
|
699
|
+
console.print(f"[red]Unknown adapter type: {adapter_type}[/red]")
|
|
700
|
+
return False
|
|
701
|
+
|
|
702
|
+
# Save updated configuration
|
|
703
|
+
with open(config_file_path, "w") as f:
|
|
704
|
+
json.dump(current_config, f, indent=2)
|
|
705
|
+
|
|
706
|
+
console.print("[dim]✓ Configuration updated[/dim]")
|
|
707
|
+
# Loop will retry validation
|
|
708
|
+
|
|
709
|
+
elif choice == 2:
|
|
710
|
+
# Continue anyway
|
|
711
|
+
console.print(
|
|
712
|
+
"[yellow]⚠️ Continuing with potentially invalid configuration.[/yellow]"
|
|
713
|
+
)
|
|
714
|
+
console.print("[dim]You can validate later with: mcp-ticketer doctor[/dim]")
|
|
715
|
+
return True
|
|
716
|
+
|
|
717
|
+
elif choice == 3:
|
|
718
|
+
# Exit
|
|
719
|
+
console.print(
|
|
720
|
+
"[yellow]Configuration saved but not validated. Run 'mcp-ticketer doctor' to test.[/yellow]"
|
|
721
|
+
)
|
|
722
|
+
return False
|
|
723
|
+
|
|
724
|
+
else:
|
|
725
|
+
console.print(
|
|
726
|
+
f"[red]Invalid choice: {choice}. Please enter 1, 2, or 3.[/red]"
|
|
727
|
+
)
|
|
728
|
+
# Continue loop to ask again
|
|
729
|
+
|
|
730
|
+
return True
|
|
731
|
+
|
|
732
|
+
|
|
321
733
|
def _prompt_for_adapter_selection(console: Console) -> str:
|
|
322
734
|
"""Interactive prompt for adapter selection.
|
|
323
735
|
|
|
@@ -381,172 +793,442 @@ def _prompt_for_adapter_selection(console: Console) -> str:
|
|
|
381
793
|
)
|
|
382
794
|
except (ValueError, typer.Abort):
|
|
383
795
|
console.print("[yellow]Setup cancelled.[/yellow]")
|
|
384
|
-
raise typer.Exit(0)
|
|
796
|
+
raise typer.Exit(0) from None
|
|
385
797
|
|
|
386
798
|
|
|
387
799
|
@app.command()
|
|
388
800
|
def setup(
|
|
389
|
-
|
|
390
|
-
None,
|
|
391
|
-
"--adapter",
|
|
392
|
-
"-a",
|
|
393
|
-
help="Adapter type to use (interactive prompt if not specified)",
|
|
394
|
-
),
|
|
395
|
-
project_path: Optional[str] = typer.Option(
|
|
801
|
+
project_path: str | None = typer.Option(
|
|
396
802
|
None, "--path", help="Project path (default: current directory)"
|
|
397
803
|
),
|
|
398
|
-
|
|
804
|
+
skip_platforms: bool = typer.Option(
|
|
399
805
|
False,
|
|
400
|
-
"--
|
|
401
|
-
"
|
|
402
|
-
help="Save to global config instead of project-specific",
|
|
403
|
-
),
|
|
404
|
-
base_path: Optional[str] = typer.Option(
|
|
405
|
-
None,
|
|
406
|
-
"--base-path",
|
|
407
|
-
"-p",
|
|
408
|
-
help="Base path for ticket storage (AITrackdown only)",
|
|
409
|
-
),
|
|
410
|
-
api_key: Optional[str] = typer.Option(
|
|
411
|
-
None, "--api-key", help="API key for Linear or API token for JIRA"
|
|
412
|
-
),
|
|
413
|
-
team_id: Optional[str] = typer.Option(
|
|
414
|
-
None, "--team-id", help="Linear team ID (required for Linear adapter)"
|
|
415
|
-
),
|
|
416
|
-
jira_server: Optional[str] = typer.Option(
|
|
417
|
-
None,
|
|
418
|
-
"--jira-server",
|
|
419
|
-
help="JIRA server URL (e.g., https://company.atlassian.net)",
|
|
420
|
-
),
|
|
421
|
-
jira_email: Optional[str] = typer.Option(
|
|
422
|
-
None, "--jira-email", help="JIRA user email for authentication"
|
|
423
|
-
),
|
|
424
|
-
jira_project: Optional[str] = typer.Option(
|
|
425
|
-
None, "--jira-project", help="Default JIRA project key"
|
|
426
|
-
),
|
|
427
|
-
github_owner: Optional[str] = typer.Option(
|
|
428
|
-
None, "--github-owner", help="GitHub repository owner"
|
|
429
|
-
),
|
|
430
|
-
github_repo: Optional[str] = typer.Option(
|
|
431
|
-
None, "--github-repo", help="GitHub repository name"
|
|
806
|
+
"--skip-platforms",
|
|
807
|
+
help="Skip platform installation (only initialize adapter)",
|
|
432
808
|
),
|
|
433
|
-
|
|
434
|
-
|
|
809
|
+
force_reinit: bool = typer.Option(
|
|
810
|
+
False,
|
|
811
|
+
"--force-reinit",
|
|
812
|
+
help="Force re-initialization even if config exists",
|
|
435
813
|
),
|
|
436
814
|
) -> None:
|
|
437
|
-
"""
|
|
815
|
+
"""Smart setup command - combines init + platform installation.
|
|
816
|
+
|
|
817
|
+
This command intelligently detects your current setup state and only
|
|
818
|
+
performs necessary configuration. It's the recommended way to get started.
|
|
438
819
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
820
|
+
Detection & Smart Actions:
|
|
821
|
+
- First run: Full setup (init + platform installation)
|
|
822
|
+
- Existing config: Skip init, offer platform installation
|
|
823
|
+
- Detects changes: Offers to update configurations
|
|
824
|
+
- Respects existing: Won't overwrite without confirmation
|
|
442
825
|
|
|
443
826
|
Examples:
|
|
444
|
-
#
|
|
827
|
+
# Smart setup (recommended for first-time setup)
|
|
445
828
|
mcp-ticketer setup
|
|
446
829
|
|
|
447
|
-
# Setup with specific adapter
|
|
448
|
-
mcp-ticketer setup --adapter linear
|
|
449
|
-
|
|
450
830
|
# Setup for different project
|
|
451
831
|
mcp-ticketer setup --path /path/to/project
|
|
452
832
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
init(
|
|
456
|
-
adapter=adapter,
|
|
457
|
-
project_path=project_path,
|
|
458
|
-
global_config=global_config,
|
|
459
|
-
base_path=base_path,
|
|
460
|
-
api_key=api_key,
|
|
461
|
-
team_id=team_id,
|
|
462
|
-
jira_server=jira_server,
|
|
463
|
-
jira_email=jira_email,
|
|
464
|
-
jira_project=jira_project,
|
|
465
|
-
github_owner=github_owner,
|
|
466
|
-
github_repo=github_repo,
|
|
467
|
-
github_token=github_token,
|
|
468
|
-
)
|
|
833
|
+
# Re-initialize configuration
|
|
834
|
+
mcp-ticketer setup --force-reinit
|
|
469
835
|
|
|
836
|
+
# Only init adapter, skip platform installation
|
|
837
|
+
mcp-ticketer setup --skip-platforms
|
|
470
838
|
|
|
471
|
-
|
|
472
|
-
def init(
|
|
473
|
-
adapter: Optional[str] = typer.Option(
|
|
474
|
-
None,
|
|
475
|
-
"--adapter",
|
|
476
|
-
"-a",
|
|
477
|
-
help="Adapter type to use (interactive prompt if not specified)",
|
|
478
|
-
),
|
|
479
|
-
project_path: Optional[str] = typer.Option(
|
|
480
|
-
None, "--path", help="Project path (default: current directory)"
|
|
481
|
-
),
|
|
482
|
-
global_config: bool = typer.Option(
|
|
483
|
-
False,
|
|
484
|
-
"--global",
|
|
485
|
-
"-g",
|
|
486
|
-
help="Save to global config instead of project-specific",
|
|
487
|
-
),
|
|
488
|
-
base_path: Optional[str] = typer.Option(
|
|
489
|
-
None,
|
|
490
|
-
"--base-path",
|
|
491
|
-
"-p",
|
|
492
|
-
help="Base path for ticket storage (AITrackdown only)",
|
|
493
|
-
),
|
|
494
|
-
api_key: Optional[str] = typer.Option(
|
|
495
|
-
None, "--api-key", help="API key for Linear or API token for JIRA"
|
|
496
|
-
),
|
|
497
|
-
team_id: Optional[str] = typer.Option(
|
|
498
|
-
None, "--team-id", help="Linear team ID (required for Linear adapter)"
|
|
499
|
-
),
|
|
500
|
-
jira_server: Optional[str] = typer.Option(
|
|
501
|
-
None,
|
|
502
|
-
"--jira-server",
|
|
503
|
-
help="JIRA server URL (e.g., https://company.atlassian.net)",
|
|
504
|
-
),
|
|
505
|
-
jira_email: Optional[str] = typer.Option(
|
|
506
|
-
None, "--jira-email", help="JIRA user email for authentication"
|
|
507
|
-
),
|
|
508
|
-
jira_project: Optional[str] = typer.Option(
|
|
509
|
-
None, "--jira-project", help="Default JIRA project key"
|
|
510
|
-
),
|
|
511
|
-
github_owner: Optional[str] = typer.Option(
|
|
512
|
-
None, "--github-owner", help="GitHub repository owner"
|
|
513
|
-
),
|
|
514
|
-
github_repo: Optional[str] = typer.Option(
|
|
515
|
-
None, "--github-repo", help="GitHub repository name"
|
|
516
|
-
),
|
|
517
|
-
github_token: Optional[str] = typer.Option(
|
|
518
|
-
None, "--github-token", help="GitHub Personal Access Token"
|
|
519
|
-
),
|
|
520
|
-
) -> None:
|
|
521
|
-
"""Initialize mcp-ticketer for the current project.
|
|
839
|
+
Note: For advanced configuration, use 'init' and 'install' separately.
|
|
522
840
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
from .env files or prompts for interactive setup if no configuration is found.
|
|
841
|
+
"""
|
|
842
|
+
from .platform_detection import PlatformDetector
|
|
526
843
|
|
|
527
|
-
|
|
528
|
-
|
|
844
|
+
proj_path = Path(project_path) if project_path else Path.cwd()
|
|
845
|
+
config_path = proj_path / ".mcp-ticketer" / "config.json"
|
|
529
846
|
|
|
530
|
-
|
|
847
|
+
console.print("[bold cyan]🚀 MCP Ticketer Smart Setup[/bold cyan]\n")
|
|
531
848
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
849
|
+
# Step 1: Detect existing configuration
|
|
850
|
+
config_exists = config_path.exists()
|
|
851
|
+
config_valid = False
|
|
852
|
+
current_adapter = None
|
|
535
853
|
|
|
536
|
-
|
|
854
|
+
if config_exists and not force_reinit:
|
|
855
|
+
try:
|
|
856
|
+
with open(config_path) as f:
|
|
857
|
+
config = json.load(f)
|
|
858
|
+
current_adapter = config.get("default_adapter")
|
|
859
|
+
config_valid = bool(current_adapter and config.get("adapters"))
|
|
860
|
+
except (json.JSONDecodeError, OSError):
|
|
861
|
+
config_valid = False
|
|
862
|
+
|
|
863
|
+
if config_valid:
|
|
864
|
+
console.print("[green]✓[/green] Configuration detected")
|
|
865
|
+
console.print(f"[dim] Adapter: {current_adapter}[/dim]")
|
|
866
|
+
console.print(f"[dim] Location: {config_path}[/dim]\n")
|
|
867
|
+
|
|
868
|
+
# Offer to reconfigure
|
|
869
|
+
if not typer.confirm(
|
|
870
|
+
"Configuration already exists. Keep existing settings?", default=True
|
|
871
|
+
):
|
|
872
|
+
console.print("[cyan]Re-initializing configuration...[/cyan]\n")
|
|
873
|
+
force_reinit = True
|
|
874
|
+
config_valid = False
|
|
875
|
+
else:
|
|
876
|
+
if config_exists:
|
|
877
|
+
console.print(
|
|
878
|
+
"[yellow]⚠[/yellow] Configuration file exists but is invalid\n"
|
|
879
|
+
)
|
|
880
|
+
else:
|
|
881
|
+
console.print("[yellow]⚠[/yellow] No configuration found\n")
|
|
882
|
+
|
|
883
|
+
# Step 2: Initialize adapter configuration if needed
|
|
884
|
+
if not config_valid or force_reinit:
|
|
885
|
+
console.print("[bold]Step 1/2: Adapter Configuration[/bold]\n")
|
|
886
|
+
|
|
887
|
+
# Run init command non-interactively through function call
|
|
888
|
+
# We'll use the discover and prompt flow from init
|
|
889
|
+
from ..core.env_discovery import discover_config
|
|
890
|
+
|
|
891
|
+
discovered = discover_config(proj_path)
|
|
892
|
+
adapter_type = None
|
|
893
|
+
|
|
894
|
+
# Try auto-discovery
|
|
895
|
+
if discovered and discovered.adapters:
|
|
896
|
+
primary = discovered.get_primary_adapter()
|
|
897
|
+
if primary:
|
|
898
|
+
adapter_type = primary.adapter_type
|
|
899
|
+
console.print(f"[green]✓ Auto-detected {adapter_type} adapter[/green]")
|
|
900
|
+
console.print(f"[dim] Source: {primary.found_in}[/dim]")
|
|
901
|
+
console.print(f"[dim] Confidence: {primary.confidence:.0%}[/dim]\n")
|
|
902
|
+
|
|
903
|
+
if not typer.confirm(
|
|
904
|
+
f"Use detected {adapter_type} adapter?", default=True
|
|
905
|
+
):
|
|
906
|
+
adapter_type = None
|
|
907
|
+
|
|
908
|
+
# If no adapter detected, prompt for selection
|
|
909
|
+
if not adapter_type:
|
|
910
|
+
adapter_type = _prompt_for_adapter_selection(console)
|
|
911
|
+
|
|
912
|
+
# Now run the full init with the selected adapter
|
|
913
|
+
console.print(f"\n[cyan]Initializing {adapter_type} adapter...[/cyan]\n")
|
|
914
|
+
|
|
915
|
+
# Call init programmatically
|
|
916
|
+
init(
|
|
917
|
+
adapter=adapter_type,
|
|
918
|
+
project_path=str(proj_path),
|
|
919
|
+
global_config=False,
|
|
920
|
+
)
|
|
921
|
+
|
|
922
|
+
console.print("\n[green]✓ Adapter configuration complete[/green]\n")
|
|
923
|
+
else:
|
|
924
|
+
console.print("[green]✓ Step 1/2: Adapter already configured[/green]\n")
|
|
925
|
+
|
|
926
|
+
# Step 3: Platform installation
|
|
927
|
+
if skip_platforms:
|
|
928
|
+
console.print(
|
|
929
|
+
"[yellow]⚠[/yellow] Skipping platform installation (--skip-platforms)\n"
|
|
930
|
+
)
|
|
931
|
+
_show_setup_complete_message(console, proj_path)
|
|
932
|
+
return
|
|
933
|
+
|
|
934
|
+
console.print("[bold]Step 2/2: Platform Installation[/bold]\n")
|
|
935
|
+
|
|
936
|
+
# Detect available platforms
|
|
937
|
+
detector = PlatformDetector()
|
|
938
|
+
detected = detector.detect_all(project_path=proj_path)
|
|
939
|
+
|
|
940
|
+
if not detected:
|
|
941
|
+
console.print("[yellow]No AI platforms detected on this system.[/yellow]")
|
|
942
|
+
console.print(
|
|
943
|
+
"\n[dim]Supported platforms: Claude Code, Claude Desktop, Gemini, Codex, Auggie[/dim]"
|
|
944
|
+
)
|
|
945
|
+
console.print(
|
|
946
|
+
"[dim]Install these platforms to use them with mcp-ticketer.[/dim]\n"
|
|
947
|
+
)
|
|
948
|
+
_show_setup_complete_message(console, proj_path)
|
|
949
|
+
return
|
|
950
|
+
|
|
951
|
+
# Filter to only installed platforms
|
|
952
|
+
installed = [p for p in detected if p.is_installed]
|
|
953
|
+
|
|
954
|
+
if not installed:
|
|
955
|
+
console.print(
|
|
956
|
+
"[yellow]AI platforms detected but have configuration issues.[/yellow]"
|
|
957
|
+
)
|
|
958
|
+
console.print(
|
|
959
|
+
"\n[dim]Run 'mcp-ticketer install --auto-detect' for details.[/dim]\n"
|
|
960
|
+
)
|
|
961
|
+
_show_setup_complete_message(console, proj_path)
|
|
962
|
+
return
|
|
963
|
+
|
|
964
|
+
# Show detected platforms
|
|
965
|
+
console.print(f"[green]✓[/green] Detected {len(installed)} platform(s):\n")
|
|
966
|
+
for plat in installed:
|
|
967
|
+
console.print(f" • {plat.display_name} ({plat.scope})")
|
|
968
|
+
|
|
969
|
+
console.print()
|
|
970
|
+
|
|
971
|
+
# Check if mcp-ticketer is already configured for these platforms
|
|
972
|
+
already_configured = _check_existing_platform_configs(installed, proj_path)
|
|
973
|
+
|
|
974
|
+
if already_configured:
|
|
975
|
+
console.print(
|
|
976
|
+
f"[green]✓[/green] mcp-ticketer already configured for {len(already_configured)} platform(s)\n"
|
|
977
|
+
)
|
|
978
|
+
for plat_name in already_configured:
|
|
979
|
+
console.print(f" • {plat_name}")
|
|
980
|
+
console.print()
|
|
981
|
+
|
|
982
|
+
if not typer.confirm("Update platform configurations anyway?", default=False):
|
|
983
|
+
console.print("[yellow]Skipping platform installation[/yellow]\n")
|
|
984
|
+
_show_setup_complete_message(console, proj_path)
|
|
985
|
+
return
|
|
986
|
+
|
|
987
|
+
# Offer to install for all or select specific
|
|
988
|
+
console.print("[bold]Platform Installation Options:[/bold]")
|
|
989
|
+
console.print("1. Install for all detected platforms")
|
|
990
|
+
console.print("2. Select specific platform")
|
|
991
|
+
console.print("3. Skip platform installation")
|
|
992
|
+
|
|
993
|
+
try:
|
|
994
|
+
choice = typer.prompt("\nSelect option (1-3)", type=int, default=1)
|
|
995
|
+
except typer.Abort:
|
|
996
|
+
console.print("[yellow]Setup cancelled[/yellow]")
|
|
997
|
+
raise typer.Exit(0) from None
|
|
998
|
+
|
|
999
|
+
if choice == 3:
|
|
1000
|
+
console.print("[yellow]Skipping platform installation[/yellow]\n")
|
|
1001
|
+
_show_setup_complete_message(console, proj_path)
|
|
1002
|
+
return
|
|
1003
|
+
|
|
1004
|
+
# Import configuration functions
|
|
1005
|
+
from .auggie_configure import configure_auggie_mcp
|
|
1006
|
+
from .codex_configure import configure_codex_mcp
|
|
1007
|
+
from .gemini_configure import configure_gemini_mcp
|
|
1008
|
+
from .mcp_configure import configure_claude_mcp
|
|
1009
|
+
|
|
1010
|
+
platform_mapping = {
|
|
1011
|
+
"claude-code": lambda: configure_claude_mcp(global_config=False, force=True),
|
|
1012
|
+
"claude-desktop": lambda: configure_claude_mcp(global_config=True, force=True),
|
|
1013
|
+
"auggie": lambda: configure_auggie_mcp(force=True),
|
|
1014
|
+
"gemini": lambda: configure_gemini_mcp(scope="project", force=True),
|
|
1015
|
+
"codex": lambda: configure_codex_mcp(force=True),
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
platforms_to_install = []
|
|
1019
|
+
|
|
1020
|
+
if choice == 1:
|
|
1021
|
+
# Install for all
|
|
1022
|
+
platforms_to_install = installed
|
|
1023
|
+
elif choice == 2:
|
|
1024
|
+
# Select specific platform
|
|
1025
|
+
console.print("\n[bold]Select platform:[/bold]")
|
|
1026
|
+
for idx, plat in enumerate(installed, 1):
|
|
1027
|
+
console.print(f" {idx}. {plat.display_name} ({plat.scope})")
|
|
1028
|
+
|
|
1029
|
+
try:
|
|
1030
|
+
plat_choice = typer.prompt("\nSelect platform number", type=int)
|
|
1031
|
+
if 1 <= plat_choice <= len(installed):
|
|
1032
|
+
platforms_to_install = [installed[plat_choice - 1]]
|
|
1033
|
+
else:
|
|
1034
|
+
console.print("[red]Invalid selection[/red]")
|
|
1035
|
+
raise typer.Exit(1) from None
|
|
1036
|
+
except typer.Abort:
|
|
1037
|
+
console.print("[yellow]Setup cancelled[/yellow]")
|
|
1038
|
+
raise typer.Exit(0) from None
|
|
1039
|
+
|
|
1040
|
+
# Install for selected platforms
|
|
1041
|
+
console.print()
|
|
1042
|
+
success_count = 0
|
|
1043
|
+
failed = []
|
|
1044
|
+
|
|
1045
|
+
for plat in platforms_to_install:
|
|
1046
|
+
config_func = platform_mapping.get(plat.name)
|
|
1047
|
+
if not config_func:
|
|
1048
|
+
console.print(f"[yellow]⚠[/yellow] No installer for {plat.display_name}")
|
|
1049
|
+
continue
|
|
1050
|
+
|
|
1051
|
+
try:
|
|
1052
|
+
console.print(f"[cyan]Installing for {plat.display_name}...[/cyan]")
|
|
1053
|
+
config_func()
|
|
1054
|
+
console.print(f"[green]✓[/green] {plat.display_name} configured\n")
|
|
1055
|
+
success_count += 1
|
|
1056
|
+
except Exception as e:
|
|
1057
|
+
console.print(
|
|
1058
|
+
f"[red]✗[/red] Failed to configure {plat.display_name}: {e}\n"
|
|
1059
|
+
)
|
|
1060
|
+
failed.append(plat.display_name)
|
|
1061
|
+
|
|
1062
|
+
# Summary
|
|
1063
|
+
console.print(
|
|
1064
|
+
f"[bold]Platform Installation:[/bold] {success_count}/{len(platforms_to_install)} succeeded"
|
|
1065
|
+
)
|
|
1066
|
+
if failed:
|
|
1067
|
+
console.print(f"[red]Failed:[/red] {', '.join(failed)}")
|
|
1068
|
+
|
|
1069
|
+
console.print()
|
|
1070
|
+
_show_setup_complete_message(console, proj_path)
|
|
1071
|
+
|
|
1072
|
+
|
|
1073
|
+
def _check_existing_platform_configs(platforms: list, proj_path: Path) -> list[str]:
|
|
1074
|
+
"""Check if mcp-ticketer is already configured for given platforms.
|
|
1075
|
+
|
|
1076
|
+
Args:
|
|
1077
|
+
platforms: List of DetectedPlatform objects
|
|
1078
|
+
proj_path: Project path
|
|
1079
|
+
|
|
1080
|
+
Returns:
|
|
1081
|
+
List of platform display names that are already configured
|
|
1082
|
+
|
|
1083
|
+
"""
|
|
1084
|
+
configured = []
|
|
1085
|
+
|
|
1086
|
+
for plat in platforms:
|
|
1087
|
+
try:
|
|
1088
|
+
if plat.name == "claude-code":
|
|
1089
|
+
config_path = Path.home() / ".claude.json"
|
|
1090
|
+
if config_path.exists():
|
|
1091
|
+
with open(config_path) as f:
|
|
1092
|
+
config = json.load(f)
|
|
1093
|
+
projects = config.get("projects", {})
|
|
1094
|
+
proj_key = str(proj_path)
|
|
1095
|
+
if proj_key in projects:
|
|
1096
|
+
mcp_servers = projects[proj_key].get("mcpServers", {})
|
|
1097
|
+
if "mcp-ticketer" in mcp_servers:
|
|
1098
|
+
configured.append(plat.display_name)
|
|
1099
|
+
|
|
1100
|
+
elif plat.name == "claude-desktop":
|
|
1101
|
+
if plat.config_path.exists():
|
|
1102
|
+
with open(plat.config_path) as f:
|
|
1103
|
+
config = json.load(f)
|
|
1104
|
+
if "mcp-ticketer" in config.get("mcpServers", {}):
|
|
1105
|
+
configured.append(plat.display_name)
|
|
1106
|
+
|
|
1107
|
+
elif plat.name in ["auggie", "codex", "gemini"]:
|
|
1108
|
+
if plat.config_path.exists():
|
|
1109
|
+
# Check if mcp-ticketer is configured
|
|
1110
|
+
# Implementation depends on each platform's config format
|
|
1111
|
+
# For now, just check if config exists (simplified)
|
|
1112
|
+
pass
|
|
1113
|
+
|
|
1114
|
+
except (json.JSONDecodeError, OSError):
|
|
1115
|
+
pass
|
|
1116
|
+
|
|
1117
|
+
return configured
|
|
1118
|
+
|
|
1119
|
+
|
|
1120
|
+
def _show_setup_complete_message(console: Console, proj_path: Path) -> None:
|
|
1121
|
+
"""Show setup complete message with next steps.
|
|
1122
|
+
|
|
1123
|
+
Args:
|
|
1124
|
+
console: Rich console for output
|
|
1125
|
+
proj_path: Project path
|
|
1126
|
+
|
|
1127
|
+
"""
|
|
1128
|
+
console.print("[bold green]🎉 Setup Complete![/bold green]\n")
|
|
1129
|
+
|
|
1130
|
+
console.print("[bold]Quick Start:[/bold]")
|
|
1131
|
+
console.print("1. Create a test ticket:")
|
|
1132
|
+
console.print(" [cyan]mcp-ticketer create 'My first ticket'[/cyan]\n")
|
|
1133
|
+
|
|
1134
|
+
console.print("2. List tickets:")
|
|
1135
|
+
console.print(" [cyan]mcp-ticketer list[/cyan]\n")
|
|
1136
|
+
|
|
1137
|
+
console.print("[bold]Useful Commands:[/bold]")
|
|
1138
|
+
console.print(" [cyan]mcp-ticketer doctor[/cyan] - Validate configuration")
|
|
1139
|
+
console.print(" [cyan]mcp-ticketer install <platform>[/cyan] - Add more platforms")
|
|
1140
|
+
console.print(" [cyan]mcp-ticketer --help[/cyan] - See all commands\n")
|
|
1141
|
+
|
|
1142
|
+
console.print(
|
|
1143
|
+
f"[dim]Configuration: {proj_path / '.mcp-ticketer' / 'config.json'}[/dim]"
|
|
1144
|
+
)
|
|
1145
|
+
|
|
1146
|
+
|
|
1147
|
+
@app.command()
|
|
1148
|
+
def init(
|
|
1149
|
+
adapter: str | None = typer.Option(
|
|
1150
|
+
None,
|
|
1151
|
+
"--adapter",
|
|
1152
|
+
"-a",
|
|
1153
|
+
help="Adapter type to use (interactive prompt if not specified)",
|
|
1154
|
+
),
|
|
1155
|
+
project_path: str | None = typer.Option(
|
|
1156
|
+
None, "--path", help="Project path (default: current directory)"
|
|
1157
|
+
),
|
|
1158
|
+
global_config: bool = typer.Option(
|
|
1159
|
+
False,
|
|
1160
|
+
"--global",
|
|
1161
|
+
"-g",
|
|
1162
|
+
help="Save to global config instead of project-specific",
|
|
1163
|
+
),
|
|
1164
|
+
base_path: str | None = typer.Option(
|
|
1165
|
+
None,
|
|
1166
|
+
"--base-path",
|
|
1167
|
+
"-p",
|
|
1168
|
+
help="Base path for ticket storage (AITrackdown only)",
|
|
1169
|
+
),
|
|
1170
|
+
api_key: str | None = typer.Option(
|
|
1171
|
+
None, "--api-key", help="API key for Linear or API token for JIRA"
|
|
1172
|
+
),
|
|
1173
|
+
team_id: str | None = typer.Option(
|
|
1174
|
+
None, "--team-id", help="Linear team ID (required for Linear adapter)"
|
|
1175
|
+
),
|
|
1176
|
+
jira_server: str | None = typer.Option(
|
|
1177
|
+
None,
|
|
1178
|
+
"--jira-server",
|
|
1179
|
+
help="JIRA server URL (e.g., https://company.atlassian.net)",
|
|
1180
|
+
),
|
|
1181
|
+
jira_email: str | None = typer.Option(
|
|
1182
|
+
None, "--jira-email", help="JIRA user email for authentication"
|
|
1183
|
+
),
|
|
1184
|
+
jira_project: str | None = typer.Option(
|
|
1185
|
+
None, "--jira-project", help="Default JIRA project key"
|
|
1186
|
+
),
|
|
1187
|
+
github_owner: str | None = typer.Option(
|
|
1188
|
+
None, "--github-owner", help="GitHub repository owner"
|
|
1189
|
+
),
|
|
1190
|
+
github_repo: str | None = typer.Option(
|
|
1191
|
+
None, "--github-repo", help="GitHub repository name"
|
|
1192
|
+
),
|
|
1193
|
+
github_token: str | None = typer.Option(
|
|
1194
|
+
None, "--github-token", help="GitHub Personal Access Token"
|
|
1195
|
+
),
|
|
1196
|
+
) -> None:
|
|
1197
|
+
"""Initialize adapter configuration only (without platform installation).
|
|
1198
|
+
|
|
1199
|
+
This command sets up adapter configuration with interactive prompts.
|
|
1200
|
+
It auto-detects adapter configuration from .env files or prompts for
|
|
1201
|
+
interactive setup if no configuration is found.
|
|
1202
|
+
|
|
1203
|
+
Creates .mcp-ticketer/config.json in the current directory.
|
|
1204
|
+
|
|
1205
|
+
RECOMMENDED: Use 'mcp-ticketer setup' instead for a complete setup
|
|
1206
|
+
experience that includes both adapter configuration and platform
|
|
1207
|
+
installation in one command.
|
|
1208
|
+
|
|
1209
|
+
The init command automatically validates your configuration after setup:
|
|
1210
|
+
- If validation passes, setup completes
|
|
1211
|
+
- If issues are detected, you can re-enter credentials, continue anyway, or exit
|
|
1212
|
+
- You get up to 3 retry attempts to fix configuration issues
|
|
1213
|
+
- You can always re-validate later with 'mcp-ticketer doctor'
|
|
1214
|
+
|
|
1215
|
+
Examples:
|
|
1216
|
+
# For first-time setup, use 'setup' instead (recommended)
|
|
1217
|
+
mcp-ticketer setup
|
|
1218
|
+
|
|
1219
|
+
# Initialize adapter only (advanced usage)
|
|
1220
|
+
mcp-ticketer init
|
|
1221
|
+
|
|
1222
|
+
# Force specific adapter
|
|
537
1223
|
mcp-ticketer init --adapter linear
|
|
538
1224
|
|
|
539
1225
|
# Initialize for different project
|
|
540
1226
|
mcp-ticketer init --path /path/to/project
|
|
541
1227
|
|
|
542
|
-
# Save globally (not recommended)
|
|
543
|
-
mcp-ticketer init --global
|
|
544
|
-
|
|
545
1228
|
"""
|
|
546
1229
|
from pathlib import Path
|
|
547
1230
|
|
|
548
1231
|
from ..core.env_discovery import discover_config
|
|
549
|
-
from ..core.project_config import ConfigResolver
|
|
550
1232
|
|
|
551
1233
|
# Determine project path
|
|
552
1234
|
proj_path = Path(project_path) if project_path else Path.cwd()
|
|
@@ -561,7 +1243,7 @@ def init(
|
|
|
561
1243
|
default=False,
|
|
562
1244
|
):
|
|
563
1245
|
console.print("[yellow]Initialization cancelled.[/yellow]")
|
|
564
|
-
raise typer.Exit(0)
|
|
1246
|
+
raise typer.Exit(0) from None
|
|
565
1247
|
|
|
566
1248
|
# 1. Try auto-discovery if no adapter specified
|
|
567
1249
|
discovered = None
|
|
@@ -573,7 +1255,7 @@ def init(
|
|
|
573
1255
|
)
|
|
574
1256
|
|
|
575
1257
|
# First try our improved .env configuration loader
|
|
576
|
-
from ..mcp.server import _load_env_configuration
|
|
1258
|
+
from ..mcp.server.main import _load_env_configuration
|
|
577
1259
|
|
|
578
1260
|
env_config = _load_env_configuration()
|
|
579
1261
|
|
|
@@ -650,11 +1332,9 @@ def init(
|
|
|
650
1332
|
elif adapter_type == "linear":
|
|
651
1333
|
# If not auto-discovered, build from CLI params or prompt
|
|
652
1334
|
if adapter_type not in config["adapters"]:
|
|
653
|
-
linear_config = {}
|
|
654
|
-
|
|
655
1335
|
# API Key
|
|
656
1336
|
linear_api_key = api_key or os.getenv("LINEAR_API_KEY")
|
|
657
|
-
if not linear_api_key
|
|
1337
|
+
if not linear_api_key:
|
|
658
1338
|
console.print("\n[bold]Linear Configuration[/bold]")
|
|
659
1339
|
console.print("You need a Linear API key to connect to Linear.")
|
|
660
1340
|
console.print(
|
|
@@ -665,30 +1345,81 @@ def init(
|
|
|
665
1345
|
"Enter your Linear API key", hide_input=True
|
|
666
1346
|
)
|
|
667
1347
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
# Team ID
|
|
1348
|
+
# Team ID or Team Key or Team URL
|
|
1349
|
+
# Try environment variables first
|
|
1350
|
+
linear_team_key = os.getenv("LINEAR_TEAM_KEY")
|
|
672
1351
|
linear_team_id = team_id or os.getenv("LINEAR_TEAM_ID")
|
|
673
|
-
if not linear_team_id and not discovered:
|
|
674
|
-
console.print("\nYou need your Linear team ID.")
|
|
675
|
-
console.print("[dim]Find it in Linear settings or team URL[/dim]\n")
|
|
676
|
-
|
|
677
|
-
linear_team_id = typer.prompt("Enter your Linear team ID")
|
|
678
|
-
|
|
679
|
-
if linear_team_id:
|
|
680
|
-
linear_config["team_id"] = linear_team_id
|
|
681
1352
|
|
|
682
|
-
if not
|
|
1353
|
+
if not linear_team_key and not linear_team_id:
|
|
1354
|
+
console.print("\n[bold]Linear Team Configuration[/bold]")
|
|
1355
|
+
console.print("You can provide either:")
|
|
1356
|
+
console.print(
|
|
1357
|
+
" 1. Team URL (e.g., https://linear.app/workspace/team/TEAMKEY/active)"
|
|
1358
|
+
)
|
|
1359
|
+
console.print(" 2. Team key (e.g., 'ENG', 'DESIGN', 'PRODUCT')")
|
|
1360
|
+
console.print(" 3. Team ID (UUID)")
|
|
683
1361
|
console.print(
|
|
684
|
-
"[
|
|
1362
|
+
"[dim]Find team URL or key in: Linear → Your Team → Team Issues Page[/dim]\n"
|
|
685
1363
|
)
|
|
1364
|
+
|
|
1365
|
+
team_input = typer.prompt("Team URL, key, or ID")
|
|
1366
|
+
|
|
1367
|
+
# Check if input is a URL
|
|
1368
|
+
if team_input.startswith("https://linear.app/"):
|
|
1369
|
+
console.print("[cyan]Detected team URL, deriving team ID...[/cyan]")
|
|
1370
|
+
import asyncio
|
|
1371
|
+
|
|
1372
|
+
from .linear_commands import derive_team_from_url
|
|
1373
|
+
|
|
1374
|
+
derived_team_id, error = asyncio.run(
|
|
1375
|
+
derive_team_from_url(linear_api_key, team_input)
|
|
1376
|
+
)
|
|
1377
|
+
|
|
1378
|
+
if derived_team_id:
|
|
1379
|
+
linear_team_id = derived_team_id
|
|
1380
|
+
console.print(
|
|
1381
|
+
"[green]✓[/green] Successfully derived team ID from URL"
|
|
1382
|
+
)
|
|
1383
|
+
else:
|
|
1384
|
+
console.print(f"[red]Error:[/red] {error}")
|
|
1385
|
+
console.print("Please provide team key or ID manually instead.")
|
|
1386
|
+
team_input = typer.prompt("Team key or ID")
|
|
1387
|
+
|
|
1388
|
+
# Store as either team_key or team_id based on format
|
|
1389
|
+
if len(team_input) > 20: # Likely a UUID
|
|
1390
|
+
linear_team_id = team_input
|
|
1391
|
+
else:
|
|
1392
|
+
linear_team_key = team_input
|
|
1393
|
+
else:
|
|
1394
|
+
# Input is team key or ID
|
|
1395
|
+
if len(team_input) > 20: # Likely a UUID
|
|
1396
|
+
linear_team_id = team_input
|
|
1397
|
+
else:
|
|
1398
|
+
linear_team_key = team_input
|
|
1399
|
+
|
|
1400
|
+
# Validate required fields (following JIRA pattern)
|
|
1401
|
+
if not linear_api_key:
|
|
1402
|
+
console.print("[red]Error:[/red] Linear API key is required")
|
|
1403
|
+
raise typer.Exit(1) from None
|
|
1404
|
+
|
|
1405
|
+
if not linear_team_id and not linear_team_key:
|
|
686
1406
|
console.print(
|
|
687
|
-
"
|
|
1407
|
+
"[red]Error:[/red] Linear requires either team ID or team key"
|
|
688
1408
|
)
|
|
689
|
-
raise typer.Exit(1)
|
|
1409
|
+
raise typer.Exit(1) from None
|
|
1410
|
+
|
|
1411
|
+
# Build configuration
|
|
1412
|
+
linear_config = {
|
|
1413
|
+
"api_key": linear_api_key,
|
|
1414
|
+
"type": "linear",
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
# Save whichever was provided
|
|
1418
|
+
if linear_team_key:
|
|
1419
|
+
linear_config["team_key"] = linear_team_key
|
|
1420
|
+
if linear_team_id:
|
|
1421
|
+
linear_config["team_id"] = linear_team_id
|
|
690
1422
|
|
|
691
|
-
linear_config["type"] = "linear"
|
|
692
1423
|
config["adapters"]["linear"] = linear_config
|
|
693
1424
|
|
|
694
1425
|
elif adapter_type == "jira":
|
|
@@ -700,7 +1431,7 @@ def init(
|
|
|
700
1431
|
project = jira_project or os.getenv("JIRA_PROJECT_KEY")
|
|
701
1432
|
|
|
702
1433
|
# Interactive prompts for missing values
|
|
703
|
-
if not server
|
|
1434
|
+
if not server:
|
|
704
1435
|
console.print("\n[bold]JIRA Configuration[/bold]")
|
|
705
1436
|
console.print("Enter your JIRA server details.\n")
|
|
706
1437
|
|
|
@@ -708,10 +1439,10 @@ def init(
|
|
|
708
1439
|
"JIRA server URL (e.g., https://company.atlassian.net)"
|
|
709
1440
|
)
|
|
710
1441
|
|
|
711
|
-
if not email
|
|
1442
|
+
if not email:
|
|
712
1443
|
email = typer.prompt("Your JIRA email address")
|
|
713
1444
|
|
|
714
|
-
if not token
|
|
1445
|
+
if not token:
|
|
715
1446
|
console.print("\nYou need a JIRA API token.")
|
|
716
1447
|
console.print(
|
|
717
1448
|
"[dim]Generate one at: https://id.atlassian.com/manage/api-tokens[/dim]\n"
|
|
@@ -719,7 +1450,7 @@ def init(
|
|
|
719
1450
|
|
|
720
1451
|
token = typer.prompt("Enter your JIRA API token", hide_input=True)
|
|
721
1452
|
|
|
722
|
-
if not project
|
|
1453
|
+
if not project:
|
|
723
1454
|
project = typer.prompt(
|
|
724
1455
|
"Default JIRA project key (optional, press Enter to skip)",
|
|
725
1456
|
default="",
|
|
@@ -729,15 +1460,15 @@ def init(
|
|
|
729
1460
|
# Validate required fields
|
|
730
1461
|
if not server:
|
|
731
1462
|
console.print("[red]Error:[/red] JIRA server URL is required")
|
|
732
|
-
raise typer.Exit(1)
|
|
1463
|
+
raise typer.Exit(1) from None
|
|
733
1464
|
|
|
734
1465
|
if not email:
|
|
735
1466
|
console.print("[red]Error:[/red] JIRA email is required")
|
|
736
|
-
raise typer.Exit(1)
|
|
1467
|
+
raise typer.Exit(1) from None
|
|
737
1468
|
|
|
738
1469
|
if not token:
|
|
739
1470
|
console.print("[red]Error:[/red] JIRA API token is required")
|
|
740
|
-
raise typer.Exit(1)
|
|
1471
|
+
raise typer.Exit(1) from None
|
|
741
1472
|
|
|
742
1473
|
jira_config = {
|
|
743
1474
|
"server": server,
|
|
@@ -759,7 +1490,7 @@ def init(
|
|
|
759
1490
|
token = github_token or os.getenv("GITHUB_TOKEN")
|
|
760
1491
|
|
|
761
1492
|
# Interactive prompts for missing values
|
|
762
|
-
if not owner
|
|
1493
|
+
if not owner:
|
|
763
1494
|
console.print("\n[bold]GitHub Configuration[/bold]")
|
|
764
1495
|
console.print("Enter your GitHub repository details.\n")
|
|
765
1496
|
|
|
@@ -767,10 +1498,10 @@ def init(
|
|
|
767
1498
|
"GitHub repository owner (username or organization)"
|
|
768
1499
|
)
|
|
769
1500
|
|
|
770
|
-
if not repo
|
|
1501
|
+
if not repo:
|
|
771
1502
|
repo = typer.prompt("GitHub repository name")
|
|
772
1503
|
|
|
773
|
-
if not token
|
|
1504
|
+
if not token:
|
|
774
1505
|
console.print("\nYou need a GitHub Personal Access Token.")
|
|
775
1506
|
console.print(
|
|
776
1507
|
"[dim]Create one at: https://github.com/settings/tokens/new[/dim]"
|
|
@@ -786,17 +1517,17 @@ def init(
|
|
|
786
1517
|
# Validate required fields
|
|
787
1518
|
if not owner:
|
|
788
1519
|
console.print("[red]Error:[/red] GitHub repository owner is required")
|
|
789
|
-
raise typer.Exit(1)
|
|
1520
|
+
raise typer.Exit(1) from None
|
|
790
1521
|
|
|
791
1522
|
if not repo:
|
|
792
1523
|
console.print("[red]Error:[/red] GitHub repository name is required")
|
|
793
|
-
raise typer.Exit(1)
|
|
1524
|
+
raise typer.Exit(1) from None
|
|
794
1525
|
|
|
795
1526
|
if not token:
|
|
796
1527
|
console.print(
|
|
797
1528
|
"[red]Error:[/red] GitHub Personal Access Token is required"
|
|
798
1529
|
)
|
|
799
|
-
raise typer.Exit(1)
|
|
1530
|
+
raise typer.Exit(1) from None
|
|
800
1531
|
|
|
801
1532
|
config["adapters"]["github"] = {
|
|
802
1533
|
"owner": owner,
|
|
@@ -805,42 +1536,46 @@ def init(
|
|
|
805
1536
|
"type": "github",
|
|
806
1537
|
}
|
|
807
1538
|
|
|
808
|
-
# 5. Save to
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
1539
|
+
# 5. Save to project-local config (global config deprecated for security)
|
|
1540
|
+
# Always save to ./.mcp-ticketer/config.json (PROJECT-SPECIFIC)
|
|
1541
|
+
config_file_path = proj_path / ".mcp-ticketer" / "config.json"
|
|
1542
|
+
config_file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1543
|
+
|
|
1544
|
+
with open(config_file_path, "w") as f:
|
|
1545
|
+
json.dump(config, f, indent=2)
|
|
814
1546
|
|
|
815
|
-
|
|
816
|
-
|
|
1547
|
+
if global_config:
|
|
1548
|
+
console.print(
|
|
1549
|
+
"[yellow]Note: Global config deprecated for security. Saved to project config instead.[/yellow]"
|
|
1550
|
+
)
|
|
817
1551
|
|
|
818
|
-
|
|
819
|
-
|
|
1552
|
+
console.print(f"[green]✓ Initialized with {adapter_type} adapter[/green]")
|
|
1553
|
+
console.print(f"[dim]Project configuration saved to {config_file_path}[/dim]")
|
|
1554
|
+
|
|
1555
|
+
# Add .mcp-ticketer to .gitignore if not already there
|
|
1556
|
+
gitignore_path = proj_path / ".gitignore"
|
|
1557
|
+
if gitignore_path.exists():
|
|
1558
|
+
gitignore_content = gitignore_path.read_text()
|
|
1559
|
+
if ".mcp-ticketer" not in gitignore_content:
|
|
1560
|
+
with open(gitignore_path, "a") as f:
|
|
1561
|
+
f.write("\n# MCP Ticketer\n.mcp-ticketer/\n")
|
|
1562
|
+
console.print("[dim]✓ Added .mcp-ticketer/ to .gitignore[/dim]")
|
|
820
1563
|
else:
|
|
821
|
-
#
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
with open(gitignore_path, "a") as f:
|
|
837
|
-
f.write("\n# MCP Ticketer\n.mcp-ticketer/\n")
|
|
838
|
-
console.print("[dim]✓ Added .mcp-ticketer/ to .gitignore[/dim]")
|
|
839
|
-
else:
|
|
840
|
-
# Create .gitignore if it doesn't exist
|
|
841
|
-
with open(gitignore_path, "w") as f:
|
|
842
|
-
f.write("# MCP Ticketer\n.mcp-ticketer/\n")
|
|
843
|
-
console.print("[dim]✓ Created .gitignore with .mcp-ticketer/[/dim]")
|
|
1564
|
+
# Create .gitignore if it doesn't exist
|
|
1565
|
+
with open(gitignore_path, "w") as f:
|
|
1566
|
+
f.write("# MCP Ticketer\n.mcp-ticketer/\n")
|
|
1567
|
+
console.print("[dim]✓ Created .gitignore with .mcp-ticketer/[/dim]")
|
|
1568
|
+
|
|
1569
|
+
# Validate configuration with loop for corrections
|
|
1570
|
+
import asyncio
|
|
1571
|
+
|
|
1572
|
+
if not asyncio.run(
|
|
1573
|
+
_validate_configuration_with_retry(
|
|
1574
|
+
console, adapter_type, config_file_path, proj_path
|
|
1575
|
+
)
|
|
1576
|
+
):
|
|
1577
|
+
# User chose to exit without valid configuration
|
|
1578
|
+
raise typer.Exit(1) from None
|
|
844
1579
|
|
|
845
1580
|
# Show next steps
|
|
846
1581
|
_show_next_steps(console, adapter_type, config_file_path)
|
|
@@ -861,16 +1596,13 @@ def _show_next_steps(
|
|
|
861
1596
|
console.print(f"MCP Ticketer is now configured to use {adapter_type.title()}.\n")
|
|
862
1597
|
|
|
863
1598
|
console.print("[bold]Next Steps:[/bold]")
|
|
864
|
-
console.print("1. [cyan]
|
|
865
|
-
console.print(" mcp-ticketer diagnose")
|
|
866
|
-
console.print("\n2. [cyan]Create a test ticket:[/cyan]")
|
|
1599
|
+
console.print("1. [cyan]Create a test ticket:[/cyan]")
|
|
867
1600
|
console.print(" mcp-ticketer create 'Test ticket from MCP Ticketer'")
|
|
868
1601
|
|
|
869
1602
|
if adapter_type != "aitrackdown":
|
|
870
1603
|
console.print(
|
|
871
|
-
f"\
|
|
1604
|
+
f"\n2. [cyan]Verify the ticket appears in {adapter_type.title()}[/cyan]"
|
|
872
1605
|
)
|
|
873
|
-
|
|
874
1606
|
if adapter_type == "linear":
|
|
875
1607
|
console.print(" Check your Linear workspace for the new ticket")
|
|
876
1608
|
elif adapter_type == "github":
|
|
@@ -878,121 +1610,36 @@ def _show_next_steps(
|
|
|
878
1610
|
elif adapter_type == "jira":
|
|
879
1611
|
console.print(" Check your JIRA project for the new ticket")
|
|
880
1612
|
else:
|
|
881
|
-
console.print("\
|
|
1613
|
+
console.print("\n2. [cyan]Check local ticket storage:[/cyan]")
|
|
882
1614
|
console.print(" ls .aitrackdown/")
|
|
883
1615
|
|
|
884
|
-
console.print("\
|
|
885
|
-
console.print(" mcp-ticketer
|
|
886
|
-
console.print(" mcp-ticketer
|
|
887
|
-
console.print(" mcp-ticketer
|
|
1616
|
+
console.print("\n3. [cyan]Install MCP for AI clients (optional):[/cyan]")
|
|
1617
|
+
console.print(" mcp-ticketer install claude-code # For Claude Code")
|
|
1618
|
+
console.print(" mcp-ticketer install claude-desktop # For Claude Desktop")
|
|
1619
|
+
console.print(" mcp-ticketer install auggie # For Auggie")
|
|
1620
|
+
console.print(" mcp-ticketer install gemini # For Gemini CLI")
|
|
888
1621
|
|
|
889
1622
|
console.print(f"\n[dim]Configuration saved to: {config_file_path}[/dim]")
|
|
890
|
-
console.print(
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
@app.command()
|
|
894
|
-
def install(
|
|
895
|
-
adapter: Optional[str] = typer.Option(
|
|
896
|
-
None,
|
|
897
|
-
"--adapter",
|
|
898
|
-
"-a",
|
|
899
|
-
help="Adapter type to use (auto-detected from .env if not specified)",
|
|
900
|
-
),
|
|
901
|
-
project_path: Optional[str] = typer.Option(
|
|
902
|
-
None, "--path", help="Project path (default: current directory)"
|
|
903
|
-
),
|
|
904
|
-
global_config: bool = typer.Option(
|
|
905
|
-
False,
|
|
906
|
-
"--global",
|
|
907
|
-
"-g",
|
|
908
|
-
help="Save to global config instead of project-specific",
|
|
909
|
-
),
|
|
910
|
-
base_path: Optional[str] = typer.Option(
|
|
911
|
-
None,
|
|
912
|
-
"--base-path",
|
|
913
|
-
"-p",
|
|
914
|
-
help="Base path for ticket storage (AITrackdown only)",
|
|
915
|
-
),
|
|
916
|
-
api_key: Optional[str] = typer.Option(
|
|
917
|
-
None, "--api-key", help="API key for Linear or API token for JIRA"
|
|
918
|
-
),
|
|
919
|
-
team_id: Optional[str] = typer.Option(
|
|
920
|
-
None, "--team-id", help="Linear team ID (required for Linear adapter)"
|
|
921
|
-
),
|
|
922
|
-
jira_server: Optional[str] = typer.Option(
|
|
923
|
-
None,
|
|
924
|
-
"--jira-server",
|
|
925
|
-
help="JIRA server URL (e.g., https://company.atlassian.net)",
|
|
926
|
-
),
|
|
927
|
-
jira_email: Optional[str] = typer.Option(
|
|
928
|
-
None, "--jira-email", help="JIRA user email for authentication"
|
|
929
|
-
),
|
|
930
|
-
jira_project: Optional[str] = typer.Option(
|
|
931
|
-
None, "--jira-project", help="Default JIRA project key"
|
|
932
|
-
),
|
|
933
|
-
github_owner: Optional[str] = typer.Option(
|
|
934
|
-
None, "--github-owner", help="GitHub repository owner"
|
|
935
|
-
),
|
|
936
|
-
github_repo: Optional[str] = typer.Option(
|
|
937
|
-
None, "--github-repo", help="GitHub repository name"
|
|
938
|
-
),
|
|
939
|
-
github_token: Optional[str] = typer.Option(
|
|
940
|
-
None, "--github-token", help="GitHub Personal Access Token"
|
|
941
|
-
),
|
|
942
|
-
) -> None:
|
|
943
|
-
"""Initialize mcp-ticketer for the current project (alias for init).
|
|
944
|
-
|
|
945
|
-
This command is synonymous with 'init' and 'setup' - all three provide
|
|
946
|
-
identical functionality with interactive prompts to guide you through
|
|
947
|
-
configuring MCP Ticketer for your preferred ticket management system.
|
|
948
|
-
|
|
949
|
-
Examples:
|
|
950
|
-
# Interactive setup (same as 'init' and 'setup')
|
|
951
|
-
mcp-ticketer install
|
|
952
|
-
|
|
953
|
-
# Force specific adapter
|
|
954
|
-
mcp-ticketer install --adapter linear
|
|
955
|
-
|
|
956
|
-
# Initialize for different project
|
|
957
|
-
mcp-ticketer install --path /path/to/project
|
|
958
|
-
|
|
959
|
-
# Save globally (not recommended)
|
|
960
|
-
mcp-ticketer install --global
|
|
961
|
-
|
|
962
|
-
"""
|
|
963
|
-
# Call init with all parameters
|
|
964
|
-
init(
|
|
965
|
-
adapter=adapter,
|
|
966
|
-
project_path=project_path,
|
|
967
|
-
global_config=global_config,
|
|
968
|
-
base_path=base_path,
|
|
969
|
-
api_key=api_key,
|
|
970
|
-
team_id=team_id,
|
|
971
|
-
jira_server=jira_server,
|
|
972
|
-
jira_email=jira_email,
|
|
973
|
-
jira_project=jira_project,
|
|
974
|
-
github_owner=github_owner,
|
|
975
|
-
github_repo=github_repo,
|
|
976
|
-
github_token=github_token,
|
|
1623
|
+
console.print(
|
|
1624
|
+
"[dim]Run 'mcp-ticketer doctor' to re-validate configuration anytime[/dim]"
|
|
977
1625
|
)
|
|
1626
|
+
console.print("[dim]Run 'mcp-ticketer --help' for more commands[/dim]")
|
|
978
1627
|
|
|
979
1628
|
|
|
980
1629
|
@app.command("set")
|
|
981
1630
|
def set_config(
|
|
982
|
-
adapter:
|
|
1631
|
+
adapter: AdapterType | None = typer.Option(
|
|
983
1632
|
None, "--adapter", "-a", help="Set default adapter"
|
|
984
1633
|
),
|
|
985
|
-
team_key:
|
|
1634
|
+
team_key: str | None = typer.Option(
|
|
986
1635
|
None, "--team-key", help="Linear team key (e.g., BTA)"
|
|
987
1636
|
),
|
|
988
|
-
team_id:
|
|
989
|
-
owner:
|
|
990
|
-
|
|
991
|
-
),
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
project: Optional[str] = typer.Option(None, "--project", help="JIRA project key"),
|
|
995
|
-
base_path: Optional[str] = typer.Option(
|
|
1637
|
+
team_id: str | None = typer.Option(None, "--team-id", help="Linear team ID"),
|
|
1638
|
+
owner: str | None = typer.Option(None, "--owner", help="GitHub repository owner"),
|
|
1639
|
+
repo: str | None = typer.Option(None, "--repo", help="GitHub repository name"),
|
|
1640
|
+
server: str | None = typer.Option(None, "--server", help="JIRA server URL"),
|
|
1641
|
+
project: str | None = typer.Option(None, "--project", help="JIRA project key"),
|
|
1642
|
+
base_path: str | None = typer.Option(
|
|
996
1643
|
None, "--base-path", help="AITrackdown base path"
|
|
997
1644
|
),
|
|
998
1645
|
) -> None:
|
|
@@ -1082,16 +1729,12 @@ def set_config(
|
|
|
1082
1729
|
@app.command("configure")
|
|
1083
1730
|
def configure_command(
|
|
1084
1731
|
show: bool = typer.Option(False, "--show", help="Show current configuration"),
|
|
1085
|
-
adapter:
|
|
1732
|
+
adapter: str | None = typer.Option(
|
|
1086
1733
|
None, "--adapter", help="Set default adapter type"
|
|
1087
1734
|
),
|
|
1088
|
-
api_key:
|
|
1089
|
-
project_id:
|
|
1090
|
-
|
|
1091
|
-
),
|
|
1092
|
-
team_id: Optional[str] = typer.Option(
|
|
1093
|
-
None, "--team-id", help="Set team ID (Linear)"
|
|
1094
|
-
),
|
|
1735
|
+
api_key: str | None = typer.Option(None, "--api-key", help="Set API key/token"),
|
|
1736
|
+
project_id: str | None = typer.Option(None, "--project-id", help="Set project ID"),
|
|
1737
|
+
team_id: str | None = typer.Option(None, "--team-id", help="Set team ID (Linear)"),
|
|
1095
1738
|
global_scope: bool = typer.Option(
|
|
1096
1739
|
False,
|
|
1097
1740
|
"--global",
|
|
@@ -1142,9 +1785,16 @@ def migrate_config(
|
|
|
1142
1785
|
migrate_config_command(dry_run=dry_run)
|
|
1143
1786
|
|
|
1144
1787
|
|
|
1145
|
-
@app.command("status")
|
|
1146
|
-
def
|
|
1147
|
-
"""Show queue and worker status.
|
|
1788
|
+
@app.command("queue-status", deprecated=True, hidden=True)
|
|
1789
|
+
def old_queue_status_command() -> None:
|
|
1790
|
+
"""Show queue and worker status.
|
|
1791
|
+
|
|
1792
|
+
DEPRECATED: Use 'mcp-ticketer queue status' instead.
|
|
1793
|
+
"""
|
|
1794
|
+
console.print(
|
|
1795
|
+
"[yellow]⚠️ This command is deprecated. Use 'mcp-ticketer queue status' instead.[/yellow]\n"
|
|
1796
|
+
)
|
|
1797
|
+
|
|
1148
1798
|
queue = Queue()
|
|
1149
1799
|
manager = WorkerManager()
|
|
1150
1800
|
|
|
@@ -1169,12 +1819,12 @@ def status_command():
|
|
|
1169
1819
|
console.print("\n[red]○ Worker is not running[/red]")
|
|
1170
1820
|
if pending > 0:
|
|
1171
1821
|
console.print(
|
|
1172
|
-
"[yellow]Note: There are pending items. Start worker with 'mcp-ticketer worker start'[/yellow]"
|
|
1822
|
+
"[yellow]Note: There are pending items. Start worker with 'mcp-ticketer queue worker start'[/yellow]"
|
|
1173
1823
|
)
|
|
1174
1824
|
|
|
1175
1825
|
|
|
1176
|
-
@app.command()
|
|
1177
|
-
def
|
|
1826
|
+
@app.command("queue-health", deprecated=True, hidden=True)
|
|
1827
|
+
def old_queue_health_command(
|
|
1178
1828
|
auto_repair: bool = typer.Option(
|
|
1179
1829
|
False, "--auto-repair", help="Attempt automatic repair of issues"
|
|
1180
1830
|
),
|
|
@@ -1182,7 +1832,13 @@ def health(
|
|
|
1182
1832
|
False, "--verbose", "-v", help="Show detailed health information"
|
|
1183
1833
|
),
|
|
1184
1834
|
) -> None:
|
|
1185
|
-
"""Check queue system health and detect issues immediately.
|
|
1835
|
+
"""Check queue system health and detect issues immediately.
|
|
1836
|
+
|
|
1837
|
+
DEPRECATED: Use 'mcp-ticketer queue health' instead.
|
|
1838
|
+
"""
|
|
1839
|
+
console.print(
|
|
1840
|
+
"[yellow]⚠️ This command is deprecated. Use 'mcp-ticketer queue health' instead.[/yellow]\n"
|
|
1841
|
+
)
|
|
1186
1842
|
health_monitor = QueueHealthMonitor()
|
|
1187
1843
|
health = health_monitor.check_health()
|
|
1188
1844
|
|
|
@@ -1246,41 +1902,48 @@ def health(
|
|
|
1246
1902
|
|
|
1247
1903
|
# Exit with appropriate code
|
|
1248
1904
|
if health["status"] == HealthStatus.CRITICAL:
|
|
1249
|
-
raise typer.Exit(1)
|
|
1905
|
+
raise typer.Exit(1) from None
|
|
1250
1906
|
elif health["status"] == HealthStatus.WARNING:
|
|
1251
|
-
raise typer.Exit(2)
|
|
1907
|
+
raise typer.Exit(2) from None
|
|
1252
1908
|
|
|
1253
1909
|
|
|
1254
|
-
@app.command()
|
|
1910
|
+
@app.command(deprecated=True, hidden=True)
|
|
1255
1911
|
def create(
|
|
1256
1912
|
title: str = typer.Argument(..., help="Ticket title"),
|
|
1257
|
-
description:
|
|
1913
|
+
description: str | None = typer.Option(
|
|
1258
1914
|
None, "--description", "-d", help="Ticket description"
|
|
1259
1915
|
),
|
|
1260
1916
|
priority: Priority = typer.Option(
|
|
1261
1917
|
Priority.MEDIUM, "--priority", "-p", help="Priority level"
|
|
1262
1918
|
),
|
|
1263
|
-
tags:
|
|
1919
|
+
tags: list[str] | None = typer.Option(
|
|
1264
1920
|
None, "--tag", "-t", help="Tags (can be specified multiple times)"
|
|
1265
1921
|
),
|
|
1266
|
-
assignee:
|
|
1922
|
+
assignee: str | None = typer.Option(
|
|
1267
1923
|
None, "--assignee", "-a", help="Assignee username"
|
|
1268
1924
|
),
|
|
1269
|
-
project:
|
|
1925
|
+
project: str | None = typer.Option(
|
|
1270
1926
|
None,
|
|
1271
1927
|
"--project",
|
|
1272
1928
|
help="Parent project/epic ID (synonym for --epic)",
|
|
1273
1929
|
),
|
|
1274
|
-
epic:
|
|
1930
|
+
epic: str | None = typer.Option(
|
|
1275
1931
|
None,
|
|
1276
1932
|
"--epic",
|
|
1277
1933
|
help="Parent epic/project ID (synonym for --project)",
|
|
1278
1934
|
),
|
|
1279
|
-
adapter:
|
|
1935
|
+
adapter: AdapterType | None = typer.Option(
|
|
1280
1936
|
None, "--adapter", help="Override default adapter"
|
|
1281
1937
|
),
|
|
1282
1938
|
) -> None:
|
|
1283
|
-
"""Create a new ticket with comprehensive health checks.
|
|
1939
|
+
"""Create a new ticket with comprehensive health checks.
|
|
1940
|
+
|
|
1941
|
+
DEPRECATED: Use 'mcp-ticketer ticket create' instead.
|
|
1942
|
+
"""
|
|
1943
|
+
console.print(
|
|
1944
|
+
"[yellow]⚠️ This command is deprecated. Use 'mcp-ticketer ticket create' instead.[/yellow]\n"
|
|
1945
|
+
)
|
|
1946
|
+
|
|
1284
1947
|
# IMMEDIATE HEALTH CHECK - Critical for reliability
|
|
1285
1948
|
health_monitor = QueueHealthMonitor()
|
|
1286
1949
|
health = health_monitor.check_health()
|
|
@@ -1309,7 +1972,7 @@ def create(
|
|
|
1309
1972
|
console.print(
|
|
1310
1973
|
"[red]Cannot safely create ticket. Please check system status.[/red]"
|
|
1311
1974
|
)
|
|
1312
|
-
raise typer.Exit(1)
|
|
1975
|
+
raise typer.Exit(1) from None
|
|
1313
1976
|
else:
|
|
1314
1977
|
console.print(
|
|
1315
1978
|
"[green]✓ Auto-repair successful. Proceeding with ticket creation.[/green]"
|
|
@@ -1318,7 +1981,7 @@ def create(
|
|
|
1318
1981
|
console.print(
|
|
1319
1982
|
"[red]❌ No repair actions available. Manual intervention required.[/red]"
|
|
1320
1983
|
)
|
|
1321
|
-
raise typer.Exit(1)
|
|
1984
|
+
raise typer.Exit(1) from None
|
|
1322
1985
|
|
|
1323
1986
|
elif health["status"] == HealthStatus.WARNING:
|
|
1324
1987
|
console.print("[yellow]⚠️ Warning: Queue system has minor issues[/yellow]")
|
|
@@ -1476,22 +2139,28 @@ def create(
|
|
|
1476
2139
|
)
|
|
1477
2140
|
|
|
1478
2141
|
|
|
1479
|
-
@app.command("list")
|
|
2142
|
+
@app.command("list", deprecated=True, hidden=True)
|
|
1480
2143
|
def list_tickets(
|
|
1481
|
-
state:
|
|
2144
|
+
state: TicketState | None = typer.Option(
|
|
1482
2145
|
None, "--state", "-s", help="Filter by state"
|
|
1483
2146
|
),
|
|
1484
|
-
priority:
|
|
2147
|
+
priority: Priority | None = typer.Option(
|
|
1485
2148
|
None, "--priority", "-p", help="Filter by priority"
|
|
1486
2149
|
),
|
|
1487
2150
|
limit: int = typer.Option(10, "--limit", "-l", help="Maximum number of tickets"),
|
|
1488
|
-
adapter:
|
|
2151
|
+
adapter: AdapterType | None = typer.Option(
|
|
1489
2152
|
None, "--adapter", help="Override default adapter"
|
|
1490
2153
|
),
|
|
1491
2154
|
) -> None:
|
|
1492
|
-
"""List tickets with optional filters.
|
|
2155
|
+
"""List tickets with optional filters.
|
|
1493
2156
|
|
|
1494
|
-
|
|
2157
|
+
DEPRECATED: Use 'mcp-ticketer ticket list' instead.
|
|
2158
|
+
"""
|
|
2159
|
+
console.print(
|
|
2160
|
+
"[yellow]⚠️ This command is deprecated. Use 'mcp-ticketer ticket list' instead.[/yellow]\n"
|
|
2161
|
+
)
|
|
2162
|
+
|
|
2163
|
+
async def _list() -> None:
|
|
1495
2164
|
adapter_instance = get_adapter(
|
|
1496
2165
|
override_adapter=adapter.value if adapter else None
|
|
1497
2166
|
)
|
|
@@ -1531,17 +2200,23 @@ def list_tickets(
|
|
|
1531
2200
|
console.print(table)
|
|
1532
2201
|
|
|
1533
2202
|
|
|
1534
|
-
@app.command()
|
|
2203
|
+
@app.command(deprecated=True, hidden=True)
|
|
1535
2204
|
def show(
|
|
1536
2205
|
ticket_id: str = typer.Argument(..., help="Ticket ID"),
|
|
1537
2206
|
comments: bool = typer.Option(False, "--comments", "-c", help="Show comments"),
|
|
1538
|
-
adapter:
|
|
2207
|
+
adapter: AdapterType | None = typer.Option(
|
|
1539
2208
|
None, "--adapter", help="Override default adapter"
|
|
1540
2209
|
),
|
|
1541
2210
|
) -> None:
|
|
1542
|
-
"""Show detailed ticket information.
|
|
2211
|
+
"""Show detailed ticket information.
|
|
1543
2212
|
|
|
1544
|
-
|
|
2213
|
+
DEPRECATED: Use 'mcp-ticketer ticket show' instead.
|
|
2214
|
+
"""
|
|
2215
|
+
console.print(
|
|
2216
|
+
"[yellow]⚠️ This command is deprecated. Use 'mcp-ticketer ticket show' instead.[/yellow]\n"
|
|
2217
|
+
)
|
|
2218
|
+
|
|
2219
|
+
async def _show() -> None:
|
|
1545
2220
|
adapter_instance = get_adapter(
|
|
1546
2221
|
override_adapter=adapter.value if adapter else None
|
|
1547
2222
|
)
|
|
@@ -1555,7 +2230,7 @@ def show(
|
|
|
1555
2230
|
|
|
1556
2231
|
if not ticket:
|
|
1557
2232
|
console.print(f"[red]✗[/red] Ticket not found: {ticket_id}")
|
|
1558
|
-
raise typer.Exit(1)
|
|
2233
|
+
raise typer.Exit(1) from None
|
|
1559
2234
|
|
|
1560
2235
|
# Display ticket details
|
|
1561
2236
|
console.print(f"\n[bold]Ticket: {ticket.id}[/bold]")
|
|
@@ -1581,17 +2256,23 @@ def show(
|
|
|
1581
2256
|
console.print(comment.content)
|
|
1582
2257
|
|
|
1583
2258
|
|
|
1584
|
-
@app.command()
|
|
2259
|
+
@app.command(deprecated=True, hidden=True)
|
|
1585
2260
|
def comment(
|
|
1586
2261
|
ticket_id: str = typer.Argument(..., help="Ticket ID"),
|
|
1587
2262
|
content: str = typer.Argument(..., help="Comment content"),
|
|
1588
|
-
adapter:
|
|
2263
|
+
adapter: AdapterType | None = typer.Option(
|
|
1589
2264
|
None, "--adapter", help="Override default adapter"
|
|
1590
2265
|
),
|
|
1591
2266
|
) -> None:
|
|
1592
|
-
"""Add a comment to a ticket.
|
|
2267
|
+
"""Add a comment to a ticket.
|
|
2268
|
+
|
|
2269
|
+
DEPRECATED: Use 'mcp-ticketer ticket comment' instead.
|
|
2270
|
+
"""
|
|
2271
|
+
console.print(
|
|
2272
|
+
"[yellow]⚠️ This command is deprecated. Use 'mcp-ticketer ticket comment' instead.[/yellow]\n"
|
|
2273
|
+
)
|
|
1593
2274
|
|
|
1594
|
-
async def _comment():
|
|
2275
|
+
async def _comment() -> None:
|
|
1595
2276
|
adapter_instance = get_adapter(
|
|
1596
2277
|
override_adapter=adapter.value if adapter else None
|
|
1597
2278
|
)
|
|
@@ -1614,27 +2295,31 @@ def comment(
|
|
|
1614
2295
|
console.print(f"Content: {content}")
|
|
1615
2296
|
except Exception as e:
|
|
1616
2297
|
console.print(f"[red]✗[/red] Failed to add comment: {e}")
|
|
1617
|
-
raise typer.Exit(1)
|
|
2298
|
+
raise typer.Exit(1) from e
|
|
1618
2299
|
|
|
1619
2300
|
|
|
1620
|
-
@app.command()
|
|
2301
|
+
@app.command(deprecated=True, hidden=True)
|
|
1621
2302
|
def update(
|
|
1622
2303
|
ticket_id: str = typer.Argument(..., help="Ticket ID"),
|
|
1623
|
-
title:
|
|
1624
|
-
description:
|
|
2304
|
+
title: str | None = typer.Option(None, "--title", help="New title"),
|
|
2305
|
+
description: str | None = typer.Option(
|
|
1625
2306
|
None, "--description", "-d", help="New description"
|
|
1626
2307
|
),
|
|
1627
|
-
priority:
|
|
2308
|
+
priority: Priority | None = typer.Option(
|
|
1628
2309
|
None, "--priority", "-p", help="New priority"
|
|
1629
2310
|
),
|
|
1630
|
-
assignee:
|
|
1631
|
-
|
|
1632
|
-
),
|
|
1633
|
-
adapter: Optional[AdapterType] = typer.Option(
|
|
2311
|
+
assignee: str | None = typer.Option(None, "--assignee", "-a", help="New assignee"),
|
|
2312
|
+
adapter: AdapterType | None = typer.Option(
|
|
1634
2313
|
None, "--adapter", help="Override default adapter"
|
|
1635
2314
|
),
|
|
1636
2315
|
) -> None:
|
|
1637
|
-
"""Update ticket fields.
|
|
2316
|
+
"""Update ticket fields.
|
|
2317
|
+
|
|
2318
|
+
DEPRECATED: Use 'mcp-ticketer ticket update' instead.
|
|
2319
|
+
"""
|
|
2320
|
+
console.print(
|
|
2321
|
+
"[yellow]⚠️ This command is deprecated. Use 'mcp-ticketer ticket update' instead.[/yellow]\n"
|
|
2322
|
+
)
|
|
1638
2323
|
updates = {}
|
|
1639
2324
|
if title:
|
|
1640
2325
|
updates["title"] = title
|
|
@@ -1649,7 +2334,7 @@ def update(
|
|
|
1649
2334
|
|
|
1650
2335
|
if not updates:
|
|
1651
2336
|
console.print("[yellow]No updates specified[/yellow]")
|
|
1652
|
-
raise typer.Exit(1)
|
|
2337
|
+
raise typer.Exit(1) from None
|
|
1653
2338
|
|
|
1654
2339
|
# Get the adapter name
|
|
1655
2340
|
config = load_config()
|
|
@@ -1681,30 +2366,36 @@ def update(
|
|
|
1681
2366
|
console.print("[dim]Worker started to process request[/dim]")
|
|
1682
2367
|
|
|
1683
2368
|
|
|
1684
|
-
@app.command()
|
|
2369
|
+
@app.command(deprecated=True, hidden=True)
|
|
1685
2370
|
def transition(
|
|
1686
2371
|
ticket_id: str = typer.Argument(..., help="Ticket ID"),
|
|
1687
|
-
state_positional:
|
|
2372
|
+
state_positional: TicketState | None = typer.Argument(
|
|
1688
2373
|
None, help="Target state (positional - deprecated, use --state instead)"
|
|
1689
2374
|
),
|
|
1690
|
-
state:
|
|
2375
|
+
state: TicketState | None = typer.Option(
|
|
1691
2376
|
None, "--state", "-s", help="Target state (recommended)"
|
|
1692
2377
|
),
|
|
1693
|
-
adapter:
|
|
2378
|
+
adapter: AdapterType | None = typer.Option(
|
|
1694
2379
|
None, "--adapter", help="Override default adapter"
|
|
1695
2380
|
),
|
|
1696
2381
|
) -> None:
|
|
1697
2382
|
"""Change ticket state with validation.
|
|
1698
2383
|
|
|
2384
|
+
DEPRECATED: Use 'mcp-ticketer ticket transition' instead.
|
|
2385
|
+
|
|
1699
2386
|
Examples:
|
|
1700
2387
|
# Recommended syntax with flag:
|
|
1701
|
-
mcp-ticketer transition BTA-215 --state done
|
|
1702
|
-
mcp-ticketer transition BTA-215 -s in_progress
|
|
2388
|
+
mcp-ticketer ticket transition BTA-215 --state done
|
|
2389
|
+
mcp-ticketer ticket transition BTA-215 -s in_progress
|
|
1703
2390
|
|
|
1704
2391
|
# Legacy positional syntax (still supported):
|
|
1705
|
-
mcp-ticketer transition BTA-215 done
|
|
2392
|
+
mcp-ticketer ticket transition BTA-215 done
|
|
1706
2393
|
|
|
1707
2394
|
"""
|
|
2395
|
+
console.print(
|
|
2396
|
+
"[yellow]⚠️ This command is deprecated. Use 'mcp-ticketer ticket transition' instead.[/yellow]\n"
|
|
2397
|
+
)
|
|
2398
|
+
|
|
1708
2399
|
# Determine which state to use (prefer flag over positional)
|
|
1709
2400
|
target_state = state if state is not None else state_positional
|
|
1710
2401
|
|
|
@@ -1715,7 +2406,7 @@ def transition(
|
|
|
1715
2406
|
" - Flag syntax (recommended): mcp-ticketer transition TICKET-ID --state STATE\n"
|
|
1716
2407
|
" - Positional syntax: mcp-ticketer transition TICKET-ID STATE"
|
|
1717
2408
|
)
|
|
1718
|
-
raise typer.Exit(1)
|
|
2409
|
+
raise typer.Exit(1) from None
|
|
1719
2410
|
|
|
1720
2411
|
# Get the adapter name
|
|
1721
2412
|
config = load_config()
|
|
@@ -1747,20 +2438,26 @@ def transition(
|
|
|
1747
2438
|
console.print("[dim]Worker started to process request[/dim]")
|
|
1748
2439
|
|
|
1749
2440
|
|
|
1750
|
-
@app.command()
|
|
2441
|
+
@app.command(deprecated=True, hidden=True)
|
|
1751
2442
|
def search(
|
|
1752
|
-
query:
|
|
1753
|
-
state:
|
|
1754
|
-
priority:
|
|
1755
|
-
assignee:
|
|
2443
|
+
query: str | None = typer.Argument(None, help="Search query"),
|
|
2444
|
+
state: TicketState | None = typer.Option(None, "--state", "-s"),
|
|
2445
|
+
priority: Priority | None = typer.Option(None, "--priority", "-p"),
|
|
2446
|
+
assignee: str | None = typer.Option(None, "--assignee", "-a"),
|
|
1756
2447
|
limit: int = typer.Option(10, "--limit", "-l"),
|
|
1757
|
-
adapter:
|
|
2448
|
+
adapter: AdapterType | None = typer.Option(
|
|
1758
2449
|
None, "--adapter", help="Override default adapter"
|
|
1759
2450
|
),
|
|
1760
2451
|
) -> None:
|
|
1761
|
-
"""Search tickets with advanced query.
|
|
2452
|
+
"""Search tickets with advanced query.
|
|
1762
2453
|
|
|
1763
|
-
|
|
2454
|
+
DEPRECATED: Use 'mcp-ticketer ticket search' instead.
|
|
2455
|
+
"""
|
|
2456
|
+
console.print(
|
|
2457
|
+
"[yellow]⚠️ This command is deprecated. Use 'mcp-ticketer ticket search' instead.[/yellow]\n"
|
|
2458
|
+
)
|
|
2459
|
+
|
|
2460
|
+
async def _search() -> None:
|
|
1764
2461
|
adapter_instance = get_adapter(
|
|
1765
2462
|
override_adapter=adapter.value if adapter else None
|
|
1766
2463
|
)
|
|
@@ -1790,17 +2487,26 @@ def search(
|
|
|
1790
2487
|
console.print()
|
|
1791
2488
|
|
|
1792
2489
|
|
|
2490
|
+
# Add ticket command group to main app
|
|
2491
|
+
app.add_typer(ticket_app, name="ticket")
|
|
2492
|
+
|
|
2493
|
+
# Add platform command group to main app
|
|
2494
|
+
app.add_typer(platform_app, name="platform")
|
|
2495
|
+
|
|
1793
2496
|
# Add queue command to main app
|
|
1794
2497
|
app.add_typer(queue_app, name="queue")
|
|
1795
2498
|
|
|
1796
2499
|
# Add discover command to main app
|
|
1797
2500
|
app.add_typer(discover_app, name="discover")
|
|
1798
2501
|
|
|
2502
|
+
# Add instructions command to main app
|
|
2503
|
+
app.add_typer(instruction_app, name="instructions")
|
|
2504
|
+
|
|
1799
2505
|
|
|
1800
2506
|
# Add diagnostics command
|
|
1801
|
-
@app.command()
|
|
1802
|
-
def
|
|
1803
|
-
output_file:
|
|
2507
|
+
@app.command("doctor")
|
|
2508
|
+
def doctor_command(
|
|
2509
|
+
output_file: str | None = typer.Option(
|
|
1804
2510
|
None, "--output", "-o", help="Save full report to file"
|
|
1805
2511
|
),
|
|
1806
2512
|
json_output: bool = typer.Option(
|
|
@@ -1810,7 +2516,7 @@ def diagnose(
|
|
|
1810
2516
|
False, "--simple", help="Use simple diagnostics (no heavy dependencies)"
|
|
1811
2517
|
),
|
|
1812
2518
|
) -> None:
|
|
1813
|
-
"""Run comprehensive system diagnostics and health check."""
|
|
2519
|
+
"""Run comprehensive system diagnostics and health check (alias: diagnose)."""
|
|
1814
2520
|
if simple:
|
|
1815
2521
|
from .simple_health import simple_diagnose
|
|
1816
2522
|
|
|
@@ -1826,7 +2532,7 @@ def diagnose(
|
|
|
1826
2532
|
|
|
1827
2533
|
console.print("\n" + json.dumps(report, indent=2))
|
|
1828
2534
|
if report["issues"]:
|
|
1829
|
-
raise typer.Exit(1)
|
|
2535
|
+
raise typer.Exit(1) from None
|
|
1830
2536
|
else:
|
|
1831
2537
|
try:
|
|
1832
2538
|
asyncio.run(
|
|
@@ -1842,17 +2548,44 @@ def diagnose(
|
|
|
1842
2548
|
|
|
1843
2549
|
report = simple_diagnose()
|
|
1844
2550
|
if report["issues"]:
|
|
1845
|
-
raise typer.Exit(1)
|
|
2551
|
+
raise typer.Exit(1) from None
|
|
1846
2552
|
|
|
1847
2553
|
|
|
1848
|
-
@app.command()
|
|
1849
|
-
def
|
|
1850
|
-
|
|
2554
|
+
@app.command("diagnose", hidden=True)
|
|
2555
|
+
def diagnose_alias(
|
|
2556
|
+
output_file: str | None = typer.Option(
|
|
2557
|
+
None, "--output", "-o", help="Save full report to file"
|
|
2558
|
+
),
|
|
2559
|
+
json_output: bool = typer.Option(
|
|
2560
|
+
False, "--json", help="Output report in JSON format"
|
|
2561
|
+
),
|
|
2562
|
+
simple: bool = typer.Option(
|
|
2563
|
+
False, "--simple", help="Use simple diagnostics (no heavy dependencies)"
|
|
2564
|
+
),
|
|
2565
|
+
) -> None:
|
|
2566
|
+
"""Run comprehensive system diagnostics and health check (alias for doctor)."""
|
|
2567
|
+
# Call the doctor_command function with the same parameters
|
|
2568
|
+
doctor_command(output_file=output_file, json_output=json_output, simple=simple)
|
|
2569
|
+
|
|
2570
|
+
|
|
2571
|
+
@app.command("status")
|
|
2572
|
+
def status_command() -> None:
|
|
2573
|
+
"""Quick health check - shows system status summary (alias: health)."""
|
|
2574
|
+
from .simple_health import simple_health_check
|
|
2575
|
+
|
|
2576
|
+
result = simple_health_check()
|
|
2577
|
+
if result != 0:
|
|
2578
|
+
raise typer.Exit(result) from None
|
|
2579
|
+
|
|
2580
|
+
|
|
2581
|
+
@app.command("health")
|
|
2582
|
+
def health_alias() -> None:
|
|
2583
|
+
"""Quick health check - shows system status summary (alias for status)."""
|
|
1851
2584
|
from .simple_health import simple_health_check
|
|
1852
2585
|
|
|
1853
2586
|
result = simple_health_check()
|
|
1854
2587
|
if result != 0:
|
|
1855
|
-
raise typer.Exit(result)
|
|
2588
|
+
raise typer.Exit(result) from None
|
|
1856
2589
|
|
|
1857
2590
|
|
|
1858
2591
|
# Create MCP configuration command group
|
|
@@ -1860,18 +2593,554 @@ mcp_app = typer.Typer(
|
|
|
1860
2593
|
name="mcp",
|
|
1861
2594
|
help="Configure MCP integration for AI clients (Claude, Gemini, Codex, Auggie)",
|
|
1862
2595
|
add_completion=False,
|
|
2596
|
+
invoke_without_command=True,
|
|
1863
2597
|
)
|
|
1864
2598
|
|
|
1865
2599
|
|
|
2600
|
+
@mcp_app.callback()
|
|
2601
|
+
def mcp_callback(
|
|
2602
|
+
ctx: typer.Context,
|
|
2603
|
+
project_path: str | None = typer.Option(
|
|
2604
|
+
None, "--path", "-p", help="Project directory path (default: current directory)"
|
|
2605
|
+
),
|
|
2606
|
+
) -> None:
|
|
2607
|
+
"""MCP command group - runs MCP server if no subcommand provided.
|
|
2608
|
+
|
|
2609
|
+
Examples:
|
|
2610
|
+
mcp-ticketer mcp # Start server in current directory
|
|
2611
|
+
mcp-ticketer mcp --path /dir # Start server in specific directory
|
|
2612
|
+
mcp-ticketer mcp -p /dir # Start server (short form)
|
|
2613
|
+
mcp-ticketer mcp status # Check MCP status
|
|
2614
|
+
mcp-ticketer mcp serve # Explicitly start server
|
|
2615
|
+
|
|
2616
|
+
"""
|
|
2617
|
+
if ctx.invoked_subcommand is None:
|
|
2618
|
+
# No subcommand provided, run the serve command
|
|
2619
|
+
# Change to project directory if provided
|
|
2620
|
+
if project_path:
|
|
2621
|
+
import os
|
|
2622
|
+
|
|
2623
|
+
os.chdir(project_path)
|
|
2624
|
+
# Invoke the serve command through context
|
|
2625
|
+
ctx.invoke(mcp_serve, adapter=None, base_path=None)
|
|
2626
|
+
|
|
2627
|
+
|
|
1866
2628
|
@app.command()
|
|
1867
|
-
def
|
|
1868
|
-
|
|
2629
|
+
def install(
|
|
2630
|
+
platform: str | None = typer.Argument(
|
|
2631
|
+
None,
|
|
2632
|
+
help="Platform to install (claude-code, claude-desktop, gemini, codex, auggie)",
|
|
2633
|
+
),
|
|
2634
|
+
auto_detect: bool = typer.Option(
|
|
2635
|
+
False,
|
|
2636
|
+
"--auto-detect",
|
|
2637
|
+
"-d",
|
|
2638
|
+
help="Auto-detect and show all installed AI platforms",
|
|
2639
|
+
),
|
|
2640
|
+
install_all: bool = typer.Option(
|
|
2641
|
+
False,
|
|
2642
|
+
"--all",
|
|
2643
|
+
help="Install for all detected platforms",
|
|
2644
|
+
),
|
|
2645
|
+
adapter: str | None = typer.Option(
|
|
2646
|
+
None,
|
|
2647
|
+
"--adapter",
|
|
2648
|
+
"-a",
|
|
2649
|
+
help="Adapter type to use (interactive prompt if not specified)",
|
|
2650
|
+
),
|
|
2651
|
+
project_path: str | None = typer.Option(
|
|
2652
|
+
None, "--path", help="Project path (default: current directory)"
|
|
2653
|
+
),
|
|
2654
|
+
global_config: bool = typer.Option(
|
|
2655
|
+
False,
|
|
2656
|
+
"--global",
|
|
2657
|
+
"-g",
|
|
2658
|
+
help="Save to global config instead of project-specific",
|
|
2659
|
+
),
|
|
2660
|
+
base_path: str | None = typer.Option(
|
|
2661
|
+
None,
|
|
2662
|
+
"--base-path",
|
|
2663
|
+
"-p",
|
|
2664
|
+
help="Base path for ticket storage (AITrackdown only)",
|
|
2665
|
+
),
|
|
2666
|
+
api_key: str | None = typer.Option(
|
|
2667
|
+
None, "--api-key", help="API key for Linear or API token for JIRA"
|
|
2668
|
+
),
|
|
2669
|
+
team_id: str | None = typer.Option(
|
|
2670
|
+
None, "--team-id", help="Linear team ID (required for Linear adapter)"
|
|
2671
|
+
),
|
|
2672
|
+
jira_server: str | None = typer.Option(
|
|
2673
|
+
None,
|
|
2674
|
+
"--jira-server",
|
|
2675
|
+
help="JIRA server URL (e.g., https://company.atlassian.net)",
|
|
2676
|
+
),
|
|
2677
|
+
jira_email: str | None = typer.Option(
|
|
2678
|
+
None, "--jira-email", help="JIRA user email for authentication"
|
|
2679
|
+
),
|
|
2680
|
+
jira_project: str | None = typer.Option(
|
|
2681
|
+
None, "--jira-project", help="Default JIRA project key"
|
|
2682
|
+
),
|
|
2683
|
+
github_owner: str | None = typer.Option(
|
|
2684
|
+
None, "--github-owner", help="GitHub repository owner"
|
|
2685
|
+
),
|
|
2686
|
+
github_repo: str | None = typer.Option(
|
|
2687
|
+
None, "--github-repo", help="GitHub repository name"
|
|
2688
|
+
),
|
|
2689
|
+
github_token: str | None = typer.Option(
|
|
2690
|
+
None, "--github-token", help="GitHub Personal Access Token"
|
|
2691
|
+
),
|
|
2692
|
+
dry_run: bool = typer.Option(
|
|
2693
|
+
False,
|
|
2694
|
+
"--dry-run",
|
|
2695
|
+
help="Show what would be done without making changes (for platform installation)",
|
|
2696
|
+
),
|
|
2697
|
+
) -> None:
|
|
2698
|
+
"""Install MCP server configuration for AI platforms.
|
|
2699
|
+
|
|
2700
|
+
This command configures mcp-ticketer as an MCP server for various AI
|
|
2701
|
+
platforms. It updates platform-specific configuration files to enable
|
|
2702
|
+
mcp-ticketer integration.
|
|
2703
|
+
|
|
2704
|
+
RECOMMENDED: Use 'mcp-ticketer setup' for first-time setup, which
|
|
2705
|
+
handles both adapter configuration and platform installation together.
|
|
2706
|
+
|
|
2707
|
+
Platform Installation:
|
|
2708
|
+
# Auto-detect and prompt for platform selection
|
|
2709
|
+
mcp-ticketer install
|
|
2710
|
+
|
|
2711
|
+
# Show all detected platforms
|
|
2712
|
+
mcp-ticketer install --auto-detect
|
|
2713
|
+
|
|
2714
|
+
# Install for all detected platforms
|
|
2715
|
+
mcp-ticketer install --all
|
|
2716
|
+
|
|
2717
|
+
# Install for specific platform
|
|
2718
|
+
mcp-ticketer install claude-code # Claude Code (project-level)
|
|
2719
|
+
mcp-ticketer install claude-desktop # Claude Desktop (global)
|
|
2720
|
+
mcp-ticketer install gemini # Gemini CLI
|
|
2721
|
+
mcp-ticketer install codex # Codex
|
|
2722
|
+
mcp-ticketer install auggie # Auggie
|
|
2723
|
+
|
|
2724
|
+
Legacy Usage (adapter setup, deprecated - use 'init' or 'setup' instead):
|
|
2725
|
+
mcp-ticketer install --adapter linear # Use 'init' or 'setup' instead
|
|
2726
|
+
|
|
2727
|
+
"""
|
|
2728
|
+
from .platform_detection import PlatformDetector, get_platform_by_name
|
|
2729
|
+
|
|
2730
|
+
detector = PlatformDetector()
|
|
2731
|
+
|
|
2732
|
+
# Handle auto-detect flag (just show detected platforms and exit)
|
|
2733
|
+
if auto_detect:
|
|
2734
|
+
detected = detector.detect_all(
|
|
2735
|
+
project_path=Path(project_path) if project_path else Path.cwd()
|
|
2736
|
+
)
|
|
2737
|
+
|
|
2738
|
+
if not detected:
|
|
2739
|
+
console.print("[yellow]No AI platforms detected.[/yellow]")
|
|
2740
|
+
console.print("\n[bold]Supported platforms:[/bold]")
|
|
2741
|
+
console.print(" • Claude Code - Project-level configuration")
|
|
2742
|
+
console.print(" • Claude Desktop - Global GUI application")
|
|
2743
|
+
console.print(" • Auggie - CLI tool with global config")
|
|
2744
|
+
console.print(" • Codex - CLI tool with global config")
|
|
2745
|
+
console.print(" • Gemini - CLI tool with project/global config")
|
|
2746
|
+
console.print(
|
|
2747
|
+
"\n[dim]Install these platforms to use them with mcp-ticketer.[/dim]"
|
|
2748
|
+
)
|
|
2749
|
+
return
|
|
2750
|
+
|
|
2751
|
+
console.print("[bold]Detected AI platforms:[/bold]\n")
|
|
2752
|
+
table = Table(show_header=True, header_style="bold cyan")
|
|
2753
|
+
table.add_column("Platform", style="green")
|
|
2754
|
+
table.add_column("Status", style="yellow")
|
|
2755
|
+
table.add_column("Scope", style="blue")
|
|
2756
|
+
table.add_column("Config Path", style="dim")
|
|
2757
|
+
|
|
2758
|
+
for plat in detected:
|
|
2759
|
+
status = "✓ Installed" if plat.is_installed else "⚠ Config Issue"
|
|
2760
|
+
table.add_row(plat.display_name, status, plat.scope, str(plat.config_path))
|
|
2761
|
+
|
|
2762
|
+
console.print(table)
|
|
2763
|
+
console.print(
|
|
2764
|
+
"\n[dim]Run 'mcp-ticketer install <platform>' to configure a specific platform[/dim]"
|
|
2765
|
+
)
|
|
2766
|
+
console.print(
|
|
2767
|
+
"[dim]Run 'mcp-ticketer install --all' to configure all detected platforms[/dim]"
|
|
2768
|
+
)
|
|
2769
|
+
return
|
|
2770
|
+
|
|
2771
|
+
# Handle --all flag (install for all detected platforms)
|
|
2772
|
+
if install_all:
|
|
2773
|
+
detected = detector.detect_all(
|
|
2774
|
+
project_path=Path(project_path) if project_path else Path.cwd()
|
|
2775
|
+
)
|
|
2776
|
+
|
|
2777
|
+
if not detected:
|
|
2778
|
+
console.print("[yellow]No AI platforms detected.[/yellow]")
|
|
2779
|
+
console.print(
|
|
2780
|
+
"Run 'mcp-ticketer install --auto-detect' to see supported platforms."
|
|
2781
|
+
)
|
|
2782
|
+
return
|
|
2783
|
+
|
|
2784
|
+
# Handle dry-run mode - show what would be installed without actually installing
|
|
2785
|
+
if dry_run:
|
|
2786
|
+
console.print(
|
|
2787
|
+
"\n[yellow]DRY RUN - The following platforms would be configured:[/yellow]\n"
|
|
2788
|
+
)
|
|
2789
|
+
|
|
2790
|
+
installable_count = 0
|
|
2791
|
+
for plat in detected:
|
|
2792
|
+
if plat.is_installed:
|
|
2793
|
+
console.print(f" ✓ {plat.display_name} ({plat.scope})")
|
|
2794
|
+
installable_count += 1
|
|
2795
|
+
else:
|
|
2796
|
+
console.print(
|
|
2797
|
+
f" ⚠ {plat.display_name} ({plat.scope}) - would be skipped (configuration issue)"
|
|
2798
|
+
)
|
|
2799
|
+
|
|
2800
|
+
console.print(
|
|
2801
|
+
f"\n[dim]Would configure {installable_count} platform(s)[/dim]"
|
|
2802
|
+
)
|
|
2803
|
+
return
|
|
2804
|
+
|
|
2805
|
+
console.print(
|
|
2806
|
+
f"[bold]Installing for {len(detected)} detected platform(s)...[/bold]\n"
|
|
2807
|
+
)
|
|
2808
|
+
|
|
2809
|
+
# Import configuration functions
|
|
2810
|
+
from .auggie_configure import configure_auggie_mcp
|
|
2811
|
+
from .codex_configure import configure_codex_mcp
|
|
2812
|
+
from .gemini_configure import configure_gemini_mcp
|
|
2813
|
+
from .mcp_configure import configure_claude_mcp
|
|
2814
|
+
|
|
2815
|
+
# Map platform names to configuration functions
|
|
2816
|
+
platform_mapping = {
|
|
2817
|
+
"claude-code": lambda: configure_claude_mcp(
|
|
2818
|
+
global_config=False, force=True
|
|
2819
|
+
),
|
|
2820
|
+
"claude-desktop": lambda: configure_claude_mcp(
|
|
2821
|
+
global_config=True, force=True
|
|
2822
|
+
),
|
|
2823
|
+
"auggie": lambda: configure_auggie_mcp(force=True),
|
|
2824
|
+
"gemini": lambda: configure_gemini_mcp(scope="project", force=True),
|
|
2825
|
+
"codex": lambda: configure_codex_mcp(force=True),
|
|
2826
|
+
}
|
|
2827
|
+
|
|
2828
|
+
success_count = 0
|
|
2829
|
+
failed = []
|
|
2830
|
+
|
|
2831
|
+
for plat in detected:
|
|
2832
|
+
if not plat.is_installed:
|
|
2833
|
+
console.print(
|
|
2834
|
+
f"[yellow]⚠[/yellow] Skipping {plat.display_name} (configuration issue)"
|
|
2835
|
+
)
|
|
2836
|
+
continue
|
|
2837
|
+
|
|
2838
|
+
config_func = platform_mapping.get(plat.name)
|
|
2839
|
+
if not config_func:
|
|
2840
|
+
console.print(
|
|
2841
|
+
f"[yellow]⚠[/yellow] No installer for {plat.display_name}"
|
|
2842
|
+
)
|
|
2843
|
+
continue
|
|
2844
|
+
|
|
2845
|
+
try:
|
|
2846
|
+
console.print(f"[cyan]Installing for {plat.display_name}...[/cyan]")
|
|
2847
|
+
config_func()
|
|
2848
|
+
success_count += 1
|
|
2849
|
+
except Exception as e:
|
|
2850
|
+
console.print(
|
|
2851
|
+
f"[red]✗[/red] Failed to install for {plat.display_name}: {e}"
|
|
2852
|
+
)
|
|
2853
|
+
failed.append(plat.display_name)
|
|
2854
|
+
|
|
2855
|
+
console.print(
|
|
2856
|
+
f"\n[bold]Installation complete:[/bold] {success_count} succeeded"
|
|
2857
|
+
)
|
|
2858
|
+
if failed:
|
|
2859
|
+
console.print(f"[red]Failed:[/red] {', '.join(failed)}")
|
|
2860
|
+
return
|
|
2861
|
+
|
|
2862
|
+
# If no platform argument and no adapter flag, auto-detect and prompt
|
|
2863
|
+
if platform is None and adapter is None:
|
|
2864
|
+
detected = detector.detect_all(
|
|
2865
|
+
project_path=Path(project_path) if project_path else Path.cwd()
|
|
2866
|
+
)
|
|
2867
|
+
|
|
2868
|
+
# Filter to only installed platforms
|
|
2869
|
+
installed = [p for p in detected if p.is_installed]
|
|
2870
|
+
|
|
2871
|
+
if not installed:
|
|
2872
|
+
console.print("[yellow]No AI platforms detected.[/yellow]")
|
|
2873
|
+
console.print("\n[bold]To see supported platforms:[/bold]")
|
|
2874
|
+
console.print(" mcp-ticketer install --auto-detect")
|
|
2875
|
+
console.print("\n[bold]Or run legacy adapter setup:[/bold]")
|
|
2876
|
+
console.print(" mcp-ticketer install --adapter <adapter-type>")
|
|
2877
|
+
return
|
|
2878
|
+
|
|
2879
|
+
# Show detected platforms and prompt for selection
|
|
2880
|
+
console.print("[bold]Detected AI platforms:[/bold]\n")
|
|
2881
|
+
for idx, plat in enumerate(installed, 1):
|
|
2882
|
+
console.print(f" {idx}. {plat.display_name} ({plat.scope})")
|
|
2883
|
+
|
|
2884
|
+
console.print(
|
|
2885
|
+
"\n[dim]Enter the number of the platform to configure, or 'q' to quit:[/dim]"
|
|
2886
|
+
)
|
|
2887
|
+
choice = typer.prompt("Select platform")
|
|
2888
|
+
|
|
2889
|
+
if choice.lower() == "q":
|
|
2890
|
+
console.print("Installation cancelled.")
|
|
2891
|
+
return
|
|
2892
|
+
|
|
2893
|
+
try:
|
|
2894
|
+
idx = int(choice) - 1
|
|
2895
|
+
if idx < 0 or idx >= len(installed):
|
|
2896
|
+
console.print("[red]Invalid selection.[/red]")
|
|
2897
|
+
raise typer.Exit(1) from None
|
|
2898
|
+
platform = installed[idx].name
|
|
2899
|
+
except ValueError as e:
|
|
2900
|
+
console.print("[red]Invalid input. Please enter a number.[/red]")
|
|
2901
|
+
raise typer.Exit(1) from e
|
|
2902
|
+
|
|
2903
|
+
# If platform argument is provided, handle MCP platform installation (NEW SYNTAX)
|
|
2904
|
+
if platform is not None:
|
|
2905
|
+
# Validate that the platform is actually installed
|
|
2906
|
+
platform_info = get_platform_by_name(
|
|
2907
|
+
platform, project_path=Path(project_path) if project_path else Path.cwd()
|
|
2908
|
+
)
|
|
2909
|
+
|
|
2910
|
+
if platform_info and not platform_info.is_installed:
|
|
2911
|
+
console.print(
|
|
2912
|
+
f"[yellow]⚠[/yellow] {platform_info.display_name} was detected but has a configuration issue."
|
|
2913
|
+
)
|
|
2914
|
+
console.print(f"[dim]Config path: {platform_info.config_path}[/dim]\n")
|
|
2915
|
+
|
|
2916
|
+
proceed = typer.confirm(
|
|
2917
|
+
"Do you want to proceed with installation anyway?", default=False
|
|
2918
|
+
)
|
|
2919
|
+
if not proceed:
|
|
2920
|
+
console.print("Installation cancelled.")
|
|
2921
|
+
return
|
|
2922
|
+
|
|
2923
|
+
elif not platform_info:
|
|
2924
|
+
# Platform not detected at all - warn but allow proceeding
|
|
2925
|
+
console.print(
|
|
2926
|
+
f"[yellow]⚠[/yellow] Platform '{platform}' not detected on this system."
|
|
2927
|
+
)
|
|
2928
|
+
console.print(
|
|
2929
|
+
"[dim]Run 'mcp-ticketer install --auto-detect' to see detected platforms.[/dim]\n"
|
|
2930
|
+
)
|
|
2931
|
+
|
|
2932
|
+
proceed = typer.confirm(
|
|
2933
|
+
"Do you want to proceed with installation anyway?", default=False
|
|
2934
|
+
)
|
|
2935
|
+
if not proceed:
|
|
2936
|
+
console.print("Installation cancelled.")
|
|
2937
|
+
return
|
|
2938
|
+
|
|
2939
|
+
# Import configuration functions
|
|
2940
|
+
from .auggie_configure import configure_auggie_mcp
|
|
2941
|
+
from .codex_configure import configure_codex_mcp
|
|
2942
|
+
from .gemini_configure import configure_gemini_mcp
|
|
2943
|
+
from .mcp_configure import configure_claude_mcp
|
|
2944
|
+
|
|
2945
|
+
# Map platform names to configuration functions
|
|
2946
|
+
platform_mapping = {
|
|
2947
|
+
"claude-code": {
|
|
2948
|
+
"func": lambda: configure_claude_mcp(global_config=False, force=True),
|
|
2949
|
+
"name": "Claude Code",
|
|
2950
|
+
},
|
|
2951
|
+
"claude-desktop": {
|
|
2952
|
+
"func": lambda: configure_claude_mcp(global_config=True, force=True),
|
|
2953
|
+
"name": "Claude Desktop",
|
|
2954
|
+
},
|
|
2955
|
+
"auggie": {
|
|
2956
|
+
"func": lambda: configure_auggie_mcp(force=True),
|
|
2957
|
+
"name": "Auggie",
|
|
2958
|
+
},
|
|
2959
|
+
"gemini": {
|
|
2960
|
+
"func": lambda: configure_gemini_mcp(scope="project", force=True),
|
|
2961
|
+
"name": "Gemini CLI",
|
|
2962
|
+
},
|
|
2963
|
+
"codex": {
|
|
2964
|
+
"func": lambda: configure_codex_mcp(force=True),
|
|
2965
|
+
"name": "Codex",
|
|
2966
|
+
},
|
|
2967
|
+
}
|
|
2968
|
+
|
|
2969
|
+
if platform not in platform_mapping:
|
|
2970
|
+
console.print(f"[red]Unknown platform: {platform}[/red]")
|
|
2971
|
+
console.print("\n[bold]Available platforms:[/bold]")
|
|
2972
|
+
for p in platform_mapping.keys():
|
|
2973
|
+
console.print(f" • {p}")
|
|
2974
|
+
raise typer.Exit(1) from None
|
|
2975
|
+
|
|
2976
|
+
config = platform_mapping[platform]
|
|
2977
|
+
|
|
2978
|
+
if dry_run:
|
|
2979
|
+
console.print(f"[cyan]DRY RUN - Would install for {config['name']}[/cyan]")
|
|
2980
|
+
return
|
|
2981
|
+
|
|
2982
|
+
try:
|
|
2983
|
+
config["func"]()
|
|
2984
|
+
except Exception as e:
|
|
2985
|
+
console.print(f"[red]Installation failed: {e}[/red]")
|
|
2986
|
+
raise typer.Exit(1) from e
|
|
2987
|
+
return
|
|
2988
|
+
|
|
2989
|
+
# Otherwise, delegate to init for adapter initialization (LEGACY BEHAVIOR)
|
|
2990
|
+
# This makes 'install' and 'init' synonymous when called without platform argument
|
|
2991
|
+
init(
|
|
2992
|
+
adapter=adapter,
|
|
2993
|
+
project_path=project_path,
|
|
2994
|
+
global_config=global_config,
|
|
2995
|
+
base_path=base_path,
|
|
2996
|
+
api_key=api_key,
|
|
2997
|
+
team_id=team_id,
|
|
2998
|
+
jira_server=jira_server,
|
|
2999
|
+
jira_email=jira_email,
|
|
3000
|
+
jira_project=jira_project,
|
|
3001
|
+
github_owner=github_owner,
|
|
3002
|
+
github_repo=github_repo,
|
|
3003
|
+
github_token=github_token,
|
|
3004
|
+
)
|
|
3005
|
+
|
|
3006
|
+
|
|
3007
|
+
@app.command()
|
|
3008
|
+
def remove(
|
|
3009
|
+
platform: str | None = typer.Argument(
|
|
3010
|
+
None,
|
|
3011
|
+
help="Platform to remove (claude-code, claude-desktop, auggie, gemini, codex)",
|
|
3012
|
+
),
|
|
3013
|
+
dry_run: bool = typer.Option(
|
|
3014
|
+
False, "--dry-run", help="Show what would be done without making changes"
|
|
3015
|
+
),
|
|
3016
|
+
) -> None:
|
|
3017
|
+
"""Remove mcp-ticketer from AI platforms.
|
|
3018
|
+
|
|
3019
|
+
Without arguments, shows help and available platforms.
|
|
3020
|
+
With a platform argument, removes MCP configuration for that platform.
|
|
3021
|
+
|
|
3022
|
+
Examples:
|
|
3023
|
+
# Remove from Claude Code (project-level)
|
|
3024
|
+
mcp-ticketer remove claude-code
|
|
3025
|
+
|
|
3026
|
+
# Remove from Claude Desktop (global)
|
|
3027
|
+
mcp-ticketer remove claude-desktop
|
|
3028
|
+
|
|
3029
|
+
# Remove from Auggie
|
|
3030
|
+
mcp-ticketer remove auggie
|
|
3031
|
+
|
|
3032
|
+
# Dry run to preview changes
|
|
3033
|
+
mcp-ticketer remove claude-code --dry-run
|
|
3034
|
+
|
|
3035
|
+
"""
|
|
3036
|
+
# If no platform specified, show help message
|
|
3037
|
+
if platform is None:
|
|
3038
|
+
console.print("[bold]Remove mcp-ticketer from AI platforms[/bold]\n")
|
|
3039
|
+
console.print("Usage: mcp-ticketer remove <platform>\n")
|
|
3040
|
+
console.print("[bold]Available platforms:[/bold]")
|
|
3041
|
+
console.print(" • claude-code - Claude Code (project-level)")
|
|
3042
|
+
console.print(" • claude-desktop - Claude Desktop (global)")
|
|
3043
|
+
console.print(" • auggie - Auggie (global)")
|
|
3044
|
+
console.print(" • gemini - Gemini CLI (project-level by default)")
|
|
3045
|
+
console.print(" • codex - Codex (global)")
|
|
3046
|
+
return
|
|
3047
|
+
|
|
3048
|
+
# Import removal functions
|
|
3049
|
+
from .auggie_configure import remove_auggie_mcp
|
|
3050
|
+
from .codex_configure import remove_codex_mcp
|
|
3051
|
+
from .gemini_configure import remove_gemini_mcp
|
|
3052
|
+
from .mcp_configure import remove_claude_mcp
|
|
3053
|
+
|
|
3054
|
+
# Map platform names to removal functions
|
|
3055
|
+
platform_mapping = {
|
|
3056
|
+
"claude-code": {
|
|
3057
|
+
"func": lambda: remove_claude_mcp(global_config=False, dry_run=dry_run),
|
|
3058
|
+
"name": "Claude Code",
|
|
3059
|
+
},
|
|
3060
|
+
"claude-desktop": {
|
|
3061
|
+
"func": lambda: remove_claude_mcp(global_config=True, dry_run=dry_run),
|
|
3062
|
+
"name": "Claude Desktop",
|
|
3063
|
+
},
|
|
3064
|
+
"auggie": {
|
|
3065
|
+
"func": lambda: remove_auggie_mcp(dry_run=dry_run),
|
|
3066
|
+
"name": "Auggie",
|
|
3067
|
+
},
|
|
3068
|
+
"gemini": {
|
|
3069
|
+
"func": lambda: remove_gemini_mcp(scope="project", dry_run=dry_run),
|
|
3070
|
+
"name": "Gemini CLI",
|
|
3071
|
+
},
|
|
3072
|
+
"codex": {
|
|
3073
|
+
"func": lambda: remove_codex_mcp(dry_run=dry_run),
|
|
3074
|
+
"name": "Codex",
|
|
3075
|
+
},
|
|
3076
|
+
}
|
|
3077
|
+
|
|
3078
|
+
if platform not in platform_mapping:
|
|
3079
|
+
console.print(f"[red]Unknown platform: {platform}[/red]")
|
|
3080
|
+
console.print("\n[bold]Available platforms:[/bold]")
|
|
3081
|
+
for p in platform_mapping.keys():
|
|
3082
|
+
console.print(f" • {p}")
|
|
3083
|
+
raise typer.Exit(1) from None
|
|
3084
|
+
|
|
3085
|
+
config = platform_mapping[platform]
|
|
3086
|
+
|
|
3087
|
+
try:
|
|
3088
|
+
config["func"]()
|
|
3089
|
+
except Exception as e:
|
|
3090
|
+
console.print(f"[red]Removal failed: {e}[/red]")
|
|
3091
|
+
raise typer.Exit(1) from e
|
|
3092
|
+
|
|
3093
|
+
|
|
3094
|
+
@app.command()
|
|
3095
|
+
def uninstall(
|
|
3096
|
+
platform: str | None = typer.Argument(
|
|
3097
|
+
None,
|
|
3098
|
+
help="Platform to uninstall (claude-code, claude-desktop, auggie, gemini, codex)",
|
|
3099
|
+
),
|
|
3100
|
+
dry_run: bool = typer.Option(
|
|
3101
|
+
False, "--dry-run", help="Show what would be done without making changes"
|
|
3102
|
+
),
|
|
3103
|
+
) -> None:
|
|
3104
|
+
"""Uninstall mcp-ticketer from AI platforms (alias for remove).
|
|
3105
|
+
|
|
3106
|
+
This is an alias for the 'remove' command.
|
|
3107
|
+
|
|
3108
|
+
Without arguments, shows help and available platforms.
|
|
3109
|
+
With a platform argument, removes MCP configuration for that platform.
|
|
3110
|
+
|
|
3111
|
+
Examples:
|
|
3112
|
+
# Uninstall from Claude Code (project-level)
|
|
3113
|
+
mcp-ticketer uninstall claude-code
|
|
3114
|
+
|
|
3115
|
+
# Uninstall from Claude Desktop (global)
|
|
3116
|
+
mcp-ticketer uninstall claude-desktop
|
|
3117
|
+
|
|
3118
|
+
# Uninstall from Auggie
|
|
3119
|
+
mcp-ticketer uninstall auggie
|
|
3120
|
+
|
|
3121
|
+
# Dry run to preview changes
|
|
3122
|
+
mcp-ticketer uninstall claude-code --dry-run
|
|
3123
|
+
|
|
3124
|
+
"""
|
|
3125
|
+
# Call the remove command with the same parameters
|
|
3126
|
+
remove(platform=platform, dry_run=dry_run)
|
|
3127
|
+
|
|
3128
|
+
|
|
3129
|
+
@app.command(deprecated=True, hidden=True)
|
|
3130
|
+
def check(queue_id: str = typer.Argument(..., help="Queue ID to check")) -> None:
|
|
3131
|
+
"""Check status of a queued operation.
|
|
3132
|
+
|
|
3133
|
+
DEPRECATED: Use 'mcp-ticketer ticket check' instead.
|
|
3134
|
+
"""
|
|
3135
|
+
console.print(
|
|
3136
|
+
"[yellow]⚠️ This command is deprecated. Use 'mcp-ticketer ticket check' instead.[/yellow]\n"
|
|
3137
|
+
)
|
|
1869
3138
|
queue = Queue()
|
|
1870
3139
|
item = queue.get_item(queue_id)
|
|
1871
3140
|
|
|
1872
3141
|
if not item:
|
|
1873
3142
|
console.print(f"[red]Queue item not found: {queue_id}[/red]")
|
|
1874
|
-
raise typer.Exit(1)
|
|
3143
|
+
raise typer.Exit(1) from None
|
|
1875
3144
|
|
|
1876
3145
|
# Display status
|
|
1877
3146
|
console.print(f"\n[bold]Queue Item: {item.id}[/bold]")
|
|
@@ -1905,19 +3174,19 @@ def check(queue_id: str = typer.Argument(..., help="Queue ID to check")):
|
|
|
1905
3174
|
console.print(f"\nRetry Count: {item.retry_count}")
|
|
1906
3175
|
|
|
1907
3176
|
|
|
1908
|
-
@
|
|
1909
|
-
def
|
|
1910
|
-
adapter:
|
|
3177
|
+
@mcp_app.command(name="serve")
|
|
3178
|
+
def mcp_serve(
|
|
3179
|
+
adapter: AdapterType | None = typer.Option(
|
|
1911
3180
|
None, "--adapter", "-a", help="Override default adapter type"
|
|
1912
3181
|
),
|
|
1913
|
-
base_path:
|
|
3182
|
+
base_path: str | None = typer.Option(
|
|
1914
3183
|
None, "--base-path", help="Base path for AITrackdown adapter"
|
|
1915
3184
|
),
|
|
1916
|
-
):
|
|
3185
|
+
) -> None:
|
|
1917
3186
|
"""Start MCP server for JSON-RPC communication over stdio.
|
|
1918
3187
|
|
|
1919
3188
|
This command is used by Claude Code/Desktop when connecting to the MCP server.
|
|
1920
|
-
You typically don't need to run this manually - use 'mcp-ticketer
|
|
3189
|
+
You typically don't need to run this manually - use 'mcp-ticketer install add' to configure.
|
|
1921
3190
|
|
|
1922
3191
|
Configuration Resolution:
|
|
1923
3192
|
- When MCP server starts, it uses the current working directory (cwd)
|
|
@@ -1927,12 +3196,13 @@ def serve(
|
|
|
1927
3196
|
2. Global: ~/.mcp-ticketer/config.json
|
|
1928
3197
|
3. Default: aitrackdown adapter with .aitrackdown base path
|
|
1929
3198
|
"""
|
|
1930
|
-
from ..mcp.server import
|
|
3199
|
+
from ..mcp.server.server_sdk import configure_adapter
|
|
3200
|
+
from ..mcp.server.server_sdk import main as sdk_main
|
|
1931
3201
|
|
|
1932
3202
|
# Load configuration (respects project-specific config in cwd)
|
|
1933
3203
|
config = load_config()
|
|
1934
3204
|
|
|
1935
|
-
# Determine adapter type with priority: CLI arg > .env files >
|
|
3205
|
+
# Determine adapter type with priority: CLI arg > config > .env files > default
|
|
1936
3206
|
if adapter:
|
|
1937
3207
|
# Priority 1: Command line argument
|
|
1938
3208
|
adapter_type = adapter.value
|
|
@@ -1940,18 +3210,24 @@ def serve(
|
|
|
1940
3210
|
adapters_config = config.get("adapters", {})
|
|
1941
3211
|
adapter_config = adapters_config.get(adapter_type, {})
|
|
1942
3212
|
else:
|
|
1943
|
-
# Priority 2:
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
env_config = _load_env_configuration()
|
|
1947
|
-
if env_config:
|
|
1948
|
-
adapter_type = env_config["adapter_type"]
|
|
1949
|
-
adapter_config = env_config["adapter_config"]
|
|
1950
|
-
else:
|
|
1951
|
-
# Priority 3: Configuration file
|
|
1952
|
-
adapter_type = config.get("default_adapter", "aitrackdown")
|
|
3213
|
+
# Priority 2: Configuration file (project-specific)
|
|
3214
|
+
adapter_type = config.get("default_adapter")
|
|
3215
|
+
if adapter_type:
|
|
1953
3216
|
adapters_config = config.get("adapters", {})
|
|
1954
3217
|
adapter_config = adapters_config.get(adapter_type, {})
|
|
3218
|
+
else:
|
|
3219
|
+
# Priority 3: .env files (auto-detection fallback)
|
|
3220
|
+
from ..mcp.server.main import _load_env_configuration
|
|
3221
|
+
|
|
3222
|
+
env_config = _load_env_configuration()
|
|
3223
|
+
if env_config:
|
|
3224
|
+
adapter_type = env_config["adapter_type"]
|
|
3225
|
+
adapter_config = env_config["adapter_config"]
|
|
3226
|
+
else:
|
|
3227
|
+
# Priority 4: Default fallback
|
|
3228
|
+
adapter_type = "aitrackdown"
|
|
3229
|
+
adapters_config = config.get("adapters", {})
|
|
3230
|
+
adapter_config = adapters_config.get(adapter_type, {})
|
|
1955
3231
|
|
|
1956
3232
|
# Override with command line options if provided (highest priority)
|
|
1957
3233
|
if base_path and adapter_type == "aitrackdown":
|
|
@@ -1968,21 +3244,22 @@ def serve(
|
|
|
1968
3244
|
if sys.stderr.isatty():
|
|
1969
3245
|
# Only print if stderr is a terminal (not redirected)
|
|
1970
3246
|
console.file = sys.stderr
|
|
1971
|
-
console.print(
|
|
3247
|
+
console.print(
|
|
3248
|
+
f"[green]Starting MCP SDK server[/green] with {adapter_type} adapter"
|
|
3249
|
+
)
|
|
1972
3250
|
console.print(
|
|
1973
3251
|
"[dim]Server running on stdio. Send JSON-RPC requests via stdin.[/dim]"
|
|
1974
3252
|
)
|
|
1975
3253
|
|
|
1976
|
-
#
|
|
3254
|
+
# Configure adapter and run SDK server
|
|
1977
3255
|
try:
|
|
1978
|
-
|
|
1979
|
-
|
|
3256
|
+
configure_adapter(adapter_type, adapter_config)
|
|
3257
|
+
sdk_main()
|
|
1980
3258
|
except KeyboardInterrupt:
|
|
1981
|
-
#
|
|
3259
|
+
# Send this to stderr
|
|
1982
3260
|
if sys.stderr.isatty():
|
|
1983
3261
|
console.print("\n[yellow]Server stopped by user[/yellow]")
|
|
1984
|
-
|
|
1985
|
-
asyncio.run(server.stop())
|
|
3262
|
+
sys.exit(0)
|
|
1986
3263
|
except Exception as e:
|
|
1987
3264
|
# Log error to stderr
|
|
1988
3265
|
sys.stderr.write(f"MCP server error: {e}\n")
|
|
@@ -2000,7 +3277,7 @@ def mcp_claude(
|
|
|
2000
3277
|
force: bool = typer.Option(
|
|
2001
3278
|
False, "--force", "-f", help="Overwrite existing configuration"
|
|
2002
3279
|
),
|
|
2003
|
-
):
|
|
3280
|
+
) -> None:
|
|
2004
3281
|
"""Configure Claude Code to use mcp-ticketer MCP server.
|
|
2005
3282
|
|
|
2006
3283
|
Reads configuration from .mcp-ticketer/config.json and updates
|
|
@@ -2026,7 +3303,7 @@ def mcp_claude(
|
|
|
2026
3303
|
configure_claude_mcp(global_config=global_config, force=force)
|
|
2027
3304
|
except Exception as e:
|
|
2028
3305
|
console.print(f"[red]✗ Configuration failed:[/red] {e}")
|
|
2029
|
-
raise typer.Exit(1)
|
|
3306
|
+
raise typer.Exit(1) from e
|
|
2030
3307
|
|
|
2031
3308
|
|
|
2032
3309
|
@mcp_app.command(name="gemini")
|
|
@@ -2040,7 +3317,7 @@ def mcp_gemini(
|
|
|
2040
3317
|
force: bool = typer.Option(
|
|
2041
3318
|
False, "--force", "-f", help="Overwrite existing configuration"
|
|
2042
3319
|
),
|
|
2043
|
-
):
|
|
3320
|
+
) -> None:
|
|
2044
3321
|
"""Configure Gemini CLI to use mcp-ticketer MCP server.
|
|
2045
3322
|
|
|
2046
3323
|
Reads configuration from .mcp-ticketer/config.json and creates
|
|
@@ -2067,13 +3344,13 @@ def mcp_gemini(
|
|
|
2067
3344
|
console.print(
|
|
2068
3345
|
f"[red]✗ Invalid scope:[/red] '{scope}'. Must be 'project' or 'user'"
|
|
2069
3346
|
)
|
|
2070
|
-
raise typer.Exit(1)
|
|
3347
|
+
raise typer.Exit(1) from None
|
|
2071
3348
|
|
|
2072
3349
|
try:
|
|
2073
3350
|
configure_gemini_mcp(scope=scope, force=force) # type: ignore
|
|
2074
3351
|
except Exception as e:
|
|
2075
3352
|
console.print(f"[red]✗ Configuration failed:[/red] {e}")
|
|
2076
|
-
raise typer.Exit(1)
|
|
3353
|
+
raise typer.Exit(1) from e
|
|
2077
3354
|
|
|
2078
3355
|
|
|
2079
3356
|
@mcp_app.command(name="codex")
|
|
@@ -2081,7 +3358,7 @@ def mcp_codex(
|
|
|
2081
3358
|
force: bool = typer.Option(
|
|
2082
3359
|
False, "--force", "-f", help="Overwrite existing configuration"
|
|
2083
3360
|
),
|
|
2084
|
-
):
|
|
3361
|
+
) -> None:
|
|
2085
3362
|
"""Configure Codex CLI to use mcp-ticketer MCP server.
|
|
2086
3363
|
|
|
2087
3364
|
Reads configuration from .mcp-ticketer/config.json and creates
|
|
@@ -2105,7 +3382,7 @@ def mcp_codex(
|
|
|
2105
3382
|
configure_codex_mcp(force=force)
|
|
2106
3383
|
except Exception as e:
|
|
2107
3384
|
console.print(f"[red]✗ Configuration failed:[/red] {e}")
|
|
2108
|
-
raise typer.Exit(1)
|
|
3385
|
+
raise typer.Exit(1) from e
|
|
2109
3386
|
|
|
2110
3387
|
|
|
2111
3388
|
@mcp_app.command(name="auggie")
|
|
@@ -2113,7 +3390,7 @@ def mcp_auggie(
|
|
|
2113
3390
|
force: bool = typer.Option(
|
|
2114
3391
|
False, "--force", "-f", help="Overwrite existing configuration"
|
|
2115
3392
|
),
|
|
2116
|
-
):
|
|
3393
|
+
) -> None:
|
|
2117
3394
|
"""Configure Auggie CLI to use mcp-ticketer MCP server.
|
|
2118
3395
|
|
|
2119
3396
|
Reads configuration from .mcp-ticketer/config.json and creates
|
|
@@ -2137,16 +3414,134 @@ def mcp_auggie(
|
|
|
2137
3414
|
configure_auggie_mcp(force=force)
|
|
2138
3415
|
except Exception as e:
|
|
2139
3416
|
console.print(f"[red]✗ Configuration failed:[/red] {e}")
|
|
2140
|
-
raise typer.Exit(1)
|
|
3417
|
+
raise typer.Exit(1) from e
|
|
3418
|
+
|
|
3419
|
+
|
|
3420
|
+
@mcp_app.command(name="status")
|
|
3421
|
+
def mcp_status() -> None:
|
|
3422
|
+
"""Check MCP server status.
|
|
3423
|
+
|
|
3424
|
+
Shows whether the MCP server is configured and running for various platforms.
|
|
3425
|
+
|
|
3426
|
+
Examples:
|
|
3427
|
+
mcp-ticketer mcp status
|
|
3428
|
+
|
|
3429
|
+
"""
|
|
3430
|
+
import json
|
|
3431
|
+
from pathlib import Path
|
|
3432
|
+
|
|
3433
|
+
console.print("[bold]MCP Server Status[/bold]\n")
|
|
3434
|
+
|
|
3435
|
+
# Check project-level configuration
|
|
3436
|
+
project_config = Path.cwd() / ".mcp-ticketer" / "config.json"
|
|
3437
|
+
if project_config.exists():
|
|
3438
|
+
console.print(f"[green]✓[/green] Project config found: {project_config}")
|
|
3439
|
+
try:
|
|
3440
|
+
with open(project_config) as f:
|
|
3441
|
+
config = json.load(f)
|
|
3442
|
+
adapter = config.get("default_adapter", "aitrackdown")
|
|
3443
|
+
console.print(f" Default adapter: [cyan]{adapter}[/cyan]")
|
|
3444
|
+
except Exception as e:
|
|
3445
|
+
console.print(f" [yellow]Warning: Could not read config: {e}[/yellow]")
|
|
3446
|
+
else:
|
|
3447
|
+
console.print("[yellow]○[/yellow] No project config found")
|
|
3448
|
+
|
|
3449
|
+
# Check Claude Code configuration
|
|
3450
|
+
claude_code_config = Path.cwd() / ".mcp" / "config.json"
|
|
3451
|
+
if claude_code_config.exists():
|
|
3452
|
+
console.print(
|
|
3453
|
+
f"\n[green]✓[/green] Claude Code configured: {claude_code_config}"
|
|
3454
|
+
)
|
|
3455
|
+
else:
|
|
3456
|
+
console.print("\n[yellow]○[/yellow] Claude Code not configured")
|
|
3457
|
+
|
|
3458
|
+
# Check Claude Desktop configuration
|
|
3459
|
+
claude_desktop_config = (
|
|
3460
|
+
Path.home()
|
|
3461
|
+
/ "Library"
|
|
3462
|
+
/ "Application Support"
|
|
3463
|
+
/ "Claude"
|
|
3464
|
+
/ "claude_desktop_config.json"
|
|
3465
|
+
)
|
|
3466
|
+
if claude_desktop_config.exists():
|
|
3467
|
+
try:
|
|
3468
|
+
with open(claude_desktop_config) as f:
|
|
3469
|
+
config = json.load(f)
|
|
3470
|
+
if "mcpServers" in config and "mcp-ticketer" in config["mcpServers"]:
|
|
3471
|
+
console.print(
|
|
3472
|
+
f"[green]✓[/green] Claude Desktop configured: {claude_desktop_config}"
|
|
3473
|
+
)
|
|
3474
|
+
else:
|
|
3475
|
+
console.print(
|
|
3476
|
+
"[yellow]○[/yellow] Claude Desktop config exists but mcp-ticketer not found"
|
|
3477
|
+
)
|
|
3478
|
+
except Exception:
|
|
3479
|
+
console.print(
|
|
3480
|
+
"[yellow]○[/yellow] Claude Desktop config exists but could not be read"
|
|
3481
|
+
)
|
|
3482
|
+
else:
|
|
3483
|
+
console.print("[yellow]○[/yellow] Claude Desktop not configured")
|
|
3484
|
+
|
|
3485
|
+
# Check Gemini configuration
|
|
3486
|
+
gemini_project_config = Path.cwd() / ".gemini" / "settings.json"
|
|
3487
|
+
gemini_user_config = Path.home() / ".gemini" / "settings.json"
|
|
3488
|
+
if gemini_project_config.exists():
|
|
3489
|
+
console.print(
|
|
3490
|
+
f"\n[green]✓[/green] Gemini (project) configured: {gemini_project_config}"
|
|
3491
|
+
)
|
|
3492
|
+
elif gemini_user_config.exists():
|
|
3493
|
+
console.print(
|
|
3494
|
+
f"\n[green]✓[/green] Gemini (user) configured: {gemini_user_config}"
|
|
3495
|
+
)
|
|
3496
|
+
else:
|
|
3497
|
+
console.print("\n[yellow]○[/yellow] Gemini not configured")
|
|
3498
|
+
|
|
3499
|
+
# Check Codex configuration
|
|
3500
|
+
codex_config = Path.home() / ".codex" / "config.toml"
|
|
3501
|
+
if codex_config.exists():
|
|
3502
|
+
console.print(f"[green]✓[/green] Codex configured: {codex_config}")
|
|
3503
|
+
else:
|
|
3504
|
+
console.print("[yellow]○[/yellow] Codex not configured")
|
|
3505
|
+
|
|
3506
|
+
# Check Auggie configuration
|
|
3507
|
+
auggie_config = Path.home() / ".augment" / "settings.json"
|
|
3508
|
+
if auggie_config.exists():
|
|
3509
|
+
console.print(f"[green]✓[/green] Auggie configured: {auggie_config}")
|
|
3510
|
+
else:
|
|
3511
|
+
console.print("[yellow]○[/yellow] Auggie not configured")
|
|
3512
|
+
|
|
3513
|
+
console.print(
|
|
3514
|
+
"\n[dim]Run 'mcp-ticketer install <platform>' to configure a platform[/dim]"
|
|
3515
|
+
)
|
|
3516
|
+
|
|
3517
|
+
|
|
3518
|
+
@mcp_app.command(name="stop")
|
|
3519
|
+
def mcp_stop() -> None:
|
|
3520
|
+
"""Stop MCP server (placeholder - MCP runs on-demand via stdio).
|
|
3521
|
+
|
|
3522
|
+
Note: The MCP server runs on-demand when AI clients connect via stdio.
|
|
3523
|
+
It doesn't run as a persistent background service, so there's nothing to stop.
|
|
3524
|
+
This command is provided for consistency but has no effect.
|
|
3525
|
+
|
|
3526
|
+
Examples:
|
|
3527
|
+
mcp-ticketer mcp stop
|
|
3528
|
+
|
|
3529
|
+
"""
|
|
3530
|
+
console.print(
|
|
3531
|
+
"[yellow]ℹ[/yellow] MCP server runs on-demand via stdio (not as a background service)"
|
|
3532
|
+
)
|
|
3533
|
+
console.print("There is no persistent server process to stop.")
|
|
3534
|
+
console.print(
|
|
3535
|
+
"\n[dim]The server starts automatically when AI clients connect and stops when they disconnect.[/dim]"
|
|
3536
|
+
)
|
|
2141
3537
|
|
|
2142
3538
|
|
|
2143
3539
|
# Add command groups to main app (must be after all subcommands are defined)
|
|
2144
|
-
app.add_typer(linear_app, name="linear")
|
|
2145
3540
|
app.add_typer(mcp_app, name="mcp")
|
|
2146
3541
|
|
|
2147
3542
|
|
|
2148
|
-
def main():
|
|
2149
|
-
"""
|
|
3543
|
+
def main() -> None:
|
|
3544
|
+
"""Execute the main CLI application entry point."""
|
|
2150
3545
|
app()
|
|
2151
3546
|
|
|
2152
3547
|
|