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
@@ -2,63 +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
- from typing import Optional
9
7
 
10
8
  from rich.console import Console
11
9
 
10
+ from .python_detection import get_mcp_ticketer_python
11
+
12
12
  console = Console()
13
13
 
14
14
 
15
- def find_mcp_ticketer_binary() -> str:
16
- """Find the mcp-ticketer binary path.
15
+ def load_env_file(env_path: Path) -> dict[str, str]:
16
+ """Load environment variables from .env file.
17
17
 
18
- Returns:
19
- Path to mcp-ticketer binary
18
+ Args:
19
+ env_path: Path to .env file
20
20
 
21
- Raises:
22
- FileNotFoundError: If binary not found
21
+ Returns:
22
+ Dict of environment variable key-value pairs
23
23
 
24
24
  """
25
- # Check if running from development environment
26
- import mcp_ticketer
27
-
28
- package_path = Path(mcp_ticketer.__file__).parent.parent.parent
29
-
30
- # Check for virtual environment bin
31
- possible_paths = [
32
- # Development paths
33
- package_path / "venv" / "bin" / "mcp-ticketer",
34
- package_path / ".venv" / "bin" / "mcp-ticketer",
35
- package_path / "test_venv" / "bin" / "mcp-ticketer",
36
- # System installation
37
- Path.home() / ".local" / "bin" / "mcp-ticketer",
38
- # pipx installation
39
- Path.home()
40
- / ".local"
41
- / "pipx"
42
- / "venvs"
43
- / "mcp-ticketer"
44
- / "bin"
45
- / "mcp-ticketer",
46
- ]
47
-
48
- # Check PATH
49
- which_result = shutil.which("mcp-ticketer")
50
- if which_result:
51
- return which_result
52
-
53
- # Check possible paths
54
- for path in possible_paths:
55
- if path.exists():
56
- return str(path.resolve())
57
-
58
- raise FileNotFoundError(
59
- "Could not find mcp-ticketer binary. Please ensure mcp-ticketer is installed.\n"
60
- "Install with: pip install mcp-ticketer"
61
- )
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
62
42
 
63
43
 
64
44
  def load_project_config() -> dict:
@@ -127,28 +107,44 @@ def find_claude_mcp_config(global_config: bool = False) -> Path:
127
107
  Path.home() / ".config" / "Claude" / "claude_desktop_config.json"
128
108
  )
129
109
  else:
130
- # Project-level configuration
131
- config_path = Path.cwd() / ".mcp" / "config.json"
110
+ # Claude Code configuration (project-specific)
111
+ config_path = Path.home() / ".claude.json"
132
112
 
133
113
  return config_path
134
114
 
135
115
 
136
- def load_claude_mcp_config(config_path: Path) -> dict:
116
+ def load_claude_mcp_config(config_path: Path, is_claude_code: bool = False) -> dict:
137
117
  """Load existing Claude MCP configuration or return empty structure.
138
118
 
139
119
  Args:
140
120
  config_path: Path to MCP config file
121
+ is_claude_code: If True, return Claude Code structure with projects
141
122
 
142
123
  Returns:
143
124
  MCP configuration dict
144
125
 
145
126
  """
146
127
  if config_path.exists():
147
- with open(config_path) as f:
148
- return json.load(f)
128
+ try:
129
+ with open(config_path) as f:
130
+ content = f.read().strip()
131
+ if not content:
132
+ # Empty file, return default structure
133
+ return {"projects": {}} if is_claude_code else {"mcpServers": {}}
134
+ return json.loads(content)
135
+ except json.JSONDecodeError as e:
136
+ console.print(
137
+ f"[yellow]⚠ Warning: Invalid JSON in {config_path}, creating new config[/yellow]"
138
+ )
139
+ console.print(f"[dim]Error: {e}[/dim]")
140
+ # Return default structure on parse error
141
+ return {"projects": {}} if is_claude_code else {"mcpServers": {}}
149
142
 
150
- # Return empty structure
151
- return {"mcpServers": {}}
143
+ # Return empty structure based on config type
144
+ if is_claude_code:
145
+ return {"projects": {}}
146
+ else:
147
+ return {"mcpServers": {}}
152
148
 
153
149
 
154
150
  def save_claude_mcp_config(config_path: Path, config: dict) -> None:
@@ -168,28 +164,33 @@ def save_claude_mcp_config(config_path: Path, config: dict) -> None:
168
164
 
169
165
 
170
166
  def create_mcp_server_config(
171
- binary_path: str, project_config: dict, cwd: Optional[str] = None
167
+ python_path: str, project_config: dict, project_path: str | None = None
172
168
  ) -> dict:
173
169
  """Create MCP server configuration for mcp-ticketer.
174
170
 
175
171
  Args:
176
- binary_path: Path to mcp-ticketer binary
172
+ python_path: Path to Python executable in mcp-ticketer venv
177
173
  project_config: Project configuration from .mcp-ticketer/config.json
178
- cwd: Working directory for server (optional)
174
+ project_path: Project directory path (optional)
179
175
 
180
176
  Returns:
181
- MCP server configuration dict
177
+ MCP server configuration dict matching Claude Code stdio pattern
182
178
 
183
179
  """
180
+ # Use Python module invocation pattern (works regardless of where package is installed)
181
+ args = ["-m", "mcp_ticketer.mcp.server"]
182
+
183
+ # Add project path if provided
184
+ if project_path:
185
+ args.append(project_path)
186
+
187
+ # REQUIRED: Add "type": "stdio" for Claude Code compatibility
184
188
  config = {
185
- "command": binary_path,
186
- "args": ["serve"], # Use 'serve' command to start MCP server
189
+ "type": "stdio",
190
+ "command": python_path,
191
+ "args": args,
187
192
  }
188
193
 
189
- # Add working directory if provided
190
- if cwd:
191
- config["cwd"] = cwd
192
-
193
194
  # Add environment variables based on adapter
194
195
  adapter = project_config.get("default_adapter", "aitrackdown")
195
196
  adapters_config = project_config.get("adapters", {})
@@ -197,15 +198,49 @@ def create_mcp_server_config(
197
198
 
198
199
  env_vars = {}
199
200
 
200
- # Add adapter-specific environment variables
201
+ # Add PYTHONPATH for project context
202
+ if project_path:
203
+ env_vars["PYTHONPATH"] = project_path
204
+
205
+ # Add MCP_TICKETER_ADAPTER to identify which adapter to use
206
+ env_vars["MCP_TICKETER_ADAPTER"] = adapter
207
+
208
+ # Load environment variables from .env.local if it exists
209
+ if project_path:
210
+ env_file_path = Path(project_path) / ".env.local"
211
+ env_file_vars = load_env_file(env_file_path)
212
+
213
+ # Add relevant adapter-specific vars from .env.local
214
+ adapter_env_keys = {
215
+ "linear": ["LINEAR_API_KEY", "LINEAR_TEAM_ID", "LINEAR_TEAM_KEY"],
216
+ "github": ["GITHUB_TOKEN", "GITHUB_OWNER", "GITHUB_REPO"],
217
+ "jira": [
218
+ "JIRA_ACCESS_USER",
219
+ "JIRA_ACCESS_TOKEN",
220
+ "JIRA_ORGANIZATION_ID",
221
+ "JIRA_URL",
222
+ "JIRA_EMAIL",
223
+ "JIRA_API_TOKEN",
224
+ ],
225
+ "aitrackdown": [], # No specific env vars needed
226
+ }
227
+
228
+ # Include adapter-specific env vars from .env.local
229
+ for key in adapter_env_keys.get(adapter, []):
230
+ if key in env_file_vars:
231
+ env_vars[key] = env_file_vars[key]
232
+
233
+ # Fallback: Add adapter-specific environment variables from project config
201
234
  if adapter == "linear" and "api_key" in adapter_config:
202
- env_vars["LINEAR_API_KEY"] = adapter_config["api_key"]
235
+ if "LINEAR_API_KEY" not in env_vars:
236
+ env_vars["LINEAR_API_KEY"] = adapter_config["api_key"]
203
237
  elif adapter == "github" and "token" in adapter_config:
204
- env_vars["GITHUB_TOKEN"] = adapter_config["token"]
238
+ if "GITHUB_TOKEN" not in env_vars:
239
+ env_vars["GITHUB_TOKEN"] = adapter_config["token"]
205
240
  elif adapter == "jira":
206
- if "api_token" in adapter_config:
241
+ if "api_token" in adapter_config and "JIRA_API_TOKEN" not in env_vars:
207
242
  env_vars["JIRA_API_TOKEN"] = adapter_config["api_token"]
208
- if "email" in adapter_config:
243
+ if "email" in adapter_config and "JIRA_EMAIL" not in env_vars:
209
244
  env_vars["JIRA_EMAIL"] = adapter_config["email"]
210
245
 
211
246
  if env_vars:
@@ -214,6 +249,108 @@ def create_mcp_server_config(
214
249
  return config
215
250
 
216
251
 
252
+ def remove_claude_mcp(global_config: bool = False, dry_run: bool = False) -> None:
253
+ """Remove mcp-ticketer from Claude Code/Desktop configuration.
254
+
255
+ Args:
256
+ global_config: Remove from Claude Desktop instead of project-level
257
+ dry_run: Show what would be removed without making changes
258
+
259
+ """
260
+ # Step 1: Find Claude MCP config location
261
+ config_type = "Claude Desktop" if global_config else "Claude Code"
262
+ console.print(f"[cyan]🔍 Removing {config_type} MCP configuration...[/cyan]")
263
+
264
+ mcp_config_path = find_claude_mcp_config(global_config)
265
+ console.print(f"[dim]Primary config: {mcp_config_path}[/dim]")
266
+
267
+ # Get absolute project path for Claude Code
268
+ absolute_project_path = str(Path.cwd().resolve()) if not global_config else None
269
+
270
+ # Step 2: Check if config file exists
271
+ if not mcp_config_path.exists():
272
+ console.print(f"[yellow]⚠ No configuration found at {mcp_config_path}[/yellow]")
273
+ console.print("[dim]mcp-ticketer is not configured for this platform[/dim]")
274
+ return
275
+
276
+ # Step 3: Load existing MCP configuration
277
+ is_claude_code = not global_config
278
+ mcp_config = load_claude_mcp_config(mcp_config_path, is_claude_code=is_claude_code)
279
+
280
+ # Step 4: Check if mcp-ticketer is configured
281
+ is_configured = False
282
+ if is_claude_code:
283
+ # Check Claude Code structure: .projects[path].mcpServers["mcp-ticketer"]
284
+ if absolute_project_path:
285
+ projects = mcp_config.get("projects", {})
286
+ project_config_entry = projects.get(absolute_project_path, {})
287
+ is_configured = "mcp-ticketer" in project_config_entry.get("mcpServers", {})
288
+ else:
289
+ # Check Claude Desktop structure: .mcpServers["mcp-ticketer"]
290
+ is_configured = "mcp-ticketer" in mcp_config.get("mcpServers", {})
291
+
292
+ if not is_configured:
293
+ console.print("[yellow]⚠ mcp-ticketer is not configured[/yellow]")
294
+ console.print(f"[dim]No mcp-ticketer entry found in {mcp_config_path}[/dim]")
295
+ return
296
+
297
+ # Step 5: Show what would be removed (dry run or actual removal)
298
+ if dry_run:
299
+ console.print("\n[cyan]DRY RUN - Would remove:[/cyan]")
300
+ console.print(" Server name: mcp-ticketer")
301
+ console.print(f" From: {mcp_config_path}")
302
+ if absolute_project_path:
303
+ console.print(f" Project: {absolute_project_path}")
304
+ return
305
+
306
+ # Step 6: Remove mcp-ticketer from configuration
307
+ if is_claude_code and absolute_project_path:
308
+ # Remove from Claude Code structure
309
+ del mcp_config["projects"][absolute_project_path]["mcpServers"]["mcp-ticketer"]
310
+
311
+ # Clean up empty structures
312
+ if not mcp_config["projects"][absolute_project_path]["mcpServers"]:
313
+ del mcp_config["projects"][absolute_project_path]["mcpServers"]
314
+ if not mcp_config["projects"][absolute_project_path]:
315
+ del mcp_config["projects"][absolute_project_path]
316
+
317
+ # Also remove from legacy location if it exists
318
+ legacy_config_path = Path.cwd() / ".claude" / "mcp.local.json"
319
+ if legacy_config_path.exists():
320
+ try:
321
+ legacy_config = load_claude_mcp_config(
322
+ legacy_config_path, is_claude_code=False
323
+ )
324
+ if "mcp-ticketer" in legacy_config.get("mcpServers", {}):
325
+ del legacy_config["mcpServers"]["mcp-ticketer"]
326
+ save_claude_mcp_config(legacy_config_path, legacy_config)
327
+ console.print("[dim]✓ Removed from legacy config as well[/dim]")
328
+ except Exception as e:
329
+ console.print(f"[dim]⚠ Could not remove from legacy config: {e}[/dim]")
330
+ else:
331
+ # Remove from Claude Desktop structure
332
+ del mcp_config["mcpServers"]["mcp-ticketer"]
333
+
334
+ # Step 7: Save updated configuration
335
+ try:
336
+ save_claude_mcp_config(mcp_config_path, mcp_config)
337
+ console.print("\n[green]✓ Successfully removed mcp-ticketer[/green]")
338
+ console.print(f"[dim]Configuration updated: {mcp_config_path}[/dim]")
339
+
340
+ # Next steps
341
+ console.print("\n[bold cyan]Next Steps:[/bold cyan]")
342
+ if global_config:
343
+ console.print("1. Restart Claude Desktop")
344
+ console.print("2. mcp-ticketer will no longer be available in MCP menu")
345
+ else:
346
+ console.print("1. Restart Claude Code")
347
+ console.print("2. mcp-ticketer will no longer be available in this project")
348
+
349
+ except Exception as e:
350
+ console.print(f"\n[red]✗ Failed to update configuration:[/red] {e}")
351
+ raise
352
+
353
+
217
354
  def configure_claude_mcp(global_config: bool = False, force: bool = False) -> None:
218
355
  """Configure Claude Code to use mcp-ticketer.
219
356
 
@@ -222,18 +359,31 @@ def configure_claude_mcp(global_config: bool = False, force: bool = False) -> No
222
359
  force: Overwrite existing configuration
223
360
 
224
361
  Raises:
225
- FileNotFoundError: If binary or project config not found
362
+ FileNotFoundError: If Python executable or project config not found
226
363
  ValueError: If configuration is invalid
227
364
 
228
365
  """
229
- # Step 1: Find mcp-ticketer binary
230
- console.print("[cyan]🔍 Finding mcp-ticketer binary...[/cyan]")
366
+ # Determine project path for venv detection
367
+ project_path = Path.cwd() if not global_config else None
368
+
369
+ # Step 1: Find Python executable (project-specific if available)
370
+ console.print("[cyan]🔍 Finding mcp-ticketer Python executable...[/cyan]")
231
371
  try:
232
- binary_path = find_mcp_ticketer_binary()
233
- console.print(f"[green]✓[/green] Found: {binary_path}")
234
- except FileNotFoundError as e:
235
- console.print(f"[red]✗[/red] {e}")
236
- raise
372
+ python_path = get_mcp_ticketer_python(project_path=project_path)
373
+ console.print(f"[green]✓[/green] Found: {python_path}")
374
+
375
+ # Show if using project venv or fallback
376
+ if project_path and str(project_path / ".venv") in python_path:
377
+ console.print("[dim]Using project-specific venv[/dim]")
378
+ else:
379
+ console.print("[dim]Using pipx/system Python[/dim]")
380
+ except Exception as e:
381
+ console.print(f"[red]✗[/red] Could not find Python executable: {e}")
382
+ raise FileNotFoundError(
383
+ "Could not find mcp-ticketer Python executable. "
384
+ "Please ensure mcp-ticketer is installed.\n"
385
+ "Install with: pip install mcp-ticketer or pipx install mcp-ticketer"
386
+ ) from e
237
387
 
238
388
  # Step 2: Load project configuration
239
389
  console.print("\n[cyan]📖 Reading project configuration...[/cyan]")
@@ -246,17 +396,34 @@ def configure_claude_mcp(global_config: bool = False, force: bool = False) -> No
246
396
  raise
247
397
 
248
398
  # Step 3: Find Claude MCP config location
249
- config_type = "Claude Desktop" if global_config else "project-level"
399
+ config_type = "Claude Desktop" if global_config else "Claude Code"
250
400
  console.print(f"\n[cyan]🔧 Configuring {config_type} MCP...[/cyan]")
251
401
 
252
402
  mcp_config_path = find_claude_mcp_config(global_config)
253
- console.print(f"[dim]Config location: {mcp_config_path}[/dim]")
403
+ console.print(f"[dim]Primary config: {mcp_config_path}[/dim]")
404
+
405
+ # Get absolute project path for Claude Code
406
+ absolute_project_path = str(Path.cwd().resolve()) if not global_config else None
254
407
 
255
408
  # Step 4: Load existing MCP configuration
256
- mcp_config = load_claude_mcp_config(mcp_config_path)
409
+ is_claude_code = not global_config
410
+ mcp_config = load_claude_mcp_config(mcp_config_path, is_claude_code=is_claude_code)
257
411
 
258
412
  # Step 5: Check if mcp-ticketer already configured
259
- if "mcp-ticketer" in mcp_config.get("mcpServers", {}):
413
+ already_configured = False
414
+ if is_claude_code:
415
+ # Check Claude Code structure: .projects[path].mcpServers["mcp-ticketer"]
416
+ if absolute_project_path:
417
+ projects = mcp_config.get("projects", {})
418
+ project_config_entry = projects.get(absolute_project_path, {})
419
+ already_configured = "mcp-ticketer" in project_config_entry.get(
420
+ "mcpServers", {}
421
+ )
422
+ else:
423
+ # Check Claude Desktop structure: .mcpServers["mcp-ticketer"]
424
+ already_configured = "mcp-ticketer" in mcp_config.get("mcpServers", {})
425
+
426
+ if already_configured:
260
427
  if not force:
261
428
  console.print("[yellow]⚠ mcp-ticketer is already configured[/yellow]")
262
429
  console.print("[dim]Use --force to overwrite existing configuration[/dim]")
@@ -265,16 +432,55 @@ def configure_claude_mcp(global_config: bool = False, force: bool = False) -> No
265
432
  console.print("[yellow]⚠ Overwriting existing configuration[/yellow]")
266
433
 
267
434
  # Step 6: Create mcp-ticketer server config
268
- cwd = str(Path.cwd()) if not global_config else None
269
435
  server_config = create_mcp_server_config(
270
- binary_path=binary_path, project_config=project_config, cwd=cwd
436
+ python_path=python_path,
437
+ project_config=project_config,
438
+ project_path=absolute_project_path,
271
439
  )
272
440
 
273
- # Step 7: Update MCP configuration
274
- if "mcpServers" not in mcp_config:
275
- mcp_config["mcpServers"] = {}
276
-
277
- mcp_config["mcpServers"]["mcp-ticketer"] = server_config
441
+ # Step 7: Update MCP configuration based on platform
442
+ if is_claude_code:
443
+ # Claude Code: Write to ~/.claude.json with project-specific path
444
+ if absolute_project_path:
445
+ # Ensure projects structure exists
446
+ if "projects" not in mcp_config:
447
+ mcp_config["projects"] = {}
448
+
449
+ # Ensure project entry exists
450
+ if absolute_project_path not in mcp_config["projects"]:
451
+ mcp_config["projects"][absolute_project_path] = {}
452
+
453
+ # Ensure mcpServers for this project exists
454
+ if "mcpServers" not in mcp_config["projects"][absolute_project_path]:
455
+ mcp_config["projects"][absolute_project_path]["mcpServers"] = {}
456
+
457
+ # Add mcp-ticketer configuration
458
+ mcp_config["projects"][absolute_project_path]["mcpServers"][
459
+ "mcp-ticketer"
460
+ ] = server_config
461
+
462
+ # Also write to backward-compatible location for older Claude Code versions
463
+ legacy_config_path = Path.cwd() / ".claude" / "mcp.local.json"
464
+ console.print(f"[dim]Legacy config: {legacy_config_path}[/dim]")
465
+
466
+ try:
467
+ legacy_config = load_claude_mcp_config(
468
+ legacy_config_path, is_claude_code=False
469
+ )
470
+ if "mcpServers" not in legacy_config:
471
+ legacy_config["mcpServers"] = {}
472
+ legacy_config["mcpServers"]["mcp-ticketer"] = server_config
473
+ save_claude_mcp_config(legacy_config_path, legacy_config)
474
+ console.print("[dim]✓ Backward-compatible config also written[/dim]")
475
+ except Exception as e:
476
+ console.print(
477
+ f"[dim]⚠ Could not write legacy config (non-fatal): {e}[/dim]"
478
+ )
479
+ else:
480
+ # Claude Desktop: Write to platform-specific config
481
+ if "mcpServers" not in mcp_config:
482
+ mcp_config["mcpServers"] = {}
483
+ mcp_config["mcpServers"]["mcp-ticketer"] = server_config
278
484
 
279
485
  # Step 8: Save configuration
280
486
  try:
@@ -286,9 +492,10 @@ def configure_claude_mcp(global_config: bool = False, force: bool = False) -> No
286
492
  console.print("\n[bold]Configuration Details:[/bold]")
287
493
  console.print(" Server name: mcp-ticketer")
288
494
  console.print(f" Adapter: {adapter}")
289
- console.print(f" Binary: {binary_path}")
290
- if cwd:
291
- console.print(f" Working directory: {cwd}")
495
+ console.print(f" Python: {python_path}")
496
+ console.print(" Command: python -m mcp_ticketer.mcp.server")
497
+ if absolute_project_path:
498
+ console.print(f" Project path: {absolute_project_path}")
292
499
  if "env" in server_config:
293
500
  console.print(
294
501
  f" Environment variables: {list(server_config['env'].keys())}"
@@ -22,14 +22,17 @@ def migrate_config_command(dry_run: bool = False) -> None:
22
22
  """
23
23
  resolver = ConfigResolver()
24
24
 
25
+ # Get project config path (project-local only for security)
26
+ project_config_path = resolver.project_path / resolver.PROJECT_CONFIG_SUBPATH
27
+
25
28
  # Check if old config exists
26
- if not resolver.GLOBAL_CONFIG_PATH.exists():
29
+ if not project_config_path.exists():
27
30
  console.print("[yellow]No configuration found to migrate[/yellow]")
28
31
  return
29
32
 
30
33
  # Load old config
31
34
  try:
32
- with open(resolver.GLOBAL_CONFIG_PATH) as f:
35
+ with open(project_config_path) as f:
33
36
  old_config = json.load(f)
34
37
  except Exception as e:
35
38
  console.print(f"[red]Failed to load config: {e}[/red]")
@@ -71,22 +74,23 @@ def migrate_config_command(dry_run: bool = False) -> None:
71
74
  return
72
75
 
73
76
  # Backup old config
74
- backup_path = resolver.GLOBAL_CONFIG_PATH.with_suffix(".json.bak")
77
+ project_config_path = resolver.project_path / resolver.PROJECT_CONFIG_SUBPATH
78
+ backup_path = project_config_path.with_suffix(".json.bak")
75
79
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
76
- backup_path = resolver.GLOBAL_CONFIG_PATH.parent / f"config.{timestamp}.bak"
80
+ backup_path = project_config_path.parent / f"config.{timestamp}.bak"
77
81
 
78
82
  try:
79
- shutil.copy(resolver.GLOBAL_CONFIG_PATH, backup_path)
83
+ shutil.copy(project_config_path, backup_path)
80
84
  console.print(f"[green]✓[/green] Backed up old config to: {backup_path}")
81
85
  except Exception as e:
82
86
  console.print(f"[red]Failed to backup config: {e}[/red]")
83
87
  return
84
88
 
85
- # Save new config
89
+ # Save new config (to project-local config)
86
90
  try:
87
- resolver.save_global_config(new_config)
91
+ resolver.save_project_config(new_config)
88
92
  console.print("[green]✓[/green] Migration complete!")
89
- console.print(f"[dim]New config saved to: {resolver.GLOBAL_CONFIG_PATH}[/dim]")
93
+ console.print(f"[dim]New config saved to: {project_config_path}[/dim]")
90
94
  except Exception as e:
91
95
  console.print(f"[red]Failed to save new config: {e}[/red]")
92
96
  console.print(f"[yellow]Old config backed up at: {backup_path}[/yellow]")
@@ -0,0 +1,123 @@
1
+ """Platform-specific command groups."""
2
+
3
+ import typer
4
+
5
+ # Import platform-specific command modules
6
+ from .linear_commands import app as linear_app
7
+
8
+ # Create main platform command group
9
+ app = typer.Typer(
10
+ name="platform",
11
+ help="Platform-specific commands (Linear, JIRA, GitHub, AITrackdown)",
12
+ )
13
+
14
+ # Register Linear commands
15
+ app.add_typer(linear_app, name="linear")
16
+
17
+ # Create placeholder apps for other platforms
18
+
19
+ # JIRA platform commands (placeholder)
20
+ jira_app = typer.Typer(
21
+ name="jira",
22
+ help="JIRA-specific workspace and project management",
23
+ )
24
+
25
+
26
+ @jira_app.command("projects")
27
+ def jira_list_projects() -> None:
28
+ """List JIRA projects (placeholder - not yet implemented)."""
29
+ from rich.console import Console
30
+
31
+ console = Console()
32
+ console.print("[yellow]JIRA platform commands are not yet implemented.[/yellow]")
33
+ console.print(
34
+ "Use the generic ticket commands for JIRA operations:\n"
35
+ " mcp-ticketer ticket create 'My ticket'\n"
36
+ " mcp-ticketer ticket list"
37
+ )
38
+
39
+
40
+ @jira_app.command("configure")
41
+ def jira_configure() -> None:
42
+ """Configure JIRA adapter (placeholder - not yet implemented)."""
43
+ from rich.console import Console
44
+
45
+ console = Console()
46
+ console.print("[yellow]JIRA platform commands are not yet implemented.[/yellow]")
47
+ console.print("Use 'mcp-ticketer init --adapter jira' to configure JIRA adapter.")
48
+
49
+
50
+ # GitHub platform commands (placeholder)
51
+ github_app = typer.Typer(
52
+ name="github",
53
+ help="GitHub-specific repository and issue management",
54
+ )
55
+
56
+
57
+ @github_app.command("repos")
58
+ def github_list_repos() -> None:
59
+ """List GitHub repositories (placeholder - not yet implemented)."""
60
+ from rich.console import Console
61
+
62
+ console = Console()
63
+ console.print("[yellow]GitHub platform commands are not yet implemented.[/yellow]")
64
+ console.print(
65
+ "Use the generic ticket commands for GitHub operations:\n"
66
+ " mcp-ticketer ticket create 'My issue'\n"
67
+ " mcp-ticketer ticket list"
68
+ )
69
+
70
+
71
+ @github_app.command("configure")
72
+ def github_configure() -> None:
73
+ """Configure GitHub adapter (placeholder - not yet implemented)."""
74
+ from rich.console import Console
75
+
76
+ console = Console()
77
+ console.print("[yellow]GitHub platform commands are not yet implemented.[/yellow]")
78
+ console.print(
79
+ "Use 'mcp-ticketer init --adapter github' to configure GitHub adapter."
80
+ )
81
+
82
+
83
+ # AITrackdown platform commands (placeholder)
84
+ aitrackdown_app = typer.Typer(
85
+ name="aitrackdown",
86
+ help="AITrackdown-specific local file management",
87
+ )
88
+
89
+
90
+ @aitrackdown_app.command("info")
91
+ def aitrackdown_info() -> None:
92
+ """Show AITrackdown storage information (placeholder - not yet implemented)."""
93
+ from rich.console import Console
94
+
95
+ console = Console()
96
+ console.print(
97
+ "[yellow]AITrackdown platform commands are not yet implemented.[/yellow]"
98
+ )
99
+ console.print(
100
+ "Use the generic ticket commands for AITrackdown operations:\n"
101
+ " mcp-ticketer ticket create 'My ticket'\n"
102
+ " mcp-ticketer ticket list"
103
+ )
104
+
105
+
106
+ @aitrackdown_app.command("configure")
107
+ def aitrackdown_configure() -> None:
108
+ """Configure AITrackdown adapter (placeholder - not yet implemented)."""
109
+ from rich.console import Console
110
+
111
+ console = Console()
112
+ console.print(
113
+ "[yellow]AITrackdown platform commands are not yet implemented.[/yellow]"
114
+ )
115
+ console.print(
116
+ "Use 'mcp-ticketer init --adapter aitrackdown' to configure AITrackdown adapter."
117
+ )
118
+
119
+
120
+ # Register all platform command groups
121
+ app.add_typer(jira_app, name="jira")
122
+ app.add_typer(github_app, name="github")
123
+ app.add_typer(aitrackdown_app, name="aitrackdown")