mcp-ticketer 0.1.30__py3-none-any.whl → 1.2.11__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of mcp-ticketer might be problematic. Click here for more details.

Files changed (109) hide show
  1. mcp_ticketer/__init__.py +10 -10
  2. mcp_ticketer/__version__.py +3 -3
  3. mcp_ticketer/adapters/__init__.py +2 -0
  4. mcp_ticketer/adapters/aitrackdown.py +796 -46
  5. mcp_ticketer/adapters/asana/__init__.py +15 -0
  6. mcp_ticketer/adapters/asana/adapter.py +1416 -0
  7. mcp_ticketer/adapters/asana/client.py +292 -0
  8. mcp_ticketer/adapters/asana/mappers.py +348 -0
  9. mcp_ticketer/adapters/asana/types.py +146 -0
  10. mcp_ticketer/adapters/github.py +879 -129
  11. mcp_ticketer/adapters/hybrid.py +11 -11
  12. mcp_ticketer/adapters/jira.py +973 -73
  13. mcp_ticketer/adapters/linear/__init__.py +24 -0
  14. mcp_ticketer/adapters/linear/adapter.py +2732 -0
  15. mcp_ticketer/adapters/linear/client.py +344 -0
  16. mcp_ticketer/adapters/linear/mappers.py +420 -0
  17. mcp_ticketer/adapters/linear/queries.py +479 -0
  18. mcp_ticketer/adapters/linear/types.py +360 -0
  19. mcp_ticketer/adapters/linear.py +10 -2315
  20. mcp_ticketer/analysis/__init__.py +23 -0
  21. mcp_ticketer/analysis/orphaned.py +218 -0
  22. mcp_ticketer/analysis/similarity.py +224 -0
  23. mcp_ticketer/analysis/staleness.py +266 -0
  24. mcp_ticketer/cache/memory.py +9 -8
  25. mcp_ticketer/cli/adapter_diagnostics.py +421 -0
  26. mcp_ticketer/cli/auggie_configure.py +116 -15
  27. mcp_ticketer/cli/codex_configure.py +274 -82
  28. mcp_ticketer/cli/configure.py +888 -151
  29. mcp_ticketer/cli/diagnostics.py +400 -157
  30. mcp_ticketer/cli/discover.py +297 -26
  31. mcp_ticketer/cli/gemini_configure.py +119 -26
  32. mcp_ticketer/cli/init_command.py +880 -0
  33. mcp_ticketer/cli/instruction_commands.py +435 -0
  34. mcp_ticketer/cli/linear_commands.py +616 -0
  35. mcp_ticketer/cli/main.py +203 -1165
  36. mcp_ticketer/cli/mcp_configure.py +474 -90
  37. mcp_ticketer/cli/mcp_server_commands.py +415 -0
  38. mcp_ticketer/cli/migrate_config.py +12 -8
  39. mcp_ticketer/cli/platform_commands.py +123 -0
  40. mcp_ticketer/cli/platform_detection.py +418 -0
  41. mcp_ticketer/cli/platform_installer.py +513 -0
  42. mcp_ticketer/cli/python_detection.py +126 -0
  43. mcp_ticketer/cli/queue_commands.py +15 -15
  44. mcp_ticketer/cli/setup_command.py +639 -0
  45. mcp_ticketer/cli/simple_health.py +90 -65
  46. mcp_ticketer/cli/ticket_commands.py +1013 -0
  47. mcp_ticketer/cli/update_checker.py +313 -0
  48. mcp_ticketer/cli/utils.py +114 -66
  49. mcp_ticketer/core/__init__.py +24 -1
  50. mcp_ticketer/core/adapter.py +250 -16
  51. mcp_ticketer/core/config.py +145 -37
  52. mcp_ticketer/core/env_discovery.py +101 -22
  53. mcp_ticketer/core/env_loader.py +349 -0
  54. mcp_ticketer/core/exceptions.py +160 -0
  55. mcp_ticketer/core/http_client.py +26 -26
  56. mcp_ticketer/core/instructions.py +405 -0
  57. mcp_ticketer/core/label_manager.py +732 -0
  58. mcp_ticketer/core/mappers.py +42 -30
  59. mcp_ticketer/core/models.py +280 -28
  60. mcp_ticketer/core/onepassword_secrets.py +379 -0
  61. mcp_ticketer/core/project_config.py +183 -49
  62. mcp_ticketer/core/registry.py +3 -3
  63. mcp_ticketer/core/session_state.py +171 -0
  64. mcp_ticketer/core/state_matcher.py +592 -0
  65. mcp_ticketer/core/url_parser.py +425 -0
  66. mcp_ticketer/core/validators.py +69 -0
  67. mcp_ticketer/defaults/ticket_instructions.md +644 -0
  68. mcp_ticketer/mcp/__init__.py +29 -1
  69. mcp_ticketer/mcp/__main__.py +60 -0
  70. mcp_ticketer/mcp/server/__init__.py +25 -0
  71. mcp_ticketer/mcp/server/__main__.py +60 -0
  72. mcp_ticketer/mcp/server/constants.py +58 -0
  73. mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
  74. mcp_ticketer/mcp/server/dto.py +195 -0
  75. mcp_ticketer/mcp/server/main.py +1343 -0
  76. mcp_ticketer/mcp/server/response_builder.py +206 -0
  77. mcp_ticketer/mcp/server/routing.py +655 -0
  78. mcp_ticketer/mcp/server/server_sdk.py +151 -0
  79. mcp_ticketer/mcp/server/tools/__init__.py +56 -0
  80. mcp_ticketer/mcp/server/tools/analysis_tools.py +495 -0
  81. mcp_ticketer/mcp/server/tools/attachment_tools.py +226 -0
  82. mcp_ticketer/mcp/server/tools/bulk_tools.py +273 -0
  83. mcp_ticketer/mcp/server/tools/comment_tools.py +152 -0
  84. mcp_ticketer/mcp/server/tools/config_tools.py +1439 -0
  85. mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
  86. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +921 -0
  87. mcp_ticketer/mcp/server/tools/instruction_tools.py +300 -0
  88. mcp_ticketer/mcp/server/tools/label_tools.py +948 -0
  89. mcp_ticketer/mcp/server/tools/pr_tools.py +152 -0
  90. mcp_ticketer/mcp/server/tools/search_tools.py +215 -0
  91. mcp_ticketer/mcp/server/tools/session_tools.py +170 -0
  92. mcp_ticketer/mcp/server/tools/ticket_tools.py +1268 -0
  93. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +547 -0
  94. mcp_ticketer/queue/__init__.py +1 -0
  95. mcp_ticketer/queue/health_monitor.py +168 -136
  96. mcp_ticketer/queue/manager.py +95 -25
  97. mcp_ticketer/queue/queue.py +40 -21
  98. mcp_ticketer/queue/run_worker.py +6 -1
  99. mcp_ticketer/queue/ticket_registry.py +213 -155
  100. mcp_ticketer/queue/worker.py +109 -49
  101. mcp_ticketer-1.2.11.dist-info/METADATA +792 -0
  102. mcp_ticketer-1.2.11.dist-info/RECORD +110 -0
  103. mcp_ticketer/mcp/server.py +0 -1895
  104. mcp_ticketer-0.1.30.dist-info/METADATA +0 -413
  105. mcp_ticketer-0.1.30.dist-info/RECORD +0 -49
  106. {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/WHEEL +0 -0
  107. {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/entry_points.txt +0 -0
  108. {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/licenses/LICENSE +0 -0
  109. {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/top_level.txt +0 -0
@@ -1,12 +1,18 @@
1
1
  """CLI command for auto-discovering configuration from .env files."""
2
2
 
3
3
  from pathlib import Path
4
- from typing import Optional
5
4
 
6
5
  import typer
7
6
  from rich.console import Console
7
+ from rich.panel import Panel
8
+ from rich.table import Table
8
9
 
9
10
  from ..core.env_discovery import DiscoveredAdapter, EnvDiscovery
11
+ from ..core.onepassword_secrets import (
12
+ OnePasswordConfig,
13
+ OnePasswordSecretsLoader,
14
+ check_op_cli_status,
15
+ )
10
16
  from ..core.project_config import (
11
17
  AdapterConfig,
12
18
  ConfigResolver,
@@ -22,10 +28,12 @@ def _mask_sensitive(value: str, key: str) -> str:
22
28
  """Mask sensitive values for display.
23
29
 
24
30
  Args:
31
+ ----
25
32
  value: Value to potentially mask
26
33
  key: Key name to determine if masking needed
27
34
 
28
35
  Returns:
36
+ -------
29
37
  Masked or original value
30
38
 
31
39
  """
@@ -55,6 +63,7 @@ def _display_discovered_adapter(
55
63
  """Display information about a discovered adapter.
56
64
 
57
65
  Args:
66
+ ----
58
67
  adapter: Discovered adapter to display
59
68
  discovery: EnvDiscovery instance for validation
60
69
 
@@ -93,7 +102,7 @@ def _display_discovered_adapter(
93
102
 
94
103
  @app.command()
95
104
  def show(
96
- project_path: Optional[Path] = typer.Option(
105
+ project_path: Path | None = typer.Option(
97
106
  None,
98
107
  "--path",
99
108
  "-p",
@@ -148,7 +157,7 @@ def show(
148
157
 
149
158
  @app.command()
150
159
  def save(
151
- adapter: Optional[str] = typer.Option(
160
+ adapter: str | None = typer.Option(
152
161
  None, "--adapter", "-a", help="Which adapter to save (defaults to recommended)"
153
162
  ),
154
163
  global_config: bool = typer.Option(
@@ -157,7 +166,7 @@ def save(
157
166
  dry_run: bool = typer.Option(
158
167
  False, "--dry-run", help="Show what would be saved without saving"
159
168
  ),
160
- project_path: Optional[Path] = typer.Option(
169
+ project_path: Path | None = typer.Option(
161
170
  None,
162
171
  "--path",
163
172
  "-p",
@@ -182,7 +191,7 @@ def save(
182
191
  console.print(
183
192
  "[dim]Make sure your .env file contains adapter credentials[/dim]"
184
193
  )
185
- raise typer.Exit(1)
194
+ raise typer.Exit(1) from None
186
195
 
187
196
  # Determine which adapter to save
188
197
  if adapter:
@@ -192,13 +201,13 @@ def save(
192
201
  console.print(
193
202
  f"[dim]Available: {', '.join(a.adapter_type for a in result.adapters)}[/dim]"
194
203
  )
195
- raise typer.Exit(1)
204
+ raise typer.Exit(1) from None
196
205
  else:
197
206
  # Use recommended adapter
198
207
  discovered_adapter = result.get_primary_adapter()
199
208
  if not discovered_adapter:
200
209
  console.print("[red]Could not determine recommended adapter[/red]")
201
- raise typer.Exit(1)
210
+ raise typer.Exit(1) from None
202
211
 
203
212
  console.print(
204
213
  f"[bold]Using recommended adapter:[/bold] {discovered_adapter.adapter_type}"
@@ -217,7 +226,7 @@ def save(
217
226
  console.print(
218
227
  "[dim]Fix the configuration in your .env file and try again[/dim]"
219
228
  )
220
- raise typer.Exit(1)
229
+ raise typer.Exit(1) from None
221
230
 
222
231
  if dry_run:
223
232
  console.print("\n[yellow]Dry run - no changes made[/yellow]")
@@ -240,14 +249,15 @@ def save(
240
249
  # Add to config
241
250
  config.adapters[discovered_adapter.adapter_type] = adapter_config
242
251
 
243
- # Save
252
+ # Save (always to project config for security)
244
253
  try:
254
+ resolver.save_project_config(config, proj_path)
255
+ config_location = proj_path / resolver.PROJECT_CONFIG_SUBPATH
256
+
245
257
  if global_config:
246
- resolver.save_global_config(config)
247
- config_location = resolver.GLOBAL_CONFIG_PATH
248
- else:
249
- resolver.save_project_config(config, proj_path)
250
- config_location = proj_path / resolver.PROJECT_CONFIG_SUBPATH
258
+ console.print(
259
+ "[yellow]Note: Global config deprecated for security. Saved to project config instead.[/yellow]"
260
+ )
251
261
 
252
262
  console.print(f"\n[green]✅ Configuration saved to:[/green] {config_location}")
253
263
  console.print(
@@ -256,12 +266,12 @@ def save(
256
266
 
257
267
  except Exception as e:
258
268
  console.print(f"\n[red]Failed to save configuration:[/red] {e}")
259
- raise typer.Exit(1)
269
+ raise typer.Exit(1) from None
260
270
 
261
271
 
262
272
  @app.command()
263
273
  def interactive(
264
- project_path: Optional[Path] = typer.Option(
274
+ project_path: Path | None = typer.Option(
265
275
  None,
266
276
  "--path",
267
277
  "-p",
@@ -284,7 +294,7 @@ def interactive(
284
294
  console.print(f" ✅ {env_file}")
285
295
  else:
286
296
  console.print("[red]No .env files found[/red]")
287
- raise typer.Exit(1)
297
+ raise typer.Exit(1) from None
288
298
 
289
299
  # Show discovered adapters
290
300
  if not result.adapters:
@@ -292,7 +302,7 @@ def interactive(
292
302
  console.print(
293
303
  "[dim]Make sure your .env file contains adapter credentials[/dim]"
294
304
  )
295
- raise typer.Exit(1)
305
+ raise typer.Exit(1) from None
296
306
 
297
307
  console.print("\n[bold]Detected adapter configurations:[/bold]")
298
308
  for i, adapter in enumerate(result.adapters, 1):
@@ -332,7 +342,7 @@ def interactive(
332
342
  if choice in [1, 2]:
333
343
  if not primary:
334
344
  console.print("[red]No recommended adapter found[/red]")
335
- raise typer.Exit(1)
345
+ raise typer.Exit(1) from None
336
346
  adapters_to_save = [primary]
337
347
  default_adapter = primary.adapter_type
338
348
  elif choice == 3:
@@ -348,7 +358,7 @@ def interactive(
348
358
  default_adapter = selected.adapter_type
349
359
  else:
350
360
  console.print("[red]Invalid choice[/red]")
351
- raise typer.Exit(1)
361
+ raise typer.Exit(1) from None
352
362
  else: # choice == 4
353
363
  adapters_to_save = result.adapters
354
364
  default_adapter = (
@@ -389,22 +399,283 @@ def interactive(
389
399
 
390
400
  console.print(f" ✅ Added {discovered_adapter.adapter_type}")
391
401
 
392
- # Save
402
+ # Save (always to project config for security)
393
403
  try:
404
+ resolver.save_project_config(config, proj_path)
405
+ config_location = proj_path / resolver.PROJECT_CONFIG_SUBPATH
406
+
394
407
  if save_global:
395
- resolver.save_global_config(config)
396
- config_location = resolver.GLOBAL_CONFIG_PATH
397
- else:
398
- resolver.save_project_config(config, proj_path)
399
- config_location = proj_path / resolver.PROJECT_CONFIG_SUBPATH
408
+ console.print(
409
+ "[yellow]Note: Global config deprecated for security. Saved to project config instead.[/yellow]"
410
+ )
400
411
 
401
412
  console.print(f"\n[green]✅ Configuration saved to:[/green] {config_location}")
402
413
  console.print(f"[green]✅ Default adapter:[/green] {config.default_adapter}")
403
414
 
404
415
  except Exception as e:
405
416
  console.print(f"\n[red]Failed to save configuration:[/red] {e}")
417
+ raise typer.Exit(1) from None
418
+
419
+
420
+ @app.command(name="1password-status")
421
+ def onepassword_status() -> None:
422
+ """Check 1Password CLI installation and authentication status."""
423
+ console.print(
424
+ Panel.fit(
425
+ "[bold cyan]1Password CLI Status[/bold cyan]\n"
426
+ "Checking 1Password integration...",
427
+ border_style="cyan",
428
+ )
429
+ )
430
+
431
+ status = check_op_cli_status()
432
+
433
+ # Create status table
434
+ table = Table(title="1Password CLI Status")
435
+ table.add_column("Component", style="cyan")
436
+ table.add_column("Status", style="white")
437
+
438
+ # CLI installed
439
+ if status["installed"]:
440
+ table.add_row(
441
+ "CLI Installed", f"[green]✓ Yes[/green] (version {status['version']})"
442
+ )
443
+ else:
444
+ table.add_row("CLI Installed", "[red]✗ No[/red]")
445
+ console.print(table)
446
+ console.print(
447
+ "\n[yellow]Install 1Password CLI:[/yellow]\n"
448
+ " macOS: brew install 1password-cli\n"
449
+ " Linux: See https://developer.1password.com/docs/cli/get-started/\n"
450
+ " Windows: See https://developer.1password.com/docs/cli/get-started/"
451
+ )
452
+ return
453
+
454
+ # Authentication
455
+ if status["authenticated"]:
456
+ table.add_row("Authentication", "[green]✓ Signed in[/green]")
457
+
458
+ # Show accounts
459
+ if status["accounts"]:
460
+ for account in status["accounts"]:
461
+ account_url = account.get("url", "N/A")
462
+ account_email = account.get("email", "N/A")
463
+ table.add_row(" Account", f"{account_email} ({account_url})")
464
+ else:
465
+ table.add_row("Authentication", "[yellow]⚠ Not signed in[/yellow]")
466
+
467
+ console.print(table)
468
+
469
+ if not status["authenticated"]:
470
+ console.print("\n[yellow]Sign in to 1Password:[/yellow]\n" " Run: op signin\n")
471
+ else:
472
+ console.print(
473
+ "\n[green]✓ 1Password CLI is ready to use![/green]\n\n"
474
+ "You can now use .env files with op:// secret references.\n"
475
+ "Run 'mcp-ticketer discover 1password-template' to create template files."
476
+ )
477
+
478
+
479
+ @app.command(name="1password-template")
480
+ def onepassword_template(
481
+ adapter: str = typer.Argument(
482
+ ...,
483
+ help="Adapter type (linear, github, jira, aitrackdown)",
484
+ ),
485
+ vault: str = typer.Option(
486
+ "Development",
487
+ "--vault",
488
+ "-v",
489
+ help="1Password vault name for secret references",
490
+ ),
491
+ item: str | None = typer.Option(
492
+ None,
493
+ "--item",
494
+ "-i",
495
+ help="1Password item name (defaults to adapter name)",
496
+ ),
497
+ output: Path | None = typer.Option(
498
+ None,
499
+ "--output",
500
+ "-o",
501
+ help="Output file path (defaults to .env.1password)",
502
+ ),
503
+ ) -> None:
504
+ """Create a .env template file with 1Password secret references.
505
+
506
+ This creates a template file that uses op:// secret references,
507
+ which can be used with: op run --env-file=.env.1password -- <command>
508
+
509
+ Examples:
510
+ --------
511
+ # Create Linear template
512
+ mcp-ticketer discover 1password-template linear
513
+
514
+ # Create GitHub template with custom vault
515
+ mcp-ticketer discover 1password-template github --vault=Production
516
+
517
+ # Create template with custom item name
518
+ mcp-ticketer discover 1password-template jira --item="JIRA API Keys"
519
+
520
+ """
521
+ # Check if op CLI is available
522
+ status = check_op_cli_status()
523
+ if not status["installed"]:
524
+ console.print(
525
+ "[red]1Password CLI not installed.[/red]\n\n"
526
+ "Install it first:\n"
527
+ " macOS: brew install 1password-cli\n"
528
+ " Other: https://developer.1password.com/docs/cli/get-started/"
529
+ )
406
530
  raise typer.Exit(1)
407
531
 
532
+ # Set default output path
533
+ if output is None:
534
+ output = Path(f".env.1password.{adapter.lower()}")
535
+
536
+ # Create loader and generate template
537
+ loader = OnePasswordSecretsLoader(OnePasswordConfig())
538
+ loader.create_template_file(output, adapter, vault, item)
539
+
540
+ console.print(
541
+ Panel.fit(
542
+ f"[bold green]✓ Template created![/bold green]\n\n"
543
+ f"File: {output}\n"
544
+ f"Vault: {vault}\n"
545
+ f"Item: {item or adapter.upper()}\n\n"
546
+ f"[bold]Next steps:[/bold]\n"
547
+ f"1. Create item '{item or adapter.upper()}' in 1Password vault '{vault}'\n"
548
+ f"2. Add the required fields to the item\n"
549
+ f"3. Test with: op run --env-file={output} -- mcp-ticketer discover show\n"
550
+ f"4. Save config: op run --env-file={output} -- mcp-ticketer discover save",
551
+ border_style="green",
552
+ )
553
+ )
554
+
555
+ # Show template contents
556
+ console.print("\n[bold]Template contents:[/bold]\n")
557
+ console.print(Panel(output.read_text(), border_style="dim"))
558
+
559
+
560
+ @app.command(name="1password-test")
561
+ def onepassword_test(
562
+ env_file: Path = typer.Option(
563
+ ".env.1password",
564
+ "--file",
565
+ "-f",
566
+ help="Path to .env file with op:// references",
567
+ ),
568
+ ) -> None:
569
+ """Test 1Password secret resolution from .env file.
570
+
571
+ This command loads secrets from the specified .env file and
572
+ displays the resolved values (with sensitive data masked).
573
+
574
+ Example:
575
+ -------
576
+ mcp-ticketer discover 1password-test --file=.env.1password.linear
577
+
578
+ """
579
+ # Check if file exists
580
+ if not env_file.exists():
581
+ console.print(f"[red]File not found:[/red] {env_file}")
582
+ raise typer.Exit(1)
583
+
584
+ # Check if op CLI is available and authenticated
585
+ status = check_op_cli_status()
586
+ if not status["installed"]:
587
+ console.print("[red]1Password CLI not installed.[/red]")
588
+ raise typer.Exit(1)
589
+
590
+ if not status["authenticated"]:
591
+ console.print(
592
+ "[red]1Password CLI not authenticated.[/red]\n\n" "Run: op signin"
593
+ )
594
+ raise typer.Exit(1)
595
+
596
+ console.print(
597
+ Panel.fit(
598
+ f"[bold cyan]Testing 1Password Secret Resolution[/bold cyan]\n"
599
+ f"File: {env_file}",
600
+ border_style="cyan",
601
+ )
602
+ )
603
+
604
+ # Load secrets
605
+ loader = OnePasswordSecretsLoader(OnePasswordConfig())
606
+
607
+ try:
608
+ secrets = loader.load_secrets_from_env_file(env_file)
609
+
610
+ # Display resolved secrets
611
+ table = Table(title="Resolved Secrets")
612
+ table.add_column("Variable", style="cyan")
613
+ table.add_column("Value", style="green")
614
+
615
+ for key, value in secrets.items():
616
+ # Mask sensitive values
617
+ display_value = _mask_sensitive(value, key)
618
+ table.add_row(key, display_value)
619
+
620
+ console.print(table)
621
+
622
+ console.print(
623
+ f"\n[green]✓ Successfully resolved {len(secrets)} secrets![/green]"
624
+ )
625
+
626
+ # Test discovery with these secrets
627
+ console.print("\n[bold]Testing configuration discovery...[/bold]")
628
+ EnvDiscovery(enable_1password=False) # Already resolved
629
+
630
+ # Temporarily write resolved secrets to test discovery
631
+ import tempfile
632
+
633
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".env", delete=False) as tmp:
634
+ for key, value in secrets.items():
635
+ tmp.write(f"{key}={value}\n")
636
+ tmp_path = Path(tmp.name)
637
+
638
+ try:
639
+ # Mock the env file loading by directly providing secrets
640
+ from ..core.env_discovery import DiscoveryResult
641
+
642
+ DiscoveryResult()
643
+
644
+ # Try to detect adapters from the resolved secrets
645
+ from ..core.env_discovery import EnvDiscovery as ED
646
+
647
+ ed = ED(enable_1password=False)
648
+ ed.project_path = Path.cwd()
649
+
650
+ # Manually detect from secrets dict
651
+ linear_adapter = ed._detect_linear(secrets, str(env_file))
652
+ if linear_adapter:
653
+ console.print("\n[green]✓ Detected Linear configuration[/green]")
654
+ _display_discovered_adapter(linear_adapter, ed)
655
+
656
+ github_adapter = ed._detect_github(secrets, str(env_file))
657
+ if github_adapter:
658
+ console.print("\n[green]✓ Detected GitHub configuration[/green]")
659
+ _display_discovered_adapter(github_adapter, ed)
660
+
661
+ jira_adapter = ed._detect_jira(secrets, str(env_file))
662
+ if jira_adapter:
663
+ console.print("\n[green]✓ Detected JIRA configuration[/green]")
664
+ _display_discovered_adapter(jira_adapter, ed)
665
+ finally:
666
+ tmp_path.unlink()
667
+
668
+ except Exception as e:
669
+ console.print(f"\n[red]Failed to resolve secrets:[/red] {e}")
670
+ console.print(
671
+ "\n[yellow]Troubleshooting:[/yellow]\n"
672
+ "1. Check that the item exists in 1Password\n"
673
+ "2. Verify the vault name is correct\n"
674
+ "3. Ensure all field names match\n"
675
+ f"4. Run: op inject --in-file={env_file} (to see detailed errors)"
676
+ )
677
+ raise typer.Exit(1) from None
678
+
408
679
 
409
680
  if __name__ == "__main__":
410
681
  app()
@@ -2,11 +2,12 @@
2
2
 
3
3
  import json
4
4
  from pathlib import Path
5
- from typing import Literal, Optional
5
+ from typing import Literal
6
6
 
7
7
  from rich.console import Console
8
8
 
9
- from .mcp_configure import find_mcp_ticketer_binary, load_project_config
9
+ from .mcp_configure import load_project_config
10
+ from .python_detection import get_mcp_ticketer_python
10
11
 
11
12
  console = Console()
12
13
 
@@ -73,19 +74,26 @@ def save_gemini_config(config_path: Path, config: dict) -> None:
73
74
 
74
75
 
75
76
  def create_gemini_server_config(
76
- binary_path: str, project_config: dict, cwd: Optional[str] = None
77
+ python_path: str, project_config: dict, project_path: str | None = None
77
78
  ) -> dict:
78
79
  """Create Gemini MCP server configuration for mcp-ticketer.
79
80
 
81
+ Uses the CLI command (mcp-ticketer mcp) which implements proper
82
+ Content-Length framing via FastMCP SDK, required for modern MCP clients.
83
+
80
84
  Args:
81
- binary_path: Path to mcp-ticketer binary
85
+ python_path: Path to Python executable in mcp-ticketer venv
82
86
  project_config: Project configuration from .mcp-ticketer/config.json
83
- cwd: Working directory for server (optional)
87
+ project_path: Project directory path (optional)
84
88
 
85
89
  Returns:
86
90
  Gemini MCP server configuration dict
87
91
 
88
92
  """
93
+ # IMPORTANT: Use CLI command, NOT Python module invocation
94
+ # The CLI uses FastMCP SDK which implements proper Content-Length framing
95
+ # Legacy python -m mcp_ticketer.mcp.server uses line-delimited JSON (incompatible)
96
+
89
97
  # Get adapter configuration
90
98
  adapter = project_config.get("default_adapter", "aitrackdown")
91
99
  adapters_config = project_config.get("adapters", {})
@@ -94,9 +102,9 @@ def create_gemini_server_config(
94
102
  # Build environment variables
95
103
  env_vars = {}
96
104
 
97
- # Add PYTHONPATH if running from development environment
98
- if cwd:
99
- env_vars["PYTHONPATH"] = str(Path(cwd) / "src")
105
+ # Add PYTHONPATH for project context
106
+ if project_path:
107
+ env_vars["PYTHONPATH"] = project_path
100
108
 
101
109
  # Add adapter type
102
110
  env_vars["MCP_TICKETER_ADAPTER"] = adapter
@@ -105,9 +113,9 @@ def create_gemini_server_config(
105
113
  if adapter == "aitrackdown":
106
114
  # Set base path for local adapter
107
115
  base_path = adapter_config.get("base_path", ".aitrackdown")
108
- if cwd:
109
- # Use absolute path if cwd is provided
110
- env_vars["MCP_TICKETER_BASE_PATH"] = str(Path(cwd) / base_path)
116
+ if project_path:
117
+ # Use absolute path if project_path is provided
118
+ env_vars["MCP_TICKETER_BASE_PATH"] = str(Path(project_path) / base_path)
111
119
  else:
112
120
  env_vars["MCP_TICKETER_BASE_PATH"] = base_path
113
121
 
@@ -135,10 +143,22 @@ def create_gemini_server_config(
135
143
  if "project_key" in adapter_config:
136
144
  env_vars["JIRA_PROJECT_KEY"] = adapter_config["project_key"]
137
145
 
146
+ # Get mcp-ticketer CLI path from Python path
147
+ # If python_path is /path/to/venv/bin/python, CLI is /path/to/venv/bin/mcp-ticketer
148
+ python_dir = Path(python_path).parent
149
+ cli_path = str(python_dir / "mcp-ticketer")
150
+
151
+ # Build CLI arguments
152
+ args = ["mcp"]
153
+ if project_path:
154
+ args.extend(["--path", project_path])
155
+
138
156
  # Create server configuration with Gemini-specific options
157
+ # NOTE: Environment variables below are optional fallbacks
158
+ # The CLI loads config from .mcp-ticketer/config.json
139
159
  config = {
140
- "command": binary_path,
141
- "args": ["serve"],
160
+ "command": cli_path,
161
+ "args": args,
142
162
  "env": env_vars,
143
163
  "timeout": 15000, # 15 seconds timeout
144
164
  "trust": False, # Don't trust by default (security)
@@ -147,6 +167,72 @@ def create_gemini_server_config(
147
167
  return config
148
168
 
149
169
 
170
+ def remove_gemini_mcp(
171
+ scope: Literal["project", "user"] = "project", dry_run: bool = False
172
+ ) -> None:
173
+ """Remove mcp-ticketer from Gemini CLI configuration.
174
+
175
+ Args:
176
+ scope: Configuration scope - "project" or "user"
177
+ dry_run: Show what would be removed without making changes
178
+
179
+ """
180
+ # Step 1: Find Gemini config location
181
+ config_type = "user-level" if scope == "user" else "project-level"
182
+ console.print(f"[cyan]🔍 Removing {config_type} Gemini CLI configuration...[/cyan]")
183
+
184
+ gemini_config_path = find_gemini_config(scope)
185
+ console.print(f"[dim]Config location: {gemini_config_path}[/dim]")
186
+
187
+ # Step 2: Check if config file exists
188
+ if not gemini_config_path.exists():
189
+ console.print(
190
+ f"[yellow]⚠ No configuration found at {gemini_config_path}[/yellow]"
191
+ )
192
+ console.print("[dim]mcp-ticketer is not configured for Gemini CLI[/dim]")
193
+ return
194
+
195
+ # Step 3: Load existing Gemini configuration
196
+ gemini_config = load_gemini_config(gemini_config_path)
197
+
198
+ # Step 4: Check if mcp-ticketer is configured
199
+ if "mcp-ticketer" not in gemini_config.get("mcpServers", {}):
200
+ console.print("[yellow]⚠ mcp-ticketer is not configured[/yellow]")
201
+ console.print(f"[dim]No mcp-ticketer entry found in {gemini_config_path}[/dim]")
202
+ return
203
+
204
+ # Step 5: Show what would be removed (dry run or actual removal)
205
+ if dry_run:
206
+ console.print("\n[cyan]DRY RUN - Would remove:[/cyan]")
207
+ console.print(" Server name: mcp-ticketer")
208
+ console.print(f" From: {gemini_config_path}")
209
+ console.print(f" Scope: {config_type}")
210
+ return
211
+
212
+ # Step 6: Remove mcp-ticketer from configuration
213
+ del gemini_config["mcpServers"]["mcp-ticketer"]
214
+
215
+ # Step 7: Save updated configuration
216
+ try:
217
+ save_gemini_config(gemini_config_path, gemini_config)
218
+ console.print("\n[green]✓ Successfully removed mcp-ticketer[/green]")
219
+ console.print(f"[dim]Configuration updated: {gemini_config_path}[/dim]")
220
+
221
+ # Next steps
222
+ console.print("\n[bold cyan]Next Steps:[/bold cyan]")
223
+ if scope == "user":
224
+ console.print("1. Gemini CLI global configuration updated")
225
+ console.print("2. mcp-ticketer will no longer be available in any project")
226
+ else:
227
+ console.print("1. Gemini CLI project configuration updated")
228
+ console.print("2. mcp-ticketer will no longer be available in this project")
229
+ console.print("3. Restart Gemini CLI if currently running")
230
+
231
+ except Exception as e:
232
+ console.print(f"\n[red]✗ Failed to update configuration:[/red] {e}")
233
+ raise
234
+
235
+
150
236
  def configure_gemini_mcp(
151
237
  scope: Literal["project", "user"] = "project", force: bool = False
152
238
  ) -> None:
@@ -157,18 +243,22 @@ def configure_gemini_mcp(
157
243
  force: Overwrite existing configuration
158
244
 
159
245
  Raises:
160
- FileNotFoundError: If binary or project config not found
246
+ FileNotFoundError: If Python executable or project config not found
161
247
  ValueError: If configuration is invalid
162
248
 
163
249
  """
164
- # Step 1: Find mcp-ticketer binary
165
- console.print("[cyan]🔍 Finding mcp-ticketer binary...[/cyan]")
250
+ # Step 1: Find Python executable
251
+ console.print("[cyan]🔍 Finding mcp-ticketer Python executable...[/cyan]")
166
252
  try:
167
- binary_path = find_mcp_ticketer_binary()
168
- console.print(f"[green]✓[/green] Found: {binary_path}")
169
- except FileNotFoundError as e:
170
- console.print(f"[red]✗[/red] {e}")
171
- raise
253
+ python_path = get_mcp_ticketer_python()
254
+ console.print(f"[green]✓[/green] Found: {python_path}")
255
+ except Exception as e:
256
+ console.print(f"[red]✗[/red] Could not find Python executable: {e}")
257
+ raise FileNotFoundError(
258
+ "Could not find mcp-ticketer Python executable. "
259
+ "Please ensure mcp-ticketer is installed.\n"
260
+ "Install with: pip install mcp-ticketer or pipx install mcp-ticketer"
261
+ ) from e
172
262
 
173
263
  # Step 2: Load project configuration
174
264
  console.print("\n[cyan]📖 Reading project configuration...[/cyan]")
@@ -200,9 +290,11 @@ def configure_gemini_mcp(
200
290
  console.print("[yellow]⚠ Overwriting existing configuration[/yellow]")
201
291
 
202
292
  # Step 6: Create mcp-ticketer server config
203
- cwd = str(Path.cwd()) if scope == "project" else None
293
+ project_path = str(Path.cwd()) if scope == "project" else None
204
294
  server_config = create_gemini_server_config(
205
- binary_path=binary_path, project_config=project_config, cwd=cwd
295
+ python_path=python_path,
296
+ project_config=project_config,
297
+ project_path=project_path,
206
298
  )
207
299
 
208
300
  # Step 7: Update Gemini configuration
@@ -221,11 +313,12 @@ def configure_gemini_mcp(
221
313
  console.print("\n[bold]Configuration Details:[/bold]")
222
314
  console.print(" Server name: mcp-ticketer")
223
315
  console.print(f" Adapter: {adapter}")
224
- console.print(f" Binary: {binary_path}")
316
+ console.print(f" Python: {python_path}")
317
+ console.print(" Command: python -m mcp_ticketer.mcp.server")
225
318
  console.print(f" Timeout: {server_config['timeout']}ms")
226
319
  console.print(f" Trust: {server_config['trust']}")
227
- if cwd:
228
- console.print(f" Working directory: {cwd}")
320
+ if project_path:
321
+ console.print(f" Project path: {project_path}")
229
322
  if "env" in server_config:
230
323
  console.print(
231
324
  f" Environment variables: {list(server_config['env'].keys())}"