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.

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