mcp-ticketer 2.0.1__py3-none-any.whl → 2.2.13__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 (73) hide show
  1. mcp_ticketer/__version__.py +1 -1
  2. mcp_ticketer/_version_scm.py +1 -0
  3. mcp_ticketer/adapters/aitrackdown.py +122 -0
  4. mcp_ticketer/adapters/asana/adapter.py +121 -0
  5. mcp_ticketer/adapters/github/__init__.py +26 -0
  6. mcp_ticketer/adapters/{github.py → github/adapter.py} +1506 -365
  7. mcp_ticketer/adapters/github/client.py +335 -0
  8. mcp_ticketer/adapters/github/mappers.py +797 -0
  9. mcp_ticketer/adapters/github/queries.py +692 -0
  10. mcp_ticketer/adapters/github/types.py +460 -0
  11. mcp_ticketer/adapters/jira/__init__.py +35 -0
  12. mcp_ticketer/adapters/{jira.py → jira/adapter.py} +250 -678
  13. mcp_ticketer/adapters/jira/client.py +271 -0
  14. mcp_ticketer/adapters/jira/mappers.py +246 -0
  15. mcp_ticketer/adapters/jira/queries.py +216 -0
  16. mcp_ticketer/adapters/jira/types.py +304 -0
  17. mcp_ticketer/adapters/linear/adapter.py +1000 -92
  18. mcp_ticketer/adapters/linear/client.py +91 -1
  19. mcp_ticketer/adapters/linear/mappers.py +107 -0
  20. mcp_ticketer/adapters/linear/queries.py +112 -2
  21. mcp_ticketer/adapters/linear/types.py +50 -10
  22. mcp_ticketer/cli/configure.py +524 -89
  23. mcp_ticketer/cli/install_mcp_server.py +418 -0
  24. mcp_ticketer/cli/main.py +10 -0
  25. mcp_ticketer/cli/mcp_configure.py +177 -49
  26. mcp_ticketer/cli/platform_installer.py +9 -0
  27. mcp_ticketer/cli/setup_command.py +157 -1
  28. mcp_ticketer/cli/ticket_commands.py +443 -81
  29. mcp_ticketer/cli/utils.py +113 -0
  30. mcp_ticketer/core/__init__.py +28 -0
  31. mcp_ticketer/core/adapter.py +367 -1
  32. mcp_ticketer/core/milestone_manager.py +252 -0
  33. mcp_ticketer/core/models.py +345 -0
  34. mcp_ticketer/core/project_utils.py +281 -0
  35. mcp_ticketer/core/project_validator.py +376 -0
  36. mcp_ticketer/core/session_state.py +6 -1
  37. mcp_ticketer/core/state_matcher.py +36 -3
  38. mcp_ticketer/mcp/server/__main__.py +2 -1
  39. mcp_ticketer/mcp/server/routing.py +68 -0
  40. mcp_ticketer/mcp/server/tools/__init__.py +7 -4
  41. mcp_ticketer/mcp/server/tools/attachment_tools.py +3 -1
  42. mcp_ticketer/mcp/server/tools/config_tools.py +233 -35
  43. mcp_ticketer/mcp/server/tools/milestone_tools.py +338 -0
  44. mcp_ticketer/mcp/server/tools/search_tools.py +30 -1
  45. mcp_ticketer/mcp/server/tools/ticket_tools.py +37 -1
  46. mcp_ticketer/queue/queue.py +68 -0
  47. {mcp_ticketer-2.0.1.dist-info → mcp_ticketer-2.2.13.dist-info}/METADATA +33 -3
  48. {mcp_ticketer-2.0.1.dist-info → mcp_ticketer-2.2.13.dist-info}/RECORD +72 -36
  49. mcp_ticketer-2.2.13.dist-info/top_level.txt +2 -0
  50. py_mcp_installer/examples/phase3_demo.py +178 -0
  51. py_mcp_installer/scripts/manage_version.py +54 -0
  52. py_mcp_installer/setup.py +6 -0
  53. py_mcp_installer/src/py_mcp_installer/__init__.py +153 -0
  54. py_mcp_installer/src/py_mcp_installer/command_builder.py +445 -0
  55. py_mcp_installer/src/py_mcp_installer/config_manager.py +541 -0
  56. py_mcp_installer/src/py_mcp_installer/exceptions.py +243 -0
  57. py_mcp_installer/src/py_mcp_installer/installation_strategy.py +617 -0
  58. py_mcp_installer/src/py_mcp_installer/installer.py +656 -0
  59. py_mcp_installer/src/py_mcp_installer/mcp_inspector.py +750 -0
  60. py_mcp_installer/src/py_mcp_installer/platform_detector.py +451 -0
  61. py_mcp_installer/src/py_mcp_installer/platforms/__init__.py +26 -0
  62. py_mcp_installer/src/py_mcp_installer/platforms/claude_code.py +225 -0
  63. py_mcp_installer/src/py_mcp_installer/platforms/codex.py +181 -0
  64. py_mcp_installer/src/py_mcp_installer/platforms/cursor.py +191 -0
  65. py_mcp_installer/src/py_mcp_installer/types.py +222 -0
  66. py_mcp_installer/src/py_mcp_installer/utils.py +463 -0
  67. py_mcp_installer/tests/__init__.py +0 -0
  68. py_mcp_installer/tests/platforms/__init__.py +0 -0
  69. py_mcp_installer/tests/test_platform_detector.py +17 -0
  70. mcp_ticketer-2.0.1.dist-info/top_level.txt +0 -1
  71. {mcp_ticketer-2.0.1.dist-info → mcp_ticketer-2.2.13.dist-info}/WHEEL +0 -0
  72. {mcp_ticketer-2.0.1.dist-info → mcp_ticketer-2.2.13.dist-info}/entry_points.txt +0 -0
  73. {mcp_ticketer-2.0.1.dist-info → mcp_ticketer-2.2.13.dist-info}/licenses/LICENSE +0 -0
@@ -2,6 +2,7 @@
2
2
 
3
3
  import json
4
4
  import os
5
+ import shutil
5
6
  import subprocess
6
7
  import sys
7
8
  from pathlib import Path
@@ -13,6 +14,36 @@ from .python_detection import get_mcp_ticketer_python
13
14
  console = Console()
14
15
 
15
16
 
17
+ def is_mcp_ticketer_in_path() -> bool:
18
+ """Check if mcp-ticketer command is accessible via PATH.
19
+
20
+ This is critical for native Claude CLI mode, which writes bare
21
+ command names like "mcp-ticketer" instead of full paths.
22
+
23
+ Returns:
24
+ True if mcp-ticketer can be found in PATH, False otherwise.
25
+
26
+ Examples:
27
+ >>> # pipx with PATH configured
28
+ >>> is_mcp_ticketer_in_path()
29
+ True
30
+
31
+ >>> # pipx without PATH configured
32
+ >>> is_mcp_ticketer_in_path()
33
+ False
34
+
35
+ """
36
+ result = shutil.which("mcp-ticketer") is not None
37
+ if result:
38
+ console.print("[dim]✓ mcp-ticketer found in PATH[/dim]", highlight=False)
39
+ else:
40
+ console.print(
41
+ "[dim]⚠ mcp-ticketer not in PATH (will use legacy JSON mode)[/dim]",
42
+ highlight=False,
43
+ )
44
+ return result
45
+
46
+
16
47
  def is_claude_cli_available() -> bool:
17
48
  """Check if Claude CLI is available in PATH.
18
49
 
@@ -57,45 +88,46 @@ def build_claude_mcp_command(
57
88
  # Transport: always stdio
58
89
  cmd.extend(["--transport", "stdio"])
59
90
 
60
- # Environment variables (credentials)
91
+ # Server name - MUST come before -e flags per Claude CLI syntax:
92
+ # claude mcp add [options] <name> -e KEY=val... -- <command> [args...]
93
+ cmd.append("mcp-ticketer")
94
+
95
+ # Environment variables (credentials) - MUST come after server name
61
96
  adapters = project_config.get("adapters", {})
62
97
 
63
98
  # Linear adapter
64
99
  if "linear" in adapters:
65
100
  linear_config = adapters["linear"]
66
101
  if "api_key" in linear_config:
67
- cmd.extend(["--env", f"LINEAR_API_KEY={linear_config['api_key']}"])
102
+ cmd.extend(["-e", f"LINEAR_API_KEY={linear_config['api_key']}"])
68
103
  if "team_id" in linear_config:
69
- cmd.extend(["--env", f"LINEAR_TEAM_ID={linear_config['team_id']}"])
104
+ cmd.extend(["-e", f"LINEAR_TEAM_ID={linear_config['team_id']}"])
70
105
  if "team_key" in linear_config:
71
- cmd.extend(["--env", f"LINEAR_TEAM_KEY={linear_config['team_key']}"])
106
+ cmd.extend(["-e", f"LINEAR_TEAM_KEY={linear_config['team_key']}"])
72
107
 
73
108
  # GitHub adapter
74
109
  if "github" in adapters:
75
110
  github_config = adapters["github"]
76
111
  if "token" in github_config:
77
- cmd.extend(["--env", f"GITHUB_TOKEN={github_config['token']}"])
112
+ cmd.extend(["-e", f"GITHUB_TOKEN={github_config['token']}"])
78
113
  if "owner" in github_config:
79
- cmd.extend(["--env", f"GITHUB_OWNER={github_config['owner']}"])
114
+ cmd.extend(["-e", f"GITHUB_OWNER={github_config['owner']}"])
80
115
  if "repo" in github_config:
81
- cmd.extend(["--env", f"GITHUB_REPO={github_config['repo']}"])
116
+ cmd.extend(["-e", f"GITHUB_REPO={github_config['repo']}"])
82
117
 
83
118
  # JIRA adapter
84
119
  if "jira" in adapters:
85
120
  jira_config = adapters["jira"]
86
121
  if "api_token" in jira_config:
87
- cmd.extend(["--env", f"JIRA_API_TOKEN={jira_config['api_token']}"])
122
+ cmd.extend(["-e", f"JIRA_API_TOKEN={jira_config['api_token']}"])
88
123
  if "email" in jira_config:
89
- cmd.extend(["--env", f"JIRA_EMAIL={jira_config['email']}"])
124
+ cmd.extend(["-e", f"JIRA_EMAIL={jira_config['email']}"])
90
125
  if "url" in jira_config:
91
- cmd.extend(["--env", f"JIRA_URL={jira_config['url']}"])
126
+ cmd.extend(["-e", f"JIRA_URL={jira_config['url']}"])
92
127
 
93
128
  # Add default adapter
94
129
  default_adapter = project_config.get("default_adapter", "aitrackdown")
95
- cmd.extend(["--env", f"MCP_TICKETER_ADAPTER={default_adapter}"])
96
-
97
- # Server label
98
- cmd.append("mcp-ticketer")
130
+ cmd.extend(["-e", f"MCP_TICKETER_ADAPTER={default_adapter}"])
99
131
 
100
132
  # Command separator
101
133
  cmd.append("--")
@@ -118,6 +150,10 @@ def configure_claude_mcp_native(
118
150
  ) -> None:
119
151
  """Configure Claude Code using native 'claude mcp add' command.
120
152
 
153
+ This method is preferred when both Claude CLI and mcp-ticketer
154
+ are available in PATH. It provides better integration and
155
+ automatic updates.
156
+
121
157
  Args:
122
158
  project_config: Project configuration dict
123
159
  project_path: Path to project directory
@@ -129,6 +165,9 @@ def configure_claude_mcp_native(
129
165
  subprocess.TimeoutExpired: If command times out
130
166
 
131
167
  """
168
+ console.print("[cyan]⚙️ Configuring MCP via native Claude CLI[/cyan]")
169
+ console.print("[dim]Command will be: mcp-ticketer (resolved from PATH)[/dim]")
170
+
132
171
  # Auto-remove before re-adding when force=True
133
172
  if force:
134
173
  console.print("[cyan]🗑️ Force mode: Removing existing configuration...[/cyan]")
@@ -159,7 +198,7 @@ def configure_claude_mcp_native(
159
198
  # Show command to user (mask sensitive values)
160
199
  masked_cmd = []
161
200
  for i, arg in enumerate(cmd):
162
- if arg.startswith("--env=") or (i > 0 and cmd[i - 1] == "--env"):
201
+ if arg.startswith("-e=") or (i > 0 and cmd[i - 1] == "-e"):
163
202
  # Mask environment variable values
164
203
  if "=" in arg:
165
204
  key, _ = arg.split("=", 1)
@@ -218,6 +257,69 @@ def configure_claude_mcp_native(
218
257
  raise
219
258
 
220
259
 
260
+ def _get_adapter_env_vars() -> dict[str, str]:
261
+ """Get environment variables for the configured adapter from project config.
262
+
263
+ Reads credentials from .mcp-ticketer/config.json and returns them as
264
+ environment variables suitable for MCP server configuration.
265
+
266
+ Returns:
267
+ Dict of environment variables with adapter credentials
268
+
269
+ Example:
270
+ >>> env_vars = _get_adapter_env_vars()
271
+ >>> env_vars
272
+ {
273
+ 'MCP_TICKETER_ADAPTER': 'github',
274
+ 'GITHUB_TOKEN': 'ghp_...',
275
+ 'GITHUB_OWNER': 'username',
276
+ 'GITHUB_REPO': 'repo-name'
277
+ }
278
+
279
+ """
280
+ config_path = Path.cwd() / ".mcp-ticketer" / "config.json"
281
+ if not config_path.exists():
282
+ return {}
283
+
284
+ try:
285
+ with open(config_path) as f:
286
+ config = json.load(f)
287
+
288
+ adapter_type = config.get("default_adapter", "aitrackdown")
289
+ adapters = config.get("adapters", {})
290
+ adapter_config = adapters.get(adapter_type, {})
291
+
292
+ env_vars = {"MCP_TICKETER_ADAPTER": adapter_type}
293
+
294
+ if adapter_type == "github":
295
+ if token := adapter_config.get("token"):
296
+ env_vars["GITHUB_TOKEN"] = token
297
+ if owner := adapter_config.get("owner"):
298
+ env_vars["GITHUB_OWNER"] = owner
299
+ if repo := adapter_config.get("repo"):
300
+ env_vars["GITHUB_REPO"] = repo
301
+ elif adapter_type == "linear":
302
+ if api_key := adapter_config.get("api_key"):
303
+ env_vars["LINEAR_API_KEY"] = api_key
304
+ if team_key := adapter_config.get("team_key"):
305
+ env_vars["LINEAR_TEAM_KEY"] = team_key
306
+ elif team_id := adapter_config.get("team_id"):
307
+ env_vars["LINEAR_TEAM_ID"] = team_id
308
+ elif adapter_type == "jira":
309
+ if api_token := adapter_config.get("api_token"):
310
+ env_vars["JIRA_API_TOKEN"] = api_token
311
+ if server := adapter_config.get("server"):
312
+ env_vars["JIRA_SERVER"] = server
313
+ if email := adapter_config.get("email"):
314
+ env_vars["JIRA_EMAIL"] = email
315
+ if project := adapter_config.get("project_key"):
316
+ env_vars["JIRA_PROJECT_KEY"] = project
317
+
318
+ return env_vars
319
+ except (json.JSONDecodeError, OSError):
320
+ return {}
321
+
322
+
221
323
  def load_env_file(env_path: Path) -> dict[str, str]:
222
324
  """Load environment variables from .env file.
223
325
 
@@ -447,25 +549,23 @@ def create_mcp_server_config(
447
549
  # Environment variables below are optional fallbacks for backward compatibility
448
550
  # The FastMCP SDK server will automatically load config from the project directory
449
551
 
450
- adapter = project_config.get("default_adapter", "aitrackdown")
451
- adapters_config = project_config.get("adapters", {})
452
- adapter_config = adapters_config.get(adapter, {})
453
-
454
552
  env_vars = {}
455
553
 
456
554
  # Add PYTHONPATH for project context (only for project-specific configs)
457
555
  if project_path and not is_global_config:
458
556
  env_vars["PYTHONPATH"] = project_path
459
557
 
460
- # Add MCP_TICKETER_ADAPTER to identify which adapter to use (optional fallback)
461
- env_vars["MCP_TICKETER_ADAPTER"] = adapter
558
+ # Get adapter credentials from project config
559
+ # This is the primary source for MCP server environment variables
560
+ adapter_env_vars = _get_adapter_env_vars()
561
+ env_vars.update(adapter_env_vars)
462
562
 
463
- # Load environment variables from .env.local if it exists
563
+ # Load environment variables from .env.local if it exists (as override)
464
564
  if project_path:
465
565
  env_file_path = Path(project_path) / ".env.local"
466
566
  env_file_vars = load_env_file(env_file_path)
467
567
 
468
- # Add relevant adapter-specific vars from .env.local
568
+ # Add relevant adapter-specific vars from .env.local (overrides config.json)
469
569
  adapter_env_keys = {
470
570
  "linear": ["LINEAR_API_KEY", "LINEAR_TEAM_ID", "LINEAR_TEAM_KEY"],
471
571
  "github": ["GITHUB_TOKEN", "GITHUB_OWNER", "GITHUB_REPO"],
@@ -480,24 +580,12 @@ def create_mcp_server_config(
480
580
  "aitrackdown": [], # No specific env vars needed
481
581
  }
482
582
 
483
- # Include adapter-specific env vars from .env.local
583
+ adapter = env_vars.get("MCP_TICKETER_ADAPTER", "aitrackdown")
584
+ # Include adapter-specific env vars from .env.local (overrides config.json)
484
585
  for key in adapter_env_keys.get(adapter, []):
485
586
  if key in env_file_vars:
486
587
  env_vars[key] = env_file_vars[key]
487
588
 
488
- # Fallback: Add adapter-specific environment variables from project config
489
- if adapter == "linear" and "api_key" in adapter_config:
490
- if "LINEAR_API_KEY" not in env_vars:
491
- env_vars["LINEAR_API_KEY"] = adapter_config["api_key"]
492
- elif adapter == "github" and "token" in adapter_config:
493
- if "GITHUB_TOKEN" not in env_vars:
494
- env_vars["GITHUB_TOKEN"] = adapter_config["token"]
495
- elif adapter == "jira":
496
- if "api_token" in adapter_config and "JIRA_API_TOKEN" not in env_vars:
497
- env_vars["JIRA_API_TOKEN"] = adapter_config["api_token"]
498
- if "email" in adapter_config and "JIRA_EMAIL" not in env_vars:
499
- env_vars["JIRA_EMAIL"] = adapter_config["email"]
500
-
501
589
  if env_vars:
502
590
  config["env"] = env_vars
503
591
 
@@ -845,9 +933,16 @@ def configure_claude_mcp(global_config: bool = False, force: bool = False) -> No
845
933
  console.print(f"[red]✗[/red] {e}")
846
934
  raise
847
935
 
848
- # Check for native CLI availability
936
+ # Check for native CLI availability AND PATH configuration
849
937
  console.print("\n[cyan]🔍 Checking for Claude CLI...[/cyan]")
850
- if is_claude_cli_available():
938
+
939
+ # Native CLI requires both claude command AND mcp-ticketer in PATH
940
+ claude_cli_available = is_claude_cli_available()
941
+ mcp_ticketer_in_path = is_mcp_ticketer_in_path()
942
+
943
+ use_native_cli = claude_cli_available and mcp_ticketer_in_path
944
+
945
+ if use_native_cli:
851
946
  console.print("[green]✓[/green] Claude CLI found - using native command")
852
947
  console.print(
853
948
  "[dim]This provides better integration and automatic updates[/dim]"
@@ -863,13 +958,25 @@ def configure_claude_mcp(global_config: bool = False, force: bool = False) -> No
863
958
  force=force,
864
959
  )
865
960
 
866
- # Fall back to JSON manipulation
867
- console.print(
868
- "[yellow]⚠[/yellow] Claude CLI not found - using legacy JSON configuration"
869
- )
870
- console.print(
871
- "[dim]For better experience, install Claude CLI: https://docs.claude.ai/cli[/dim]"
872
- )
961
+ # Fall back to reliable JSON manipulation with full paths
962
+ if claude_cli_available and not mcp_ticketer_in_path:
963
+ console.print(
964
+ "[yellow]⚠[/yellow] mcp-ticketer not found in PATH - using legacy JSON mode"
965
+ )
966
+ console.print(
967
+ "[dim]Native CLI writes bare command names that fail when not in PATH[/dim]"
968
+ )
969
+ console.print(
970
+ "[dim]To enable native CLI, add pipx bin directory to your PATH:[/dim]"
971
+ )
972
+ console.print('[dim] export PATH="$HOME/.local/bin:$PATH"[/dim]')
973
+ elif not claude_cli_available:
974
+ console.print(
975
+ "[yellow]⚠[/yellow] Claude CLI not found - using legacy JSON configuration"
976
+ )
977
+ console.print(
978
+ "[dim]For better experience, install Claude CLI: https://docs.claude.ai/cli[/dim]"
979
+ )
873
980
 
874
981
  # Auto-remove before re-adding when force=True
875
982
  if force:
@@ -893,6 +1000,10 @@ def configure_claude_mcp(global_config: bool = False, force: bool = False) -> No
893
1000
 
894
1001
  console.print() # Blank line for visual separation
895
1002
 
1003
+ # Show that we're using legacy JSON mode with full paths
1004
+ console.print("\n[cyan]⚙️ Configuring MCP via legacy JSON mode[/cyan]")
1005
+ console.print("[dim]This mode uses full paths for reliable operation[/dim]")
1006
+
896
1007
  # Determine project path for venv detection
897
1008
  project_path = Path.cwd() if not global_config else None
898
1009
 
@@ -907,6 +1018,11 @@ def configure_claude_mcp(global_config: bool = False, force: bool = False) -> No
907
1018
  console.print("[dim]Using project-specific venv[/dim]")
908
1019
  else:
909
1020
  console.print("[dim]Using pipx/system Python[/dim]")
1021
+
1022
+ # Derive CLI path from Python path
1023
+ python_dir = Path(python_path).parent
1024
+ cli_path = str(python_dir / "mcp-ticketer")
1025
+ console.print(f"[dim]CLI command will be: {cli_path}[/dim]")
910
1026
  except Exception as e:
911
1027
  console.print(f"[red]✗[/red] Could not find Python executable: {e}")
912
1028
  raise FileNotFoundError(
@@ -1058,9 +1174,21 @@ def configure_claude_mcp(global_config: bool = False, force: bool = False) -> No
1058
1174
  if absolute_project_path:
1059
1175
  console.print(f" Project path: {absolute_project_path}")
1060
1176
  if "env" in server_config:
1061
- console.print(
1062
- f" Environment variables: {list(server_config['env'].keys())}"
1063
- )
1177
+ env_keys = list(server_config["env"].keys())
1178
+ console.print(f" Environment variables: {env_keys}")
1179
+
1180
+ # Security warning about credentials
1181
+ sensitive_keys = [
1182
+ k for k in env_keys if "TOKEN" in k or "KEY" in k or "PASSWORD" in k
1183
+ ]
1184
+ if sensitive_keys:
1185
+ console.print(
1186
+ "\n[yellow]⚠️ Security Notice:[/yellow] Configuration contains credentials"
1187
+ )
1188
+ console.print(f"[yellow] Location: {mcp_config_path}[/yellow]")
1189
+ console.print(
1190
+ "[yellow] Make sure this file is excluded from version control[/yellow]"
1191
+ )
1064
1192
 
1065
1193
  # Migration success message (if legacy config was detected)
1066
1194
  if is_legacy:
@@ -13,6 +13,7 @@ from rich.table import Table
13
13
  # Import for typing compatibility
14
14
  from .init_command import init
15
15
  from .platform_detection import PlatformDetector, get_platform_by_name
16
+ from .setup_command import _update_mcp_json_credentials
16
17
 
17
18
  console = Console()
18
19
 
@@ -248,6 +249,10 @@ def install(
248
249
  console.print(f"[cyan]Installing for {plat.display_name}...[/cyan]")
249
250
  config_func()
250
251
  success_count += 1
252
+
253
+ # Update credentials in parent .mcp.json for Claude platforms
254
+ if plat.name in ("claude-code", "claude-desktop"):
255
+ _update_mcp_json_credentials(Path.cwd(), console)
251
256
  except Exception as e:
252
257
  console.print(
253
258
  f"[red]✗[/red] Failed to install for {plat.display_name}: {e}"
@@ -388,6 +393,10 @@ def install(
388
393
 
389
394
  try:
390
395
  config["func"]()
396
+
397
+ # Update credentials in parent .mcp.json for Claude platforms
398
+ if platform in ("claude-code", "claude-desktop"):
399
+ _update_mcp_json_credentials(Path.cwd(), console)
391
400
  except Exception as e:
392
401
  console.print(f"[red]Installation failed: {e}[/red]")
393
402
  raise typer.Exit(1) from e
@@ -332,6 +332,9 @@ def setup(
332
332
  # Check and install adapter-specific dependencies
333
333
  _check_and_install_adapter_dependencies(adapter_type, console)
334
334
 
335
+ # Update existing MCP configurations with new credentials
336
+ _update_mcp_json_credentials(proj_path, console)
337
+
335
338
  console.print("\n[green]✓ Adapter configuration complete[/green]\n")
336
339
  else:
337
340
  console.print("[green]✓ Step 1/2: Adapter already configured[/green]\n")
@@ -340,6 +343,9 @@ def setup(
340
343
  # This handles the case where credentials exist but defaults were never set
341
344
  _prompt_and_update_default_values(config_path, current_adapter, console)
342
345
 
346
+ # Update existing MCP configurations with credentials
347
+ _update_mcp_json_credentials(proj_path, console)
348
+
343
349
  # Step 3: Platform installation
344
350
  if skip_platforms:
345
351
  console.print(
@@ -352,7 +358,7 @@ def setup(
352
358
 
353
359
  # Detect available platforms
354
360
  detector = PlatformDetector()
355
- detected = detector.detect_all(project_path=proj_path)
361
+ detected = detector.detect_all(project_path=proj_path, exclude_desktop=True)
356
362
 
357
363
  if not detected:
358
364
  console.print("[yellow]No AI platforms detected on this system.[/yellow]")
@@ -612,6 +618,156 @@ def _check_existing_platform_configs(platforms: list, proj_path: Path) -> list[s
612
618
  return configured
613
619
 
614
620
 
621
+ def _update_mcp_json_credentials(proj_path: Path, console: Console) -> None:
622
+ """Update .mcp.json with adapter credentials if mcp-ticketer is already configured.
623
+
624
+ This function updates the existing MCP configuration with the latest credentials
625
+ from the project's .mcp-ticketer/config.json file. It also ensures .mcp.json is
626
+ added to .gitignore to prevent credential leaks.
627
+
628
+ Additionally, it updates the official @modelcontextprotocol/server-github MCP server
629
+ if found, since it also requires GITHUB_PERSONAL_ACCESS_TOKEN.
630
+
631
+ Args:
632
+ proj_path: Project path
633
+ console: Rich console for output
634
+
635
+ """
636
+ # Check multiple .mcp.json locations
637
+ new_mcp_json_path = Path.home() / ".config" / "claude" / "mcp.json"
638
+ old_mcp_json_path = Path.home() / ".claude.json"
639
+ legacy_mcp_json_path = proj_path / ".claude" / "mcp.local.json"
640
+ project_mcp_json_path = proj_path / ".mcp.json"
641
+
642
+ mcp_json_paths = [
643
+ (new_mcp_json_path, True), # (path, is_global_mcp_config)
644
+ (old_mcp_json_path, False),
645
+ (legacy_mcp_json_path, False),
646
+ (project_mcp_json_path, False),
647
+ ]
648
+
649
+ # Also check parent directories for .mcp.json (Claude Code inheritance)
650
+ # This handles cases like /Users/masa/Projects/.mcp.json
651
+ current = proj_path.parent
652
+ home = Path.home()
653
+ checked_parents: set[Path] = set()
654
+ while current != home and current != current.parent:
655
+ parent_mcp = current / ".mcp.json"
656
+ if parent_mcp not in checked_parents and parent_mcp.exists():
657
+ mcp_json_paths.append((parent_mcp, False))
658
+ checked_parents.add(parent_mcp)
659
+ current = current.parent
660
+
661
+ # Import the helper function to get adapter credentials
662
+ from .mcp_configure import _get_adapter_env_vars
663
+
664
+ env_vars = _get_adapter_env_vars()
665
+
666
+ if not env_vars:
667
+ return
668
+
669
+ updated_count = 0
670
+
671
+ for mcp_json_path, is_global_mcp_config in mcp_json_paths:
672
+ if not mcp_json_path.exists():
673
+ continue
674
+
675
+ try:
676
+ with open(mcp_json_path) as f:
677
+ mcp_config = json.load(f)
678
+
679
+ # Check if mcp-ticketer is configured
680
+ mcp_servers = None
681
+ if is_global_mcp_config:
682
+ # Global mcp.json uses flat structure
683
+ mcp_servers = mcp_config.get("mcpServers", {})
684
+ else:
685
+ # Old structure uses projects
686
+ projects = mcp_config.get("projects", {})
687
+ project_key = str(proj_path.resolve())
688
+ if project_key in projects:
689
+ mcp_servers = projects[project_key].get("mcpServers", {})
690
+ else:
691
+ # Try flat structure for backward compatibility
692
+ mcp_servers = mcp_config.get("mcpServers", {})
693
+
694
+ if mcp_servers is None:
695
+ continue
696
+
697
+ config_updated = False
698
+
699
+ # Update the mcp-ticketer server env vars if configured
700
+ if "mcp-ticketer" in mcp_servers:
701
+ current_env = mcp_servers["mcp-ticketer"].get("env", {})
702
+ current_env.update(env_vars)
703
+ mcp_servers["mcp-ticketer"]["env"] = current_env
704
+ config_updated = True
705
+
706
+ # Also update official @modelcontextprotocol/server-github if present
707
+ # This server uses GITHUB_PERSONAL_ACCESS_TOKEN instead of GITHUB_TOKEN
708
+ if "github" in mcp_servers and "GITHUB_TOKEN" in env_vars:
709
+ github_server = mcp_servers["github"]
710
+ # Check if it's the official GitHub MCP server (uses npx or server-github)
711
+ cmd = github_server.get("command", "")
712
+ args = github_server.get("args", [])
713
+ is_official_github = (
714
+ cmd == "npx" and any("server-github" in str(arg) for arg in args)
715
+ ) or "server-github" in cmd
716
+
717
+ if is_official_github:
718
+ current_env = github_server.get("env", {})
719
+ # The official server uses GITHUB_PERSONAL_ACCESS_TOKEN
720
+ current_env["GITHUB_PERSONAL_ACCESS_TOKEN"] = env_vars[
721
+ "GITHUB_TOKEN"
722
+ ]
723
+ github_server["env"] = current_env
724
+ config_updated = True
725
+ console.print(
726
+ f"[dim] Also updated official GitHub MCP server in {mcp_json_path}[/dim]"
727
+ )
728
+
729
+ if not config_updated:
730
+ continue
731
+
732
+ # Save updated config
733
+ with open(mcp_json_path, "w") as f:
734
+ json.dump(mcp_config, f, indent=2)
735
+
736
+ updated_count += 1
737
+
738
+ except (json.JSONDecodeError, OSError) as e:
739
+ console.print(
740
+ f"[yellow]Warning: Could not update {mcp_json_path.name}: {e}[/yellow]"
741
+ )
742
+
743
+ if updated_count > 0:
744
+ console.print(
745
+ f"[green]✓[/green] Updated MCP configuration with adapter credentials ({updated_count} file(s))"
746
+ )
747
+
748
+ # Ensure .mcp.json files are in .gitignore (only for project-local files)
749
+ gitignore_path = proj_path / ".gitignore"
750
+ patterns_to_add = [".claude/", ".mcp.json"]
751
+
752
+ if gitignore_path.exists():
753
+ content = gitignore_path.read_text()
754
+ patterns_added = []
755
+
756
+ for pattern in patterns_to_add:
757
+ if pattern not in content:
758
+ patterns_added.append(pattern)
759
+
760
+ if patterns_added:
761
+ with open(gitignore_path, "a") as f:
762
+ f.write("\n# MCP configuration (contains tokens)\n")
763
+ for pattern in patterns_added:
764
+ f.write(f"{pattern}\n")
765
+
766
+ console.print(
767
+ f"[dim]✓ Added {', '.join(patterns_added)} to .gitignore[/dim]"
768
+ )
769
+
770
+
615
771
  def _show_setup_complete_message(console: Console, proj_path: Path) -> None:
616
772
  """Show setup complete message with next steps.
617
773