mcp-ticketer 0.4.3__py3-none-any.whl → 0.4.5__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 (31) hide show
  1. mcp_ticketer/__init__.py +12 -3
  2. mcp_ticketer/__version__.py +1 -1
  3. mcp_ticketer/adapters/aitrackdown.py +16 -5
  4. mcp_ticketer/adapters/github.py +1 -2
  5. mcp_ticketer/adapters/jira.py +1 -2
  6. mcp_ticketer/adapters/linear/adapter.py +21 -9
  7. mcp_ticketer/adapters/linear/client.py +1 -2
  8. mcp_ticketer/adapters/linear/mappers.py +1 -2
  9. mcp_ticketer/cli/adapter_diagnostics.py +2 -4
  10. mcp_ticketer/cli/auggie_configure.py +35 -15
  11. mcp_ticketer/cli/codex_configure.py +38 -31
  12. mcp_ticketer/cli/configure.py +9 -3
  13. mcp_ticketer/cli/discover.py +6 -2
  14. mcp_ticketer/cli/gemini_configure.py +38 -25
  15. mcp_ticketer/cli/main.py +147 -32
  16. mcp_ticketer/cli/mcp_configure.py +115 -78
  17. mcp_ticketer/cli/python_detection.py +126 -0
  18. mcp_ticketer/core/__init__.py +1 -2
  19. mcp_ticketer/mcp/__init__.py +29 -1
  20. mcp_ticketer/mcp/__main__.py +60 -0
  21. mcp_ticketer/mcp/server.py +34 -14
  22. mcp_ticketer/mcp/tools/__init__.py +9 -7
  23. mcp_ticketer/queue/__init__.py +3 -1
  24. mcp_ticketer/queue/manager.py +10 -46
  25. mcp_ticketer/queue/ticket_registry.py +5 -5
  26. {mcp_ticketer-0.4.3.dist-info → mcp_ticketer-0.4.5.dist-info}/METADATA +13 -4
  27. {mcp_ticketer-0.4.3.dist-info → mcp_ticketer-0.4.5.dist-info}/RECORD +31 -29
  28. {mcp_ticketer-0.4.3.dist-info → mcp_ticketer-0.4.5.dist-info}/WHEEL +0 -0
  29. {mcp_ticketer-0.4.3.dist-info → mcp_ticketer-0.4.5.dist-info}/entry_points.txt +0 -0
  30. {mcp_ticketer-0.4.3.dist-info → mcp_ticketer-0.4.5.dist-info}/licenses/LICENSE +0 -0
  31. {mcp_ticketer-0.4.3.dist-info → mcp_ticketer-0.4.5.dist-info}/top_level.txt +0 -0
mcp_ticketer/cli/main.py CHANGED
@@ -20,8 +20,7 @@ from ..core.models import Comment, SearchQuery
20
20
  from ..queue import Queue, QueueStatus, WorkerManager
21
21
  from ..queue.health_monitor import HealthStatus, QueueHealthMonitor
22
22
  from ..queue.ticket_registry import TicketRegistry
23
- from .configure import (configure_wizard, set_adapter_config,
24
- show_current_config)
23
+ from .configure import configure_wizard, set_adapter_config, show_current_config
25
24
  from .diagnostics import run_diagnostics
26
25
  from .discover import app as discover_app
27
26
  from .migrate_config import migrate_config_command
@@ -679,7 +678,9 @@ def init(
679
678
  if not linear_team_key and not linear_team_id and not discovered:
680
679
  console.print("\n[bold]Linear Team Configuration[/bold]")
681
680
  console.print("Enter your team key (e.g., 'ENG', 'DESIGN', 'PRODUCT')")
682
- console.print("[dim]Find it in: Linear Settings → Teams → Your Team → Key field[/dim]\n")
681
+ console.print(
682
+ "[dim]Find it in: Linear Settings → Teams → Your Team → Key field[/dim]\n"
683
+ )
683
684
 
684
685
  linear_team_key = typer.prompt("Team key")
685
686
 
@@ -893,10 +894,11 @@ def _show_next_steps(
893
894
  console.print("\n3. [cyan]Check local ticket storage:[/cyan]")
894
895
  console.print(" ls .aitrackdown/")
895
896
 
896
- console.print("\n4. [cyan]Configure MCP clients (optional):[/cyan]")
897
- console.print(" mcp-ticketer mcp claude # For Claude Code")
898
- console.print(" mcp-ticketer mcp auggie # For Auggie")
899
- console.print(" mcp-ticketer mcp gemini # For Gemini CLI")
897
+ console.print("\n4. [cyan]Install MCP for AI clients (optional):[/cyan]")
898
+ console.print(" mcp-ticketer install claude-code # For Claude Code")
899
+ console.print(" mcp-ticketer install claude-desktop # For Claude Desktop")
900
+ console.print(" mcp-ticketer install auggie # For Auggie")
901
+ console.print(" mcp-ticketer install gemini # For Gemini CLI")
900
902
 
901
903
  console.print(f"\n[dim]Configuration saved to: {config_file_path}[/dim]")
902
904
  console.print("[dim]Run 'mcp-ticketer --help' for more commands[/dim]")
@@ -1871,6 +1873,10 @@ mcp_app = typer.Typer(
1871
1873
 
1872
1874
  @app.command()
1873
1875
  def install(
1876
+ platform: str | None = typer.Argument(
1877
+ None,
1878
+ help="Platform to install (claude-code, claude-desktop, gemini, codex, auggie)",
1879
+ ),
1874
1880
  adapter: str | None = typer.Option(
1875
1881
  None,
1876
1882
  "--adapter",
@@ -1918,41 +1924,31 @@ def install(
1918
1924
  github_token: str | None = typer.Option(
1919
1925
  None, "--github-token", help="GitHub Personal Access Token"
1920
1926
  ),
1921
- platform: str | None = typer.Option(
1922
- None,
1923
- "--platform",
1924
- help="Platform to configure MCP for (claude-code, claude-desktop, auggie, gemini, codex)",
1925
- ),
1926
1927
  dry_run: bool = typer.Option(
1927
1928
  False,
1928
1929
  "--dry-run",
1929
1930
  help="Show what would be done without making changes (for platform installation)",
1930
1931
  ),
1931
1932
  ) -> None:
1932
- """Install and initialize mcp-ticketer (synonymous with 'init' and 'setup').
1933
+ """Install MCP for AI platforms OR initialize adapter setup.
1933
1934
 
1934
- Without arguments, runs interactive setup wizard to configure your ticket adapter.
1935
- With --platform, installs MCP configuration for AI platforms.
1935
+ With platform argument (new syntax): Install MCP configuration for AI platforms
1936
+ Without platform argument (legacy): Run adapter setup wizard (same as 'init' and 'setup')
1936
1937
 
1937
- This command serves two purposes:
1938
- 1. Adapter initialization (same as 'init' and 'setup')
1939
- 2. Platform MCP configuration (when using --platform flag)
1940
-
1941
- Examples:
1942
- # Interactive setup (same as 'init' and 'setup')
1943
- mcp-ticketer install
1938
+ New Command Structure:
1939
+ # Install MCP for AI platforms
1940
+ mcp-ticketer install claude-code # Claude Code (project-level)
1941
+ mcp-ticketer install claude-desktop # Claude Desktop (global)
1942
+ mcp-ticketer install gemini # Gemini CLI
1943
+ mcp-ticketer install codex # Codex
1944
+ mcp-ticketer install auggie # Auggie
1944
1945
 
1945
- # Setup with specific adapter
1946
+ Legacy Adapter Setup (still supported):
1947
+ mcp-ticketer install # Interactive setup wizard
1946
1948
  mcp-ticketer install --adapter linear
1947
1949
 
1948
- # Install MCP for Claude Code
1949
- mcp-ticketer install --platform claude-code
1950
-
1951
- # Install MCP for Claude Desktop
1952
- mcp-ticketer install --platform claude-desktop
1953
-
1954
1950
  """
1955
- # If platform flag is provided, handle MCP platform installation
1951
+ # If platform argument is provided, handle MCP platform installation (NEW SYNTAX)
1956
1952
  if platform is not None:
1957
1953
  # Import configuration functions
1958
1954
  from .auggie_configure import configure_auggie_mcp
@@ -2004,8 +2000,8 @@ def install(
2004
2000
  raise typer.Exit(1)
2005
2001
  return
2006
2002
 
2007
- # Otherwise, delegate to init for adapter initialization
2008
- # This makes 'install' and 'init' synonymous when called without --platform
2003
+ # Otherwise, delegate to init for adapter initialization (LEGACY BEHAVIOR)
2004
+ # This makes 'install' and 'init' synonymous when called without platform argument
2009
2005
  init(
2010
2006
  adapter=adapter,
2011
2007
  project_path=project_path,
@@ -2429,6 +2425,125 @@ def mcp_auggie(
2429
2425
  raise typer.Exit(1)
2430
2426
 
2431
2427
 
2428
+ @mcp_app.command(name="status")
2429
+ def mcp_status():
2430
+ """Check MCP server status.
2431
+
2432
+ Shows whether the MCP server is configured and running for various platforms.
2433
+
2434
+ Examples:
2435
+ mcp-ticketer mcp status
2436
+
2437
+ """
2438
+ import json
2439
+ from pathlib import Path
2440
+
2441
+ console.print("[bold]MCP Server Status[/bold]\n")
2442
+
2443
+ # Check project-level configuration
2444
+ project_config = Path.cwd() / ".mcp-ticketer" / "config.json"
2445
+ if project_config.exists():
2446
+ console.print(f"[green]✓[/green] Project config found: {project_config}")
2447
+ try:
2448
+ with open(project_config) as f:
2449
+ config = json.load(f)
2450
+ adapter = config.get("default_adapter", "aitrackdown")
2451
+ console.print(f" Default adapter: [cyan]{adapter}[/cyan]")
2452
+ except Exception as e:
2453
+ console.print(f" [yellow]Warning: Could not read config: {e}[/yellow]")
2454
+ else:
2455
+ console.print("[yellow]○[/yellow] No project config found")
2456
+
2457
+ # Check Claude Code configuration
2458
+ claude_code_config = Path.cwd() / ".mcp" / "config.json"
2459
+ if claude_code_config.exists():
2460
+ console.print(
2461
+ f"\n[green]✓[/green] Claude Code configured: {claude_code_config}"
2462
+ )
2463
+ else:
2464
+ console.print("\n[yellow]○[/yellow] Claude Code not configured")
2465
+
2466
+ # Check Claude Desktop configuration
2467
+ claude_desktop_config = (
2468
+ Path.home()
2469
+ / "Library"
2470
+ / "Application Support"
2471
+ / "Claude"
2472
+ / "claude_desktop_config.json"
2473
+ )
2474
+ if claude_desktop_config.exists():
2475
+ try:
2476
+ with open(claude_desktop_config) as f:
2477
+ config = json.load(f)
2478
+ if "mcpServers" in config and "mcp-ticketer" in config["mcpServers"]:
2479
+ console.print(
2480
+ f"[green]✓[/green] Claude Desktop configured: {claude_desktop_config}"
2481
+ )
2482
+ else:
2483
+ console.print(
2484
+ "[yellow]○[/yellow] Claude Desktop config exists but mcp-ticketer not found"
2485
+ )
2486
+ except Exception:
2487
+ console.print(
2488
+ "[yellow]○[/yellow] Claude Desktop config exists but could not be read"
2489
+ )
2490
+ else:
2491
+ console.print("[yellow]○[/yellow] Claude Desktop not configured")
2492
+
2493
+ # Check Gemini configuration
2494
+ gemini_project_config = Path.cwd() / ".gemini" / "settings.json"
2495
+ gemini_user_config = Path.home() / ".gemini" / "settings.json"
2496
+ if gemini_project_config.exists():
2497
+ console.print(
2498
+ f"\n[green]✓[/green] Gemini (project) configured: {gemini_project_config}"
2499
+ )
2500
+ elif gemini_user_config.exists():
2501
+ console.print(
2502
+ f"\n[green]✓[/green] Gemini (user) configured: {gemini_user_config}"
2503
+ )
2504
+ else:
2505
+ console.print("\n[yellow]○[/yellow] Gemini not configured")
2506
+
2507
+ # Check Codex configuration
2508
+ codex_config = Path.home() / ".codex" / "config.toml"
2509
+ if codex_config.exists():
2510
+ console.print(f"[green]✓[/green] Codex configured: {codex_config}")
2511
+ else:
2512
+ console.print("[yellow]○[/yellow] Codex not configured")
2513
+
2514
+ # Check Auggie configuration
2515
+ auggie_config = Path.home() / ".augment" / "settings.json"
2516
+ if auggie_config.exists():
2517
+ console.print(f"[green]✓[/green] Auggie configured: {auggie_config}")
2518
+ else:
2519
+ console.print("[yellow]○[/yellow] Auggie not configured")
2520
+
2521
+ console.print(
2522
+ "\n[dim]Run 'mcp-ticketer install <platform>' to configure a platform[/dim]"
2523
+ )
2524
+
2525
+
2526
+ @mcp_app.command(name="stop")
2527
+ def mcp_stop():
2528
+ """Stop MCP server (placeholder - MCP runs on-demand via stdio).
2529
+
2530
+ Note: The MCP server runs on-demand when AI clients connect via stdio.
2531
+ It doesn't run as a persistent background service, so there's nothing to stop.
2532
+ This command is provided for consistency but has no effect.
2533
+
2534
+ Examples:
2535
+ mcp-ticketer mcp stop
2536
+
2537
+ """
2538
+ console.print(
2539
+ "[yellow]ℹ[/yellow] MCP server runs on-demand via stdio (not as a background service)"
2540
+ )
2541
+ console.print("There is no persistent server process to stop.")
2542
+ console.print(
2543
+ "\n[dim]The server starts automatically when AI clients connect and stops when they disconnect.[/dim]"
2544
+ )
2545
+
2546
+
2432
2547
  # Add command groups to main app (must be after all subcommands are defined)
2433
2548
  app.add_typer(mcp_app, name="mcp")
2434
2549
 
@@ -2,65 +2,43 @@
2
2
 
3
3
  import json
4
4
  import os
5
- import shutil
6
5
  import sys
7
6
  from pathlib import Path
8
7
 
9
8
  from rich.console import Console
10
9
 
10
+ from .python_detection import get_mcp_ticketer_python
11
+
11
12
  console = Console()
12
13
 
13
14
 
14
- def find_mcp_ticketer_binary() -> str:
15
- """Find the mcp-ticketer binary path.
15
+ def load_env_file(env_path: Path) -> dict[str, str]:
16
+ """Load environment variables from .env file.
16
17
 
17
- Returns:
18
- Path to mcp-ticketer binary (prefers simple 'mcp-ticketer' if in PATH)
18
+ Args:
19
+ env_path: Path to .env file
19
20
 
20
- Raises:
21
- FileNotFoundError: If binary not found
21
+ Returns:
22
+ Dict of environment variable key-value pairs
22
23
 
23
24
  """
24
- # PRIORITY 1: Check PATH first (like kuzu-memory)
25
- # This allows the system to resolve the binary location
26
- which_result = shutil.which("mcp-ticketer")
27
- if which_result:
28
- # Return just "mcp-ticketer" for PATH-based installations
29
- # This is more portable and matches kuzu-memory's approach
30
- return "mcp-ticketer"
31
-
32
- # FALLBACK: Check development environment
33
- import mcp_ticketer
34
-
35
- package_path = Path(mcp_ticketer.__file__).parent.parent.parent
36
-
37
- # Check for virtual environment bin
38
- possible_paths = [
39
- # Development paths
40
- package_path / "venv" / "bin" / "mcp-ticketer",
41
- package_path / ".venv" / "bin" / "mcp-ticketer",
42
- package_path / "test_venv" / "bin" / "mcp-ticketer",
43
- # System installation
44
- Path.home() / ".local" / "bin" / "mcp-ticketer",
45
- # pipx installation
46
- Path.home()
47
- / ".local"
48
- / "pipx"
49
- / "venvs"
50
- / "mcp-ticketer"
51
- / "bin"
52
- / "mcp-ticketer",
53
- ]
54
-
55
- # Check possible paths
56
- for path in possible_paths:
57
- if path.exists():
58
- return str(path.resolve())
59
-
60
- raise FileNotFoundError(
61
- "Could not find mcp-ticketer binary. Please ensure mcp-ticketer is installed.\n"
62
- "Install with: pip install mcp-ticketer"
63
- )
25
+ env_vars = {}
26
+ if not env_path.exists():
27
+ return env_vars
28
+
29
+ with open(env_path) as f:
30
+ for line in f:
31
+ line = line.strip()
32
+ # Skip comments and empty lines
33
+ if not line or line.startswith("#"):
34
+ continue
35
+
36
+ # Parse KEY=VALUE format
37
+ if "=" in line:
38
+ key, value = line.split("=", 1)
39
+ env_vars[key.strip()] = value.strip()
40
+
41
+ return env_vars
64
42
 
65
43
 
66
44
  def load_project_config() -> dict:
@@ -129,8 +107,8 @@ def find_claude_mcp_config(global_config: bool = False) -> Path:
129
107
  Path.home() / ".config" / "Claude" / "claude_desktop_config.json"
130
108
  )
131
109
  else:
132
- # Project-level configuration
133
- config_path = Path.cwd() / ".mcp" / "config.json"
110
+ # Project-level configuration for Claude Code
111
+ config_path = Path.cwd() / ".claude" / "settings.local.json"
134
112
 
135
113
  return config_path
136
114
 
@@ -149,7 +127,7 @@ def load_claude_mcp_config(config_path: Path) -> dict:
149
127
  with open(config_path) as f:
150
128
  return json.load(f)
151
129
 
152
- # Return empty structure
130
+ # Return empty structure (Claude Code uses mcpServers key)
153
131
  return {"mcpServers": {}}
154
132
 
155
133
 
@@ -170,28 +148,37 @@ def save_claude_mcp_config(config_path: Path, config: dict) -> None:
170
148
 
171
149
 
172
150
  def create_mcp_server_config(
173
- binary_path: str, project_config: dict, cwd: str | None = None
151
+ python_path: str, project_config: dict, project_path: str | None = None
174
152
  ) -> dict:
175
153
  """Create MCP server configuration for mcp-ticketer.
176
154
 
177
155
  Args:
178
- binary_path: Path to mcp-ticketer binary
156
+ python_path: Path to Python executable in mcp-ticketer venv
179
157
  project_config: Project configuration from .mcp-ticketer/config.json
180
- cwd: Working directory for server (optional)
158
+ project_path: Project directory path (optional)
181
159
 
182
160
  Returns:
183
- MCP server configuration dict
161
+ MCP server configuration dict matching Claude Code stdio pattern
184
162
 
185
163
  """
164
+ # Ensure python3 is used (not python)
165
+ if python_path.endswith("/python"):
166
+ python_path = python_path.replace("/python", "/python3")
167
+
168
+ # Use module invocation pattern: python -m mcp_ticketer.mcp.server
169
+ args = ["-m", "mcp_ticketer.mcp.server"]
170
+
171
+ # Add project path if provided
172
+ if project_path:
173
+ args.append(project_path)
174
+
175
+ # REQUIRED: Add "type": "stdio" for Claude Code compatibility
186
176
  config = {
187
- "command": binary_path,
188
- "args": ["mcp", "serve"], # Use 'mcp serve' command to start MCP server
177
+ "type": "stdio",
178
+ "command": python_path,
179
+ "args": args,
189
180
  }
190
181
 
191
- # Add working directory if provided
192
- if cwd:
193
- config["cwd"] = cwd
194
-
195
182
  # Add environment variables based on adapter
196
183
  adapter = project_config.get("default_adapter", "aitrackdown")
197
184
  adapters_config = project_config.get("adapters", {})
@@ -199,15 +186,49 @@ def create_mcp_server_config(
199
186
 
200
187
  env_vars = {}
201
188
 
202
- # Add adapter-specific environment variables
189
+ # Add PYTHONPATH for project context
190
+ if project_path:
191
+ env_vars["PYTHONPATH"] = project_path
192
+
193
+ # Add MCP_TICKETER_ADAPTER to identify which adapter to use
194
+ env_vars["MCP_TICKETER_ADAPTER"] = adapter
195
+
196
+ # Load environment variables from .env.local if it exists
197
+ if project_path:
198
+ env_file_path = Path(project_path) / ".env.local"
199
+ env_file_vars = load_env_file(env_file_path)
200
+
201
+ # Add relevant adapter-specific vars from .env.local
202
+ adapter_env_keys = {
203
+ "linear": ["LINEAR_API_KEY", "LINEAR_TEAM_ID", "LINEAR_TEAM_KEY"],
204
+ "github": ["GITHUB_TOKEN", "GITHUB_OWNER", "GITHUB_REPO"],
205
+ "jira": [
206
+ "JIRA_ACCESS_USER",
207
+ "JIRA_ACCESS_TOKEN",
208
+ "JIRA_ORGANIZATION_ID",
209
+ "JIRA_URL",
210
+ "JIRA_EMAIL",
211
+ "JIRA_API_TOKEN",
212
+ ],
213
+ "aitrackdown": [], # No specific env vars needed
214
+ }
215
+
216
+ # Include adapter-specific env vars from .env.local
217
+ for key in adapter_env_keys.get(adapter, []):
218
+ if key in env_file_vars:
219
+ env_vars[key] = env_file_vars[key]
220
+
221
+ # Fallback: Add adapter-specific environment variables from project config
203
222
  if adapter == "linear" and "api_key" in adapter_config:
204
- env_vars["LINEAR_API_KEY"] = adapter_config["api_key"]
223
+ if "LINEAR_API_KEY" not in env_vars:
224
+ env_vars["LINEAR_API_KEY"] = adapter_config["api_key"]
205
225
  elif adapter == "github" and "token" in adapter_config:
206
- env_vars["GITHUB_TOKEN"] = adapter_config["token"]
226
+ if "GITHUB_TOKEN" not in env_vars:
227
+ env_vars["GITHUB_TOKEN"] = adapter_config["token"]
207
228
  elif adapter == "jira":
208
- if "api_token" in adapter_config:
229
+ if "api_token" in adapter_config and "JIRA_API_TOKEN" not in env_vars:
209
230
  env_vars["JIRA_API_TOKEN"] = adapter_config["api_token"]
210
- if "email" in adapter_config:
231
+ if "email" in adapter_config and "JIRA_EMAIL" not in env_vars:
211
232
  env_vars["JIRA_EMAIL"] = adapter_config["email"]
212
233
 
213
234
  if env_vars:
@@ -284,18 +305,31 @@ def configure_claude_mcp(global_config: bool = False, force: bool = False) -> No
284
305
  force: Overwrite existing configuration
285
306
 
286
307
  Raises:
287
- FileNotFoundError: If binary or project config not found
308
+ FileNotFoundError: If Python executable or project config not found
288
309
  ValueError: If configuration is invalid
289
310
 
290
311
  """
291
- # Step 1: Find mcp-ticketer binary
292
- console.print("[cyan]🔍 Finding mcp-ticketer binary...[/cyan]")
312
+ # Determine project path for venv detection
313
+ project_path = Path.cwd() if not global_config else None
314
+
315
+ # Step 1: Find Python executable (project-specific if available)
316
+ console.print("[cyan]🔍 Finding mcp-ticketer Python executable...[/cyan]")
293
317
  try:
294
- binary_path = find_mcp_ticketer_binary()
295
- console.print(f"[green]✓[/green] Found: {binary_path}")
296
- except FileNotFoundError as e:
297
- console.print(f"[red]✗[/red] {e}")
298
- raise
318
+ python_path = get_mcp_ticketer_python(project_path=project_path)
319
+ console.print(f"[green]✓[/green] Found: {python_path}")
320
+
321
+ # Show if using project venv or fallback
322
+ if project_path and str(project_path / ".venv") in python_path:
323
+ console.print("[dim]Using project-specific venv[/dim]")
324
+ else:
325
+ console.print("[dim]Using pipx/system Python[/dim]")
326
+ except Exception as e:
327
+ console.print(f"[red]✗[/red] Could not find Python executable: {e}")
328
+ raise FileNotFoundError(
329
+ "Could not find mcp-ticketer Python executable. "
330
+ "Please ensure mcp-ticketer is installed.\n"
331
+ "Install with: pip install mcp-ticketer or pipx install mcp-ticketer"
332
+ )
299
333
 
300
334
  # Step 2: Load project configuration
301
335
  console.print("\n[cyan]📖 Reading project configuration...[/cyan]")
@@ -327,9 +361,11 @@ def configure_claude_mcp(global_config: bool = False, force: bool = False) -> No
327
361
  console.print("[yellow]⚠ Overwriting existing configuration[/yellow]")
328
362
 
329
363
  # Step 6: Create mcp-ticketer server config
330
- cwd = str(Path.cwd()) if not global_config else None
364
+ project_path = str(Path.cwd()) if not global_config else None
331
365
  server_config = create_mcp_server_config(
332
- binary_path=binary_path, project_config=project_config, cwd=cwd
366
+ python_path=python_path,
367
+ project_config=project_config,
368
+ project_path=project_path,
333
369
  )
334
370
 
335
371
  # Step 7: Update MCP configuration
@@ -348,9 +384,10 @@ def configure_claude_mcp(global_config: bool = False, force: bool = False) -> No
348
384
  console.print("\n[bold]Configuration Details:[/bold]")
349
385
  console.print(" Server name: mcp-ticketer")
350
386
  console.print(f" Adapter: {adapter}")
351
- console.print(f" Binary: {binary_path}")
352
- if cwd:
353
- console.print(f" Working directory: {cwd}")
387
+ console.print(f" Python: {python_path}")
388
+ console.print(" Command: python -m mcp_ticketer.mcp.server")
389
+ if project_path:
390
+ console.print(f" Project path: {project_path}")
354
391
  if "env" in server_config:
355
392
  console.print(
356
393
  f" Environment variables: {list(server_config['env'].keys())}"
@@ -0,0 +1,126 @@
1
+ """Reliable Python executable detection for mcp-ticketer.
2
+
3
+ This module provides reliable detection of the Python executable for mcp-ticketer
4
+ across different installation methods (pipx, pip, uv, direct venv).
5
+
6
+ The module follows the proven pattern from mcp-vector-search:
7
+ - Detect venv Python path reliably
8
+ - Use `python -m mcp_ticketer.mcp.server` instead of binary paths
9
+ - Support multiple installation methods transparently
10
+ """
11
+
12
+ import os
13
+ import shutil
14
+ import sys
15
+ from pathlib import Path
16
+
17
+
18
+ def get_mcp_ticketer_python(project_path: Path | None = None) -> str:
19
+ """Get the correct Python executable for mcp-ticketer MCP server.
20
+
21
+ This function follows the mcp-vector-search pattern of using project-specific
22
+ venv Python for proper project isolation and dependency management.
23
+
24
+ Detection priority:
25
+ 1. Project-local venv (.venv/bin/python) if project_path provided
26
+ 2. Current Python executable if in pipx venv
27
+ 3. Python from mcp-ticketer binary shebang
28
+ 4. Current Python executable (fallback)
29
+
30
+ Args:
31
+ project_path: Optional project directory path to check for local venv
32
+
33
+ Returns:
34
+ Path to Python executable
35
+
36
+ Examples:
37
+ >>> # With project venv
38
+ >>> python_path = get_mcp_ticketer_python(Path("/home/user/my-project"))
39
+ >>> # Returns: "/home/user/my-project/.venv/bin/python"
40
+
41
+ >>> # Without project path (fallback to pipx)
42
+ >>> python_path = get_mcp_ticketer_python()
43
+ >>> # Returns: "/Users/user/.local/pipx/venvs/mcp-ticketer/bin/python"
44
+
45
+ """
46
+ # Priority 1: Check for project-local venv
47
+ if project_path:
48
+ project_venv_python = project_path / ".venv" / "bin" / "python"
49
+ if project_venv_python.exists():
50
+ return str(project_venv_python)
51
+
52
+ current_executable = sys.executable
53
+
54
+ # Priority 2: Check if we're in a pipx venv
55
+ if "/pipx/venvs/" in current_executable:
56
+ return current_executable
57
+
58
+ # Priority 3: Check mcp-ticketer binary shebang
59
+ mcp_ticketer_path = shutil.which("mcp-ticketer")
60
+ if mcp_ticketer_path:
61
+ try:
62
+ with open(mcp_ticketer_path) as f:
63
+ first_line = f.readline().strip()
64
+ if first_line.startswith("#!") and "python" in first_line:
65
+ python_path = first_line[2:].strip()
66
+ if os.path.exists(python_path):
67
+ return python_path
68
+ except OSError:
69
+ pass
70
+
71
+ # Priority 4: Fallback to current Python
72
+ return current_executable
73
+
74
+
75
+ def get_mcp_server_command(project_path: str | None = None) -> tuple[str, list[str]]:
76
+ """Get the complete command to run the MCP server.
77
+
78
+ Args:
79
+ project_path: Optional project path to pass as argument and check for venv
80
+
81
+ Returns:
82
+ Tuple of (python_executable, args_list)
83
+ Example: ("/path/to/python", ["-m", "mcp_ticketer.mcp.server", "/project/path"])
84
+
85
+ Examples:
86
+ >>> python, args = get_mcp_server_command("/home/user/project")
87
+ >>> # python: "/home/user/project/.venv/bin/python" (if .venv exists)
88
+ >>> # args: ["-m", "mcp_ticketer.mcp.server", "/home/user/project"]
89
+
90
+ """
91
+ # Convert project_path to Path object for venv detection
92
+ project_path_obj = Path(project_path) if project_path else None
93
+ python_path = get_mcp_ticketer_python(project_path=project_path_obj)
94
+ args = ["-m", "mcp_ticketer.mcp.server"]
95
+
96
+ if project_path:
97
+ args.append(str(project_path))
98
+
99
+ return python_path, args
100
+
101
+
102
+ def validate_python_executable(python_path: str) -> bool:
103
+ """Validate that a Python executable can import mcp_ticketer.
104
+
105
+ Args:
106
+ python_path: Path to Python executable to validate
107
+
108
+ Returns:
109
+ True if Python can import mcp_ticketer, False otherwise
110
+
111
+ Examples:
112
+ >>> is_valid = validate_python_executable("/usr/bin/python3")
113
+ >>> # Returns: False (system Python doesn't have mcp_ticketer)
114
+
115
+ """
116
+ try:
117
+ import subprocess
118
+
119
+ result = subprocess.run(
120
+ [python_path, "-c", "import mcp_ticketer.mcp.server"],
121
+ capture_output=True,
122
+ timeout=5,
123
+ )
124
+ return result.returncode == 0
125
+ except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
126
+ return False
@@ -1,8 +1,7 @@
1
1
  """Core models and abstractions for MCP Ticketer."""
2
2
 
3
3
  from .adapter import BaseAdapter
4
- from .models import (Attachment, Comment, Epic, Priority, Task, TicketState,
5
- TicketType)
4
+ from .models import Attachment, Comment, Epic, Priority, Task, TicketState, TicketType
6
5
  from .registry import AdapterRegistry
7
6
 
8
7
  __all__ = [