mcp-ticketer 0.1.13__py3-none-any.whl → 0.1.15__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.

@@ -1,6 +1,6 @@
1
1
  """Version information for mcp-ticketer package."""
2
2
 
3
- __version__ = "0.1.13"
3
+ __version__ = "0.1.15"
4
4
  __version_info__ = tuple(int(part) for part in __version__.split("."))
5
5
 
6
6
  # Package metadata
mcp_ticketer/cli/main.py CHANGED
@@ -160,11 +160,22 @@ def get_adapter(override_adapter: Optional[str] = None, override_config: Optiona
160
160
 
161
161
  @app.command()
162
162
  def init(
163
- adapter: AdapterType = typer.Option(
164
- AdapterType.AITRACKDOWN,
163
+ adapter: Optional[str] = typer.Option(
164
+ None,
165
165
  "--adapter",
166
166
  "-a",
167
- help="Adapter type to use"
167
+ help="Adapter type to use (auto-detected from .env if not specified)"
168
+ ),
169
+ project_path: Optional[str] = typer.Option(
170
+ None,
171
+ "--path",
172
+ help="Project path (default: current directory)"
173
+ ),
174
+ global_config: bool = typer.Option(
175
+ False,
176
+ "--global",
177
+ "-g",
178
+ help="Save to global config instead of project-specific"
168
179
  ),
169
180
  base_path: Optional[str] = typer.Option(
170
181
  None,
@@ -213,97 +224,306 @@ def init(
213
224
  help="GitHub Personal Access Token"
214
225
  ),
215
226
  ) -> None:
216
- """Initialize MCP Ticketer configuration."""
227
+ """Initialize mcp-ticketer for the current project.
228
+
229
+ Creates .mcp-ticketer/config.json in the current directory with
230
+ auto-detected or specified adapter configuration.
231
+
232
+ Examples:
233
+ # Auto-detect from .env.local
234
+ mcp-ticketer init
235
+
236
+ # Force specific adapter
237
+ mcp-ticketer init --adapter linear
238
+
239
+ # Initialize for different project
240
+ mcp-ticketer init --path /path/to/project
241
+
242
+ # Save globally (not recommended)
243
+ mcp-ticketer init --global
244
+ """
245
+ from pathlib import Path
246
+ from ..core.project_config import ConfigResolver
247
+ from ..core.env_discovery import discover_config
248
+
249
+ # Determine project path
250
+ proj_path = Path(project_path) if project_path else Path.cwd()
251
+
252
+ # Check if already initialized (unless using --global)
253
+ if not global_config:
254
+ config_path = proj_path / ".mcp-ticketer" / "config.json"
255
+
256
+ if config_path.exists():
257
+ if not typer.confirm(
258
+ f"Configuration already exists at {config_path}. Overwrite?",
259
+ default=False
260
+ ):
261
+ console.print("[yellow]Initialization cancelled.[/yellow]")
262
+ raise typer.Exit(0)
263
+
264
+ # 1. Try auto-discovery if no adapter specified
265
+ discovered = None
266
+ adapter_type = adapter
267
+
268
+ if not adapter_type:
269
+ console.print("[cyan]🔍 Auto-discovering configuration from .env files...[/cyan]")
270
+ discovered = discover_config(proj_path)
271
+
272
+ if discovered and discovered.adapters:
273
+ primary = discovered.get_primary_adapter()
274
+ if primary:
275
+ adapter_type = primary.adapter_type
276
+ console.print(f"[green]✓ Detected {adapter_type} adapter from environment files[/green]")
277
+
278
+ # Show what was discovered
279
+ console.print(f"\n[dim]Configuration found in: {primary.found_in}[/dim]")
280
+ console.print(f"[dim]Confidence: {primary.confidence:.0%}[/dim]")
281
+ else:
282
+ adapter_type = "aitrackdown" # Fallback
283
+ console.print("[yellow]⚠ No credentials found, defaulting to aitrackdown[/yellow]")
284
+ else:
285
+ adapter_type = "aitrackdown" # Fallback
286
+ console.print("[yellow]⚠ No .env files found, defaulting to aitrackdown[/yellow]")
287
+
288
+ # 2. Create configuration based on adapter type
217
289
  config = {
218
- "default_adapter": adapter.value,
290
+ "default_adapter": adapter_type,
219
291
  "adapters": {}
220
292
  }
221
293
 
222
- if adapter == AdapterType.AITRACKDOWN:
294
+ # 3. If discovered and matches adapter_type, use discovered config
295
+ if discovered and adapter_type != "aitrackdown":
296
+ discovered_adapter = discovered.get_adapter_by_type(adapter_type)
297
+ if discovered_adapter:
298
+ config["adapters"][adapter_type] = discovered_adapter.config
299
+
300
+ # 4. Handle manual configuration for specific adapters
301
+ if adapter_type == "aitrackdown":
223
302
  config["adapters"]["aitrackdown"] = {"base_path": base_path or ".aitrackdown"}
224
- elif adapter == AdapterType.LINEAR:
225
- # For Linear, we need team_id and optionally api_key
226
- if not team_id:
227
- console.print("[red]Error:[/red] --team-id is required for Linear adapter")
228
- raise typer.Exit(1)
229
-
230
- config["adapters"]["linear"] = {"team_id": team_id}
231
-
232
- # Check for API key in environment or parameter
233
- linear_api_key = api_key or os.getenv("LINEAR_API_KEY")
234
- if not linear_api_key:
235
- console.print("[yellow]Warning:[/yellow] No Linear API key provided.")
236
- console.print("Set LINEAR_API_KEY environment variable or use --api-key option")
237
- else:
238
- config["adapters"]["linear"]["api_key"] = linear_api_key
239
-
240
- elif adapter == AdapterType.JIRA:
241
- # For JIRA, we need server, email, and API token
242
- server = jira_server or os.getenv("JIRA_SERVER")
243
- email = jira_email or os.getenv("JIRA_EMAIL")
244
- token = api_key or os.getenv("JIRA_API_TOKEN")
245
- project = jira_project or os.getenv("JIRA_PROJECT_KEY")
246
-
247
- if not server:
248
- console.print("[red]Error:[/red] JIRA server URL is required")
249
- console.print("Use --jira-server or set JIRA_SERVER environment variable")
250
- raise typer.Exit(1)
251
-
252
- if not email:
253
- console.print("[red]Error:[/red] JIRA email is required")
254
- console.print("Use --jira-email or set JIRA_EMAIL environment variable")
255
- raise typer.Exit(1)
256
-
257
- if not token:
258
- console.print("[red]Error:[/red] JIRA API token is required")
259
- console.print("Use --api-key or set JIRA_API_TOKEN environment variable")
260
- console.print("[dim]Generate token at: https://id.atlassian.com/manage/api-tokens[/dim]")
261
- raise typer.Exit(1)
262
-
263
- config["adapters"]["jira"] = {
264
- "server": server,
265
- "email": email,
266
- "api_token": token
267
- }
268
303
 
269
- if project:
270
- config["adapters"]["jira"]["project_key"] = project
304
+ elif adapter_type == "linear":
305
+ # If not auto-discovered, build from CLI params
306
+ if adapter_type not in config["adapters"]:
307
+ linear_config = {}
308
+
309
+ # Team ID
310
+ if team_id:
311
+ linear_config["team_id"] = team_id
312
+
313
+ # API Key
314
+ linear_api_key = api_key or os.getenv("LINEAR_API_KEY")
315
+ if linear_api_key:
316
+ linear_config["api_key"] = linear_api_key
317
+ elif not discovered:
318
+ console.print("[yellow]Warning:[/yellow] No Linear API key provided.")
319
+ console.print("Set LINEAR_API_KEY environment variable or use --api-key option")
320
+
321
+ if linear_config:
322
+ config["adapters"]["linear"] = linear_config
323
+
324
+ elif adapter_type == "jira":
325
+ # If not auto-discovered, build from CLI params
326
+ if adapter_type not in config["adapters"]:
327
+ server = jira_server or os.getenv("JIRA_SERVER")
328
+ email = jira_email or os.getenv("JIRA_EMAIL")
329
+ token = api_key or os.getenv("JIRA_API_TOKEN")
330
+ project = jira_project or os.getenv("JIRA_PROJECT_KEY")
331
+
332
+ if not server:
333
+ console.print("[red]Error:[/red] JIRA server URL is required")
334
+ console.print("Use --jira-server or set JIRA_SERVER environment variable")
335
+ raise typer.Exit(1)
336
+
337
+ if not email:
338
+ console.print("[red]Error:[/red] JIRA email is required")
339
+ console.print("Use --jira-email or set JIRA_EMAIL environment variable")
340
+ raise typer.Exit(1)
341
+
342
+ if not token:
343
+ console.print("[red]Error:[/red] JIRA API token is required")
344
+ console.print("Use --api-key or set JIRA_API_TOKEN environment variable")
345
+ console.print("[dim]Generate token at: https://id.atlassian.com/manage/api-tokens[/dim]")
346
+ raise typer.Exit(1)
347
+
348
+ jira_config = {
349
+ "server": server,
350
+ "email": email,
351
+ "api_token": token
352
+ }
353
+
354
+ if project:
355
+ jira_config["project_key"] = project
356
+
357
+ config["adapters"]["jira"] = jira_config
358
+
359
+ elif adapter_type == "github":
360
+ # If not auto-discovered, build from CLI params
361
+ if adapter_type not in config["adapters"]:
362
+ owner = github_owner or os.getenv("GITHUB_OWNER")
363
+ repo = github_repo or os.getenv("GITHUB_REPO")
364
+ token = github_token or os.getenv("GITHUB_TOKEN")
365
+
366
+ if not owner:
367
+ console.print("[red]Error:[/red] GitHub repository owner is required")
368
+ console.print("Use --github-owner or set GITHUB_OWNER environment variable")
369
+ raise typer.Exit(1)
370
+
371
+ if not repo:
372
+ console.print("[red]Error:[/red] GitHub repository name is required")
373
+ console.print("Use --github-repo or set GITHUB_REPO environment variable")
374
+ raise typer.Exit(1)
375
+
376
+ if not token:
377
+ console.print("[red]Error:[/red] GitHub Personal Access Token is required")
378
+ console.print("Use --github-token or set GITHUB_TOKEN environment variable")
379
+ console.print("[dim]Create token at: https://github.com/settings/tokens/new[/dim]")
380
+ console.print("[dim]Required scopes: repo (for private repos) or public_repo (for public repos)[/dim]")
381
+ raise typer.Exit(1)
382
+
383
+ config["adapters"]["github"] = {
384
+ "owner": owner,
385
+ "repo": repo,
386
+ "token": token
387
+ }
388
+
389
+ # 5. Save to appropriate location
390
+ if global_config:
391
+ # Save to ~/.mcp-ticketer/config.json
392
+ resolver = ConfigResolver(project_path=proj_path)
393
+ config_file_path = resolver.GLOBAL_CONFIG_PATH
394
+ config_file_path.parent.mkdir(parents=True, exist_ok=True)
395
+
396
+ with open(config_file_path, 'w') as f:
397
+ json.dump(config, f, indent=2)
398
+
399
+ console.print(f"[green]✓ Initialized with {adapter_type} adapter[/green]")
400
+ console.print(f"[dim]Global configuration saved to {config_file_path}[/dim]")
401
+ else:
402
+ # Save to ./.mcp-ticketer/config.json (PROJECT-SPECIFIC)
403
+ config_file_path = proj_path / ".mcp-ticketer" / "config.json"
404
+ config_file_path.parent.mkdir(parents=True, exist_ok=True)
405
+
406
+ with open(config_file_path, 'w') as f:
407
+ json.dump(config, f, indent=2)
408
+
409
+ console.print(f"[green]✓ Initialized with {adapter_type} adapter[/green]")
410
+ console.print(f"[dim]Project configuration saved to {config_file_path}[/dim]")
411
+
412
+ # Add .mcp-ticketer to .gitignore if not already there
413
+ gitignore_path = proj_path / ".gitignore"
414
+ if gitignore_path.exists():
415
+ gitignore_content = gitignore_path.read_text()
416
+ if ".mcp-ticketer" not in gitignore_content:
417
+ with open(gitignore_path, 'a') as f:
418
+ f.write("\n# MCP Ticketer\n.mcp-ticketer/\n")
419
+ console.print("[dim]✓ Added .mcp-ticketer/ to .gitignore[/dim]")
271
420
  else:
272
- console.print("[yellow]Warning:[/yellow] No default project key specified")
273
- console.print("You may need to specify project key for some operations")
274
-
275
- elif adapter == AdapterType.GITHUB:
276
- # For GitHub, we need owner, repo, and token
277
- owner = github_owner or os.getenv("GITHUB_OWNER")
278
- repo = github_repo or os.getenv("GITHUB_REPO")
279
- token = github_token or os.getenv("GITHUB_TOKEN")
280
-
281
- if not owner:
282
- console.print("[red]Error:[/red] GitHub repository owner is required")
283
- console.print("Use --github-owner or set GITHUB_OWNER environment variable")
284
- raise typer.Exit(1)
285
-
286
- if not repo:
287
- console.print("[red]Error:[/red] GitHub repository name is required")
288
- console.print("Use --github-repo or set GITHUB_REPO environment variable")
289
- raise typer.Exit(1)
290
-
291
- if not token:
292
- console.print("[red]Error:[/red] GitHub Personal Access Token is required")
293
- console.print("Use --github-token or set GITHUB_TOKEN environment variable")
294
- console.print("[dim]Create token at: https://github.com/settings/tokens/new[/dim]")
295
- console.print("[dim]Required scopes: repo (for private repos) or public_repo (for public repos)[/dim]")
296
- raise typer.Exit(1)
297
-
298
- config["adapters"]["github"] = {
299
- "owner": owner,
300
- "repo": repo,
301
- "token": token
302
- }
303
-
304
- save_config(config)
305
- console.print(f"[green]✓[/green] Initialized with {adapter.value} adapter")
306
- console.print(f"[dim]Configuration saved to {CONFIG_FILE}[/dim]")
421
+ # Create .gitignore if it doesn't exist
422
+ with open(gitignore_path, 'w') as f:
423
+ f.write("# MCP Ticketer\n.mcp-ticketer/\n")
424
+ console.print("[dim]✓ Created .gitignore with .mcp-ticketer/[/dim]")
425
+
426
+
427
+ @app.command()
428
+ def install(
429
+ adapter: Optional[str] = typer.Option(
430
+ None,
431
+ "--adapter",
432
+ "-a",
433
+ help="Adapter type to use (auto-detected from .env if not specified)"
434
+ ),
435
+ project_path: Optional[str] = typer.Option(
436
+ None,
437
+ "--path",
438
+ help="Project path (default: current directory)"
439
+ ),
440
+ global_config: bool = typer.Option(
441
+ False,
442
+ "--global",
443
+ "-g",
444
+ help="Save to global config instead of project-specific"
445
+ ),
446
+ base_path: Optional[str] = typer.Option(
447
+ None,
448
+ "--base-path",
449
+ "-p",
450
+ help="Base path for ticket storage (AITrackdown only)"
451
+ ),
452
+ api_key: Optional[str] = typer.Option(
453
+ None,
454
+ "--api-key",
455
+ help="API key for Linear or API token for JIRA"
456
+ ),
457
+ team_id: Optional[str] = typer.Option(
458
+ None,
459
+ "--team-id",
460
+ help="Linear team ID (required for Linear adapter)"
461
+ ),
462
+ jira_server: Optional[str] = typer.Option(
463
+ None,
464
+ "--jira-server",
465
+ help="JIRA server URL (e.g., https://company.atlassian.net)"
466
+ ),
467
+ jira_email: Optional[str] = typer.Option(
468
+ None,
469
+ "--jira-email",
470
+ help="JIRA user email for authentication"
471
+ ),
472
+ jira_project: Optional[str] = typer.Option(
473
+ None,
474
+ "--jira-project",
475
+ help="Default JIRA project key"
476
+ ),
477
+ github_owner: Optional[str] = typer.Option(
478
+ None,
479
+ "--github-owner",
480
+ help="GitHub repository owner"
481
+ ),
482
+ github_repo: Optional[str] = typer.Option(
483
+ None,
484
+ "--github-repo",
485
+ help="GitHub repository name"
486
+ ),
487
+ github_token: Optional[str] = typer.Option(
488
+ None,
489
+ "--github-token",
490
+ help="GitHub Personal Access Token"
491
+ ),
492
+ ) -> None:
493
+ """Initialize mcp-ticketer for the current project (alias for init).
494
+
495
+ This command is synonymous with 'init' and provides the same functionality.
496
+ Creates .mcp-ticketer/config.json in the current directory with
497
+ auto-detected or specified adapter configuration.
498
+
499
+ Examples:
500
+ # Auto-detect from .env.local
501
+ mcp-ticketer install
502
+
503
+ # Force specific adapter
504
+ mcp-ticketer install --adapter linear
505
+
506
+ # Initialize for different project
507
+ mcp-ticketer install --path /path/to/project
508
+
509
+ # Save globally (not recommended)
510
+ mcp-ticketer install --global
511
+ """
512
+ # Call init with all parameters
513
+ init(
514
+ adapter=adapter,
515
+ project_path=project_path,
516
+ global_config=global_config,
517
+ base_path=base_path,
518
+ api_key=api_key,
519
+ team_id=team_id,
520
+ jira_server=jira_server,
521
+ jira_email=jira_email,
522
+ jira_project=jira_project,
523
+ github_owner=github_owner,
524
+ github_repo=github_repo,
525
+ github_token=github_token,
526
+ )
307
527
 
308
528
 
309
529
  @app.command("set")
@@ -915,7 +1135,7 @@ def check(
915
1135
 
916
1136
 
917
1137
  @app.command()
918
- def mcp(
1138
+ def serve(
919
1139
  adapter: Optional[AdapterType] = typer.Option(
920
1140
  None,
921
1141
  "--adapter",
@@ -928,7 +1148,11 @@ def mcp(
928
1148
  help="Base path for AITrackdown adapter"
929
1149
  ),
930
1150
  ):
931
- """Start MCP server for JSON-RPC communication over stdio."""
1151
+ """Start MCP server for JSON-RPC communication over stdio.
1152
+
1153
+ This command is used by Claude Code/Desktop when connecting to the MCP server.
1154
+ You typically don't need to run this manually - use 'mcp-ticketer mcp' to configure.
1155
+ """
932
1156
  from ..mcp.server import MCPTicketServer
933
1157
 
934
1158
  # Load configuration
@@ -974,6 +1198,38 @@ def mcp(
974
1198
  sys.exit(1)
975
1199
 
976
1200
 
1201
+ @app.command()
1202
+ def mcp(
1203
+ global_config: bool = typer.Option(
1204
+ False,
1205
+ "--global",
1206
+ "-g",
1207
+ help="Configure Claude Desktop instead of project-level"
1208
+ ),
1209
+ force: bool = typer.Option(
1210
+ False,
1211
+ "--force",
1212
+ "-f",
1213
+ help="Overwrite existing configuration"
1214
+ ),
1215
+ ):
1216
+ """Configure Claude Code to use mcp-ticketer MCP server.
1217
+
1218
+ Reads configuration from .mcp-ticketer/config.json and updates
1219
+ Claude Code's MCP settings accordingly.
1220
+
1221
+ By default, configures project-level (.mcp/config.json).
1222
+ Use --global to configure Claude Desktop instead.
1223
+ """
1224
+ from ..cli.mcp_configure import configure_claude_mcp
1225
+
1226
+ try:
1227
+ configure_claude_mcp(global_config=global_config, force=force)
1228
+ except Exception as e:
1229
+ console.print(f"[red]✗ Configuration failed:[/red] {e}")
1230
+ raise typer.Exit(1)
1231
+
1232
+
977
1233
  def main():
978
1234
  """Main entry point."""
979
1235
  app()
@@ -0,0 +1,285 @@
1
+ """MCP configuration for Claude Code integration."""
2
+
3
+ import json
4
+ import os
5
+ import shutil
6
+ import sys
7
+ from pathlib import Path
8
+ from typing import Optional
9
+
10
+ from rich.console import Console
11
+
12
+ console = Console()
13
+
14
+
15
+ def find_mcp_ticketer_binary() -> str:
16
+ """Find the mcp-ticketer binary path.
17
+
18
+ Returns:
19
+ Path to mcp-ticketer binary
20
+
21
+ Raises:
22
+ FileNotFoundError: If binary not found
23
+ """
24
+ # Check if running from development environment
25
+ import mcp_ticketer
26
+ package_path = Path(mcp_ticketer.__file__).parent.parent.parent
27
+
28
+ # Check for virtual environment bin
29
+ possible_paths = [
30
+ # Development paths
31
+ package_path / "venv" / "bin" / "mcp-ticketer",
32
+ package_path / ".venv" / "bin" / "mcp-ticketer",
33
+ package_path / "test_venv" / "bin" / "mcp-ticketer",
34
+ # System installation
35
+ Path.home() / ".local" / "bin" / "mcp-ticketer",
36
+ # pipx installation
37
+ Path.home() / ".local" / "pipx" / "venvs" / "mcp-ticketer" / "bin" / "mcp-ticketer",
38
+ ]
39
+
40
+ # Check PATH
41
+ which_result = shutil.which("mcp-ticketer")
42
+ if which_result:
43
+ return which_result
44
+
45
+ # Check possible paths
46
+ for path in possible_paths:
47
+ if path.exists():
48
+ return str(path.resolve())
49
+
50
+ raise FileNotFoundError(
51
+ "Could not find mcp-ticketer binary. Please ensure mcp-ticketer is installed.\n"
52
+ "Install with: pip install mcp-ticketer"
53
+ )
54
+
55
+
56
+ def load_project_config() -> dict:
57
+ """Load mcp-ticketer project configuration.
58
+
59
+ Returns:
60
+ Project configuration dict
61
+
62
+ Raises:
63
+ FileNotFoundError: If config not found
64
+ ValueError: If config is invalid
65
+ """
66
+ # Check for project-specific config first
67
+ project_config_path = Path.cwd() / ".mcp-ticketer" / "config.json"
68
+
69
+ if not project_config_path.exists():
70
+ # Check global config
71
+ global_config_path = Path.home() / ".mcp-ticketer" / "config.json"
72
+ if global_config_path.exists():
73
+ project_config_path = global_config_path
74
+ else:
75
+ raise FileNotFoundError(
76
+ "No mcp-ticketer configuration found.\n"
77
+ "Run 'mcp-ticketer init' to create configuration."
78
+ )
79
+
80
+ with open(project_config_path, "r") as f:
81
+ config = json.load(f)
82
+
83
+ # Validate config
84
+ if "default_adapter" not in config:
85
+ raise ValueError("Invalid config: missing 'default_adapter'")
86
+
87
+ return config
88
+
89
+
90
+ def find_claude_mcp_config(global_config: bool = False) -> Path:
91
+ """Find or create Claude Code MCP configuration file.
92
+
93
+ Args:
94
+ global_config: If True, use Claude Desktop config instead of project-level
95
+
96
+ Returns:
97
+ Path to MCP configuration file
98
+ """
99
+ if global_config:
100
+ # Claude Desktop configuration
101
+ if sys.platform == "darwin": # macOS
102
+ config_path = Path.home() / "Library" / "Application Support" / "Claude" / "claude_desktop_config.json"
103
+ elif sys.platform == "win32": # Windows
104
+ config_path = Path(os.environ.get("APPDATA", "")) / "Claude" / "claude_desktop_config.json"
105
+ else: # Linux
106
+ config_path = Path.home() / ".config" / "Claude" / "claude_desktop_config.json"
107
+ else:
108
+ # Project-level configuration
109
+ config_path = Path.cwd() / ".mcp" / "config.json"
110
+
111
+ return config_path
112
+
113
+
114
+ def load_claude_mcp_config(config_path: Path) -> dict:
115
+ """Load existing Claude MCP configuration or return empty structure.
116
+
117
+ Args:
118
+ config_path: Path to MCP config file
119
+
120
+ Returns:
121
+ MCP configuration dict
122
+ """
123
+ if config_path.exists():
124
+ with open(config_path, "r") as f:
125
+ return json.load(f)
126
+
127
+ # Return empty structure
128
+ return {"mcpServers": {}}
129
+
130
+
131
+ def save_claude_mcp_config(config_path: Path, config: dict) -> None:
132
+ """Save Claude MCP configuration to file.
133
+
134
+ Args:
135
+ config_path: Path to MCP config file
136
+ config: Configuration to save
137
+ """
138
+ # Ensure directory exists
139
+ config_path.parent.mkdir(parents=True, exist_ok=True)
140
+
141
+ # Write with formatting
142
+ with open(config_path, "w") as f:
143
+ json.dump(config, f, indent=2)
144
+
145
+
146
+ def create_mcp_server_config(
147
+ binary_path: str,
148
+ project_config: dict,
149
+ cwd: Optional[str] = None
150
+ ) -> dict:
151
+ """Create MCP server configuration for mcp-ticketer.
152
+
153
+ Args:
154
+ binary_path: Path to mcp-ticketer binary
155
+ project_config: Project configuration from .mcp-ticketer/config.json
156
+ cwd: Working directory for server (optional)
157
+
158
+ Returns:
159
+ MCP server configuration dict
160
+ """
161
+ config = {
162
+ "command": binary_path,
163
+ "args": ["serve"], # Use 'serve' command to start MCP server
164
+ }
165
+
166
+ # Add working directory if provided
167
+ if cwd:
168
+ config["cwd"] = cwd
169
+
170
+ # Add environment variables based on adapter
171
+ adapter = project_config.get("default_adapter", "aitrackdown")
172
+ adapters_config = project_config.get("adapters", {})
173
+ adapter_config = adapters_config.get(adapter, {})
174
+
175
+ env_vars = {}
176
+
177
+ # Add adapter-specific environment variables
178
+ if adapter == "linear" and "api_key" in adapter_config:
179
+ env_vars["LINEAR_API_KEY"] = adapter_config["api_key"]
180
+ elif adapter == "github" and "token" in adapter_config:
181
+ env_vars["GITHUB_TOKEN"] = adapter_config["token"]
182
+ elif adapter == "jira":
183
+ if "api_token" in adapter_config:
184
+ env_vars["JIRA_API_TOKEN"] = adapter_config["api_token"]
185
+ if "email" in adapter_config:
186
+ env_vars["JIRA_EMAIL"] = adapter_config["email"]
187
+
188
+ if env_vars:
189
+ config["env"] = env_vars
190
+
191
+ return config
192
+
193
+
194
+ def configure_claude_mcp(global_config: bool = False, force: bool = False) -> None:
195
+ """Configure Claude Code to use mcp-ticketer.
196
+
197
+ Args:
198
+ global_config: Configure Claude Desktop instead of project-level
199
+ force: Overwrite existing configuration
200
+
201
+ Raises:
202
+ FileNotFoundError: If binary or project config not found
203
+ ValueError: If configuration is invalid
204
+ """
205
+ # Step 1: Find mcp-ticketer binary
206
+ console.print("[cyan]🔍 Finding mcp-ticketer binary...[/cyan]")
207
+ try:
208
+ binary_path = find_mcp_ticketer_binary()
209
+ console.print(f"[green]✓[/green] Found: {binary_path}")
210
+ except FileNotFoundError as e:
211
+ console.print(f"[red]✗[/red] {e}")
212
+ raise
213
+
214
+ # Step 2: Load project configuration
215
+ console.print("\n[cyan]📖 Reading project configuration...[/cyan]")
216
+ try:
217
+ project_config = load_project_config()
218
+ adapter = project_config.get("default_adapter", "aitrackdown")
219
+ console.print(f"[green]✓[/green] Adapter: {adapter}")
220
+ except (FileNotFoundError, ValueError) as e:
221
+ console.print(f"[red]✗[/red] {e}")
222
+ raise
223
+
224
+ # Step 3: Find Claude MCP config location
225
+ config_type = "Claude Desktop" if global_config else "project-level"
226
+ console.print(f"\n[cyan]🔧 Configuring {config_type} MCP...[/cyan]")
227
+
228
+ mcp_config_path = find_claude_mcp_config(global_config)
229
+ console.print(f"[dim]Config location: {mcp_config_path}[/dim]")
230
+
231
+ # Step 4: Load existing MCP configuration
232
+ mcp_config = load_claude_mcp_config(mcp_config_path)
233
+
234
+ # Step 5: Check if mcp-ticketer already configured
235
+ if "mcp-ticketer" in mcp_config.get("mcpServers", {}):
236
+ if not force:
237
+ console.print("[yellow]⚠ mcp-ticketer is already configured[/yellow]")
238
+ console.print("[dim]Use --force to overwrite existing configuration[/dim]")
239
+ return
240
+ else:
241
+ console.print("[yellow]⚠ Overwriting existing configuration[/yellow]")
242
+
243
+ # Step 6: Create mcp-ticketer server config
244
+ cwd = str(Path.cwd()) if not global_config else None
245
+ server_config = create_mcp_server_config(
246
+ binary_path=binary_path,
247
+ project_config=project_config,
248
+ cwd=cwd
249
+ )
250
+
251
+ # Step 7: Update MCP configuration
252
+ if "mcpServers" not in mcp_config:
253
+ mcp_config["mcpServers"] = {}
254
+
255
+ mcp_config["mcpServers"]["mcp-ticketer"] = server_config
256
+
257
+ # Step 8: Save configuration
258
+ try:
259
+ save_claude_mcp_config(mcp_config_path, mcp_config)
260
+ console.print(f"\n[green]✓ Successfully configured mcp-ticketer[/green]")
261
+ console.print(f"[dim]Configuration saved to: {mcp_config_path}[/dim]")
262
+
263
+ # Print configuration details
264
+ console.print("\n[bold]Configuration Details:[/bold]")
265
+ console.print(f" Server name: mcp-ticketer")
266
+ console.print(f" Adapter: {adapter}")
267
+ console.print(f" Binary: {binary_path}")
268
+ if cwd:
269
+ console.print(f" Working directory: {cwd}")
270
+ if "env" in server_config:
271
+ console.print(f" Environment variables: {list(server_config['env'].keys())}")
272
+
273
+ # Next steps
274
+ console.print("\n[bold cyan]Next Steps:[/bold cyan]")
275
+ if global_config:
276
+ console.print("1. Restart Claude Desktop")
277
+ console.print("2. Open a conversation")
278
+ else:
279
+ console.print("1. Restart Claude Code")
280
+ console.print("2. Open this project in Claude Code")
281
+ console.print("3. mcp-ticketer tools will be available in the MCP menu")
282
+
283
+ except Exception as e:
284
+ console.print(f"\n[red]✗ Failed to save configuration:[/red] {e}")
285
+ raise
@@ -414,7 +414,7 @@ class ConfigResolver:
414
414
  ) -> Dict[str, Any]:
415
415
  """Resolve adapter configuration with hierarchical precedence.
416
416
 
417
- Precedence (highest to lowest):
417
+ Resolution order (highest to lowest priority):
418
418
  1. CLI overrides
419
419
  2. Environment variables (os.getenv)
420
420
  3. Project-specific config (.mcp-ticketer/config.json)
@@ -432,7 +432,7 @@ class ConfigResolver:
432
432
  global_config = self.load_global_config()
433
433
  project_config = self.load_project_config()
434
434
 
435
- # Determine which adapter to use
435
+ # Determine which adapter to use (check project config first)
436
436
  if adapter_name:
437
437
  target_adapter = adapter_name
438
438
  elif project_config and project_config.default_adapter:
@@ -452,7 +452,7 @@ class ConfigResolver:
452
452
  # Start with empty config
453
453
  resolved_config = {"adapter": target_adapter}
454
454
 
455
- # 1. Apply global adapter config
455
+ # 1. Apply global adapter config (LOWEST PRIORITY)
456
456
  if target_adapter in global_config.adapters:
457
457
  global_adapter_config = global_config.adapters[target_adapter].to_dict()
458
458
  resolved_config.update(global_adapter_config)
@@ -473,7 +473,7 @@ class ConfigResolver:
473
473
  f"Applied auto-discovered config from {discovered_adapter.found_in}"
474
474
  )
475
475
 
476
- # 3. Apply project-specific config if exists
476
+ # 3. Apply project-specific config (HIGHER PRIORITY - overrides global and .env)
477
477
  if project_config:
478
478
  # Check if this project has specific adapter config
479
479
  project_path_str = str(self.project_path)
@@ -486,11 +486,11 @@ class ConfigResolver:
486
486
  proj_global_adapter_config = project_config.adapters[target_adapter].to_dict()
487
487
  resolved_config.update(proj_global_adapter_config)
488
488
 
489
- # 4. Apply environment variable overrides (os.getenv)
489
+ # 4. Apply environment variable overrides (os.getenv - HIGHER PRIORITY)
490
490
  env_overrides = self._get_env_overrides(target_adapter)
491
491
  resolved_config.update(env_overrides)
492
492
 
493
- # 5. Apply CLI overrides
493
+ # 5. Apply CLI overrides (HIGHEST PRIORITY)
494
494
  if cli_overrides:
495
495
  resolved_config.update(cli_overrides)
496
496
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcp-ticketer
3
- Version: 0.1.13
3
+ Version: 0.1.15
4
4
  Summary: Universal ticket management interface for AI agents with MCP support
5
5
  Author-email: MCP Ticketer Team <support@mcp-ticketer.io>
6
6
  Maintainer-email: MCP Ticketer Team <support@mcp-ticketer.io>
@@ -1,5 +1,5 @@
1
1
  mcp_ticketer/__init__.py,sha256=ayPQdFr6msypD06_G96a1H0bdFCT1m1wDtv8MZBpY4I,496
2
- mcp_ticketer/__version__.py,sha256=_43DgubrWUqhaLrhYbn9KefWfUiLWjH_hMHmCrgw1Tc,1115
2
+ mcp_ticketer/__version__.py,sha256=OaT1sLIF9tJvQbLP2vx2vOgiGiMS2mbt4EzloZeALPo,1115
3
3
  mcp_ticketer/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  mcp_ticketer/adapters/__init__.py,sha256=K_1egvhHb5F_7yFceUx2YzPGEoc7vX-q8dMVaS4K6gw,356
5
5
  mcp_ticketer/adapters/aitrackdown.py,sha256=gqS_N6VGLoG5itUu17ANG5SefaAITYoW-t2xL9SrY-Y,15372
@@ -12,7 +12,8 @@ mcp_ticketer/cache/memory.py,sha256=gTzv-xF7qGfiYVUjG7lnzo0ZcqgXQajMl4NAYUcaytg,
12
12
  mcp_ticketer/cli/__init__.py,sha256=YeljyLtv906TqkvRuEPhmKO-Uk0CberQ9I6kx1tx2UA,88
13
13
  mcp_ticketer/cli/configure.py,sha256=etFutvc0QpaVDMOsZiiN7wKuaT98Od1Tj9W6lsEWw5A,16351
14
14
  mcp_ticketer/cli/discover.py,sha256=putWrGcctUH8K0fOMtr9MZA9VnWoXzbtoe7mXSkDxhg,13156
15
- mcp_ticketer/cli/main.py,sha256=3NAHRr5Q4nCExbyQ6YIiUxn1nyqi_e4TBgR8OuMhHqk,30461
15
+ mcp_ticketer/cli/main.py,sha256=HxvlZb4ZGxWoqOLwp2eIoS9Ie7C8LF-gShdnwolqVrE,39135
16
+ mcp_ticketer/cli/mcp_configure.py,sha256=1WbBdF25OvxfAGcjxtYa9xmBOGEPQu-wh_nkefmWjMQ,9352
16
17
  mcp_ticketer/cli/migrate_config.py,sha256=iZIstnlr9vkhiW_MlnSyJOkMi4KHQqrZ6Hz1ECD_VUk,6045
17
18
  mcp_ticketer/cli/queue_commands.py,sha256=f3pEHKZ43dBHEIoCBvdfvjfMB9_WJltps9ATwTzorY0,8160
18
19
  mcp_ticketer/cli/utils.py,sha256=NxsS91vKA8xZnDXKU2y0Gcyc4I_ctRyJj-wT7Xd1Q_Q,18589
@@ -23,7 +24,7 @@ mcp_ticketer/core/env_discovery.py,sha256=SPoyq_y5j-3gJG5gYNVjCIIrbdzimOdDbTYySm
23
24
  mcp_ticketer/core/http_client.py,sha256=RM9CEMNcuRb-FxhAijmM_FeBMgxgh1OII9HIPBdJue0,13855
24
25
  mcp_ticketer/core/mappers.py,sha256=8I4jcqDqoQEdWlteDMpVeVF3Wo0fDCkmFPRr8oNv8gA,16933
25
26
  mcp_ticketer/core/models.py,sha256=GhuTitY6t_QlqfEUvWT6Q2zvY7qAtx_SQyCMMn8iYkk,6473
26
- mcp_ticketer/core/project_config.py,sha256=UqJpxAunmCvR9R_ev9ZrsLvG4engQe1OFQBEmcRx9cs,22254
27
+ mcp_ticketer/core/project_config.py,sha256=VVSeCwuESuemL-iC4fqbPrJxR4i5k5fhUpujnY7MCZA,22389
27
28
  mcp_ticketer/core/registry.py,sha256=fwje0fnjp0YKPZ0SrVWk82SMNLs7CD0JlHQmx7SigNo,3537
28
29
  mcp_ticketer/mcp/__init__.py,sha256=Bvzof9vBu6VwcXcIZK8RgKv6ycRV9tDlO-9TUmd8zqQ,122
29
30
  mcp_ticketer/mcp/server.py,sha256=TDuU8ChZC2QYSRo0uGHkVReblTf--hriOjxo-pSAF_Y,34068
@@ -33,9 +34,9 @@ mcp_ticketer/queue/manager.py,sha256=79AH9oUxdBXH3lmJ3kIlFf2GQkWHL6XB6u5JqVWPq60
33
34
  mcp_ticketer/queue/queue.py,sha256=z4aivQCtsH5_OUr2OfXSfnFKzugTahNnwHw0LS3ZhZc,11549
34
35
  mcp_ticketer/queue/run_worker.py,sha256=HFoykfDpOoz8OUxWbQ2Fka_UlGrYwjPVZ-DEimGFH9o,802
35
36
  mcp_ticketer/queue/worker.py,sha256=cVjHR_kfnGKAkiUg0HuXCnbKeKNBBEuj0XZHgIuIn4k,14017
36
- mcp_ticketer-0.1.13.dist-info/licenses/LICENSE,sha256=KOVrunjtILSzY-2N8Lqa3-Q8dMaZIG4LrlLTr9UqL08,1073
37
- mcp_ticketer-0.1.13.dist-info/METADATA,sha256=29lPCGuZI3U_pdIoRYH3C5h4Xn3DaZo-AfKDXQX7IYs,11211
38
- mcp_ticketer-0.1.13.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
39
- mcp_ticketer-0.1.13.dist-info/entry_points.txt,sha256=o1IxVhnHnBNG7FZzbFq-Whcs1Djbofs0qMjiUYBLx2s,60
40
- mcp_ticketer-0.1.13.dist-info/top_level.txt,sha256=WnAG4SOT1Vm9tIwl70AbGG_nA217YyV3aWFhxLH2rxw,13
41
- mcp_ticketer-0.1.13.dist-info/RECORD,,
37
+ mcp_ticketer-0.1.15.dist-info/licenses/LICENSE,sha256=KOVrunjtILSzY-2N8Lqa3-Q8dMaZIG4LrlLTr9UqL08,1073
38
+ mcp_ticketer-0.1.15.dist-info/METADATA,sha256=VYRxHsh-APXWl3wdmdNSfiniYI84jQwFiVaZ9Y5oOBA,11211
39
+ mcp_ticketer-0.1.15.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
40
+ mcp_ticketer-0.1.15.dist-info/entry_points.txt,sha256=o1IxVhnHnBNG7FZzbFq-Whcs1Djbofs0qMjiUYBLx2s,60
41
+ mcp_ticketer-0.1.15.dist-info/top_level.txt,sha256=WnAG4SOT1Vm9tIwl70AbGG_nA217YyV3aWFhxLH2rxw,13
42
+ mcp_ticketer-0.1.15.dist-info/RECORD,,