mcp-ticketer 0.2.0__py3-none-any.whl → 0.3.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.
- mcp_ticketer/__version__.py +1 -1
- mcp_ticketer/cli/adapter_diagnostics.py +384 -0
- mcp_ticketer/cli/main.py +327 -67
- mcp_ticketer/mcp/server.py +172 -37
- {mcp_ticketer-0.2.0.dist-info → mcp_ticketer-0.3.0.dist-info}/METADATA +1 -1
- {mcp_ticketer-0.2.0.dist-info → mcp_ticketer-0.3.0.dist-info}/RECORD +10 -9
- {mcp_ticketer-0.2.0.dist-info → mcp_ticketer-0.3.0.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.2.0.dist-info → mcp_ticketer-0.3.0.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.2.0.dist-info → mcp_ticketer-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.2.0.dist-info → mcp_ticketer-0.3.0.dist-info}/top_level.txt +0 -0
mcp_ticketer/__version__.py
CHANGED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
"""Adapter diagnostics and configuration validation."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
7
|
+
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
from rich.text import Text
|
|
12
|
+
|
|
13
|
+
from ..core import AdapterRegistry
|
|
14
|
+
from ..core.env_discovery import discover_config
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def diagnose_adapter_configuration(console: Console) -> None:
|
|
18
|
+
"""Diagnose adapter configuration and provide recommendations.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
console: Rich console for output
|
|
22
|
+
"""
|
|
23
|
+
console.print("\n[bold blue]🔍 MCP Ticketer Adapter Configuration Diagnostics[/bold blue]\n")
|
|
24
|
+
|
|
25
|
+
# 1. Check .env files
|
|
26
|
+
_check_env_files(console)
|
|
27
|
+
|
|
28
|
+
# 2. Check configuration files
|
|
29
|
+
_check_configuration_files(console)
|
|
30
|
+
|
|
31
|
+
# 3. Check adapter discovery
|
|
32
|
+
_check_adapter_discovery(console)
|
|
33
|
+
|
|
34
|
+
# 4. Test adapter instantiation
|
|
35
|
+
_test_adapter_instantiation(console)
|
|
36
|
+
|
|
37
|
+
# 5. Provide recommendations
|
|
38
|
+
_provide_recommendations(console)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _check_env_files(console: Console) -> None:
|
|
42
|
+
"""Check .env files for configuration."""
|
|
43
|
+
console.print("[bold]1. .env File Configuration[/bold]")
|
|
44
|
+
|
|
45
|
+
# Load .env files
|
|
46
|
+
from ..mcp.server import _load_env_configuration
|
|
47
|
+
env_config = _load_env_configuration()
|
|
48
|
+
|
|
49
|
+
# Check for .env files
|
|
50
|
+
env_files = [".env.local", ".env"]
|
|
51
|
+
|
|
52
|
+
table = Table(show_header=True, header_style="bold magenta")
|
|
53
|
+
table.add_column("File", style="cyan")
|
|
54
|
+
table.add_column("Status", style="green")
|
|
55
|
+
table.add_column("Variables Found", style="yellow")
|
|
56
|
+
|
|
57
|
+
for env_file in env_files:
|
|
58
|
+
env_path = Path.cwd() / env_file
|
|
59
|
+
if env_path.exists():
|
|
60
|
+
try:
|
|
61
|
+
# Count variables in file
|
|
62
|
+
var_count = 0
|
|
63
|
+
with open(env_path, 'r') as f:
|
|
64
|
+
for line in f:
|
|
65
|
+
line = line.strip()
|
|
66
|
+
if line and not line.startswith('#') and '=' in line:
|
|
67
|
+
var_count += 1
|
|
68
|
+
|
|
69
|
+
status = "✅ Found"
|
|
70
|
+
variables = f"{var_count} variables"
|
|
71
|
+
except Exception:
|
|
72
|
+
status = "⚠️ Error reading"
|
|
73
|
+
variables = "Unknown"
|
|
74
|
+
else:
|
|
75
|
+
status = "❌ Missing"
|
|
76
|
+
variables = "N/A"
|
|
77
|
+
|
|
78
|
+
table.add_row(env_file, status, variables)
|
|
79
|
+
|
|
80
|
+
console.print(table)
|
|
81
|
+
|
|
82
|
+
# Show discovered configuration
|
|
83
|
+
if env_config:
|
|
84
|
+
console.print(f"\n[green]✅ Discovered adapter: {env_config['adapter_type']}[/green]")
|
|
85
|
+
config_keys = list(env_config['adapter_config'].keys())
|
|
86
|
+
console.print(f"[dim]Configuration keys: {config_keys}[/dim]")
|
|
87
|
+
else:
|
|
88
|
+
console.print("\n[yellow]⚠️ No adapter configuration found in .env files[/yellow]")
|
|
89
|
+
|
|
90
|
+
console.print()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _check_configuration_files(console: Console) -> None:
|
|
94
|
+
"""Check configuration files."""
|
|
95
|
+
console.print("[bold]2. Configuration Files[/bold]")
|
|
96
|
+
|
|
97
|
+
config_files = [
|
|
98
|
+
(".env.local", "Local environment file (highest priority)"),
|
|
99
|
+
(".env", "Environment file"),
|
|
100
|
+
(".mcp-ticketer/config.json", "Project configuration"),
|
|
101
|
+
(str(Path.home() / ".mcp-ticketer" / "config.json"), "Global configuration"),
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
table = Table(show_header=True, header_style="bold magenta")
|
|
105
|
+
table.add_column("File", style="cyan")
|
|
106
|
+
table.add_column("Description", style="white")
|
|
107
|
+
table.add_column("Status", style="green")
|
|
108
|
+
table.add_column("Size", style="yellow")
|
|
109
|
+
|
|
110
|
+
for file_path, description in config_files:
|
|
111
|
+
path = Path(file_path)
|
|
112
|
+
if path.exists():
|
|
113
|
+
try:
|
|
114
|
+
size = path.stat().st_size
|
|
115
|
+
status = "✅ Found"
|
|
116
|
+
size_str = f"{size} bytes"
|
|
117
|
+
except Exception:
|
|
118
|
+
status = "⚠️ Error"
|
|
119
|
+
size_str = "Unknown"
|
|
120
|
+
else:
|
|
121
|
+
status = "❌ Missing"
|
|
122
|
+
size_str = "N/A"
|
|
123
|
+
|
|
124
|
+
table.add_row(str(path), description, status, size_str)
|
|
125
|
+
|
|
126
|
+
console.print(table)
|
|
127
|
+
console.print()
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _check_adapter_discovery(console: Console) -> None:
|
|
131
|
+
"""Check adapter discovery from configuration."""
|
|
132
|
+
console.print("[bold]3. Adapter Discovery[/bold]")
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
# Discover configuration
|
|
136
|
+
discovered = discover_config(Path.cwd())
|
|
137
|
+
|
|
138
|
+
if discovered and discovered.adapters:
|
|
139
|
+
primary = discovered.get_primary_adapter()
|
|
140
|
+
|
|
141
|
+
table = Table(show_header=True, header_style="bold magenta")
|
|
142
|
+
table.add_column("Adapter", style="cyan")
|
|
143
|
+
table.add_column("Confidence", style="white")
|
|
144
|
+
table.add_column("Source", style="green")
|
|
145
|
+
table.add_column("Status", style="yellow")
|
|
146
|
+
|
|
147
|
+
for adapter_info in discovered.adapters:
|
|
148
|
+
confidence = f"{adapter_info.confidence:.0%}"
|
|
149
|
+
status = "✅ Primary" if adapter_info == primary else "⚪ Available"
|
|
150
|
+
|
|
151
|
+
table.add_row(
|
|
152
|
+
adapter_info.adapter_type,
|
|
153
|
+
confidence,
|
|
154
|
+
adapter_info.found_in,
|
|
155
|
+
status
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
console.print(table)
|
|
159
|
+
|
|
160
|
+
if primary:
|
|
161
|
+
console.print(f"\n[green]✅ Primary adapter detected: {primary.adapter_type}[/green]")
|
|
162
|
+
console.print(f"[dim]Source: {primary.found_in}[/dim]")
|
|
163
|
+
console.print(f"[dim]Confidence: {primary.confidence:.0%}[/dim]")
|
|
164
|
+
else:
|
|
165
|
+
console.print("\n[yellow]⚠️ No primary adapter detected[/yellow]")
|
|
166
|
+
else:
|
|
167
|
+
console.print("[red]❌ No adapters discovered[/red]")
|
|
168
|
+
console.print("[dim]This usually means no credentials are configured[/dim]")
|
|
169
|
+
|
|
170
|
+
except Exception as e:
|
|
171
|
+
console.print(f"[red]❌ Error during discovery: {e}[/red]")
|
|
172
|
+
|
|
173
|
+
console.print()
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _test_adapter_instantiation(console: Console) -> None:
|
|
177
|
+
"""Test adapter instantiation."""
|
|
178
|
+
console.print("[bold]4. Adapter Instantiation Test[/bold]")
|
|
179
|
+
|
|
180
|
+
# Determine which adapter to test from .env files
|
|
181
|
+
from ..mcp.server import _load_env_configuration
|
|
182
|
+
env_config = _load_env_configuration()
|
|
183
|
+
|
|
184
|
+
if env_config:
|
|
185
|
+
adapter_type = env_config["adapter_type"]
|
|
186
|
+
config = env_config["adapter_config"]
|
|
187
|
+
else:
|
|
188
|
+
# Try to discover from existing discovery system
|
|
189
|
+
try:
|
|
190
|
+
discovered = discover_config(Path.cwd())
|
|
191
|
+
if discovered and discovered.adapters:
|
|
192
|
+
primary = discovered.get_primary_adapter()
|
|
193
|
+
if primary:
|
|
194
|
+
adapter_type = primary.adapter_type
|
|
195
|
+
# Build config from discovery
|
|
196
|
+
from ..mcp.server import _build_adapter_config_from_env_vars
|
|
197
|
+
config = _build_adapter_config_from_env_vars(adapter_type, {})
|
|
198
|
+
else:
|
|
199
|
+
adapter_type = "aitrackdown"
|
|
200
|
+
config = {"base_path": ".aitrackdown"}
|
|
201
|
+
else:
|
|
202
|
+
adapter_type = "aitrackdown"
|
|
203
|
+
config = {"base_path": ".aitrackdown"}
|
|
204
|
+
except Exception:
|
|
205
|
+
adapter_type = "aitrackdown"
|
|
206
|
+
config = {"base_path": ".aitrackdown"}
|
|
207
|
+
|
|
208
|
+
console.print(f"Testing adapter: [cyan]{adapter_type}[/cyan]")
|
|
209
|
+
console.print(f"Configuration keys: [yellow]{list(config.keys())}[/yellow]")
|
|
210
|
+
|
|
211
|
+
try:
|
|
212
|
+
# Try to instantiate adapter
|
|
213
|
+
adapter = AdapterRegistry.get_adapter(adapter_type, config)
|
|
214
|
+
|
|
215
|
+
console.print(f"[green]✅ Adapter instantiated successfully: {adapter.__class__.__name__}[/green]")
|
|
216
|
+
|
|
217
|
+
# Test basic functionality
|
|
218
|
+
if hasattr(adapter, 'validate_credentials'):
|
|
219
|
+
try:
|
|
220
|
+
is_valid, error_msg = adapter.validate_credentials()
|
|
221
|
+
if is_valid:
|
|
222
|
+
console.print("[green]✅ Credentials validation passed[/green]")
|
|
223
|
+
else:
|
|
224
|
+
console.print(f"[red]❌ Credentials validation failed: {error_msg}[/red]")
|
|
225
|
+
except Exception as e:
|
|
226
|
+
console.print(f"[yellow]⚠️ Credentials validation error: {e}[/yellow]")
|
|
227
|
+
|
|
228
|
+
except Exception as e:
|
|
229
|
+
console.print(f"[red]❌ Adapter instantiation failed: {e}[/red]")
|
|
230
|
+
|
|
231
|
+
# Provide specific guidance based on adapter type
|
|
232
|
+
if adapter_type == "linear":
|
|
233
|
+
console.print("\n[yellow]Linear adapter requires in .env/.env.local:[/yellow]")
|
|
234
|
+
console.print("• LINEAR_API_KEY=your_api_key")
|
|
235
|
+
console.print("• LINEAR_TEAM_ID=your_team_id (or LINEAR_TEAM_KEY=your_team_key)")
|
|
236
|
+
elif adapter_type == "github":
|
|
237
|
+
console.print("\n[yellow]GitHub adapter requires in .env/.env.local:[/yellow]")
|
|
238
|
+
console.print("• GITHUB_TOKEN=your_token")
|
|
239
|
+
console.print("• GITHUB_OWNER=your_username")
|
|
240
|
+
console.print("• GITHUB_REPO=your_repository")
|
|
241
|
+
elif adapter_type == "jira":
|
|
242
|
+
console.print("\n[yellow]JIRA adapter requires in .env/.env.local:[/yellow]")
|
|
243
|
+
console.print("• JIRA_SERVER=your_server_url")
|
|
244
|
+
console.print("• JIRA_EMAIL=your_email")
|
|
245
|
+
console.print("• JIRA_API_TOKEN=your_token")
|
|
246
|
+
|
|
247
|
+
console.print()
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _provide_recommendations(console: Console) -> None:
|
|
251
|
+
"""Provide configuration recommendations."""
|
|
252
|
+
console.print("[bold]5. Recommendations[/bold]")
|
|
253
|
+
|
|
254
|
+
# Check .env configuration
|
|
255
|
+
from ..mcp.server import _load_env_configuration
|
|
256
|
+
env_config = _load_env_configuration()
|
|
257
|
+
|
|
258
|
+
recommendations = []
|
|
259
|
+
|
|
260
|
+
if not env_config:
|
|
261
|
+
recommendations.append(
|
|
262
|
+
"Create .env.local or .env file with adapter configuration"
|
|
263
|
+
)
|
|
264
|
+
recommendations.append(
|
|
265
|
+
"Add MCP_TICKETER_ADAPTER=linear (or github, jira) to specify adapter type"
|
|
266
|
+
)
|
|
267
|
+
else:
|
|
268
|
+
adapter_type = env_config["adapter_type"]
|
|
269
|
+
config = env_config["adapter_config"]
|
|
270
|
+
|
|
271
|
+
# Check for incomplete configurations
|
|
272
|
+
if adapter_type == "linear":
|
|
273
|
+
if not config.get("api_key"):
|
|
274
|
+
recommendations.append("Add LINEAR_API_KEY to .env file")
|
|
275
|
+
if not config.get("team_id") and not config.get("team_key"):
|
|
276
|
+
recommendations.append("Add LINEAR_TEAM_ID or LINEAR_TEAM_KEY to .env file")
|
|
277
|
+
|
|
278
|
+
elif adapter_type == "github":
|
|
279
|
+
missing = []
|
|
280
|
+
if not config.get("token"):
|
|
281
|
+
missing.append("GITHUB_TOKEN")
|
|
282
|
+
if not config.get("owner"):
|
|
283
|
+
missing.append("GITHUB_OWNER")
|
|
284
|
+
if not config.get("repo"):
|
|
285
|
+
missing.append("GITHUB_REPO")
|
|
286
|
+
if missing:
|
|
287
|
+
recommendations.append(f"Add missing GitHub variables to .env: {', '.join(missing)}")
|
|
288
|
+
|
|
289
|
+
elif adapter_type == "jira":
|
|
290
|
+
missing = []
|
|
291
|
+
if not config.get("server"):
|
|
292
|
+
missing.append("JIRA_SERVER")
|
|
293
|
+
if not config.get("email"):
|
|
294
|
+
missing.append("JIRA_EMAIL")
|
|
295
|
+
if not config.get("api_token"):
|
|
296
|
+
missing.append("JIRA_API_TOKEN")
|
|
297
|
+
if missing:
|
|
298
|
+
recommendations.append(f"Add missing JIRA variables to .env: {', '.join(missing)}")
|
|
299
|
+
|
|
300
|
+
if recommendations:
|
|
301
|
+
for i, rec in enumerate(recommendations, 1):
|
|
302
|
+
console.print(f"{i}. [yellow]{rec}[/yellow]")
|
|
303
|
+
else:
|
|
304
|
+
console.print("[green]✅ Configuration looks good![/green]")
|
|
305
|
+
|
|
306
|
+
# Show .env file examples
|
|
307
|
+
console.print("\n[bold].env File Examples:[/bold]")
|
|
308
|
+
console.print("• Linear: [cyan]echo 'MCP_TICKETER_ADAPTER=linear\\nLINEAR_API_KEY=your_key\\nLINEAR_TEAM_ID=your_team' > .env.local[/cyan]")
|
|
309
|
+
console.print("• GitHub: [cyan]echo 'MCP_TICKETER_ADAPTER=github\\nGITHUB_TOKEN=your_token\\nGITHUB_OWNER=user\\nGITHUB_REPO=repo' > .env.local[/cyan]")
|
|
310
|
+
|
|
311
|
+
console.print("\n[bold]Quick Setup Commands:[/bold]")
|
|
312
|
+
console.print("• For Linear: [cyan]mcp-ticketer init linear[/cyan]")
|
|
313
|
+
console.print("• For GitHub: [cyan]mcp-ticketer init github[/cyan]")
|
|
314
|
+
console.print("• For JIRA: [cyan]mcp-ticketer init jira[/cyan]")
|
|
315
|
+
console.print("• For local files: [cyan]mcp-ticketer init aitrackdown[/cyan]")
|
|
316
|
+
|
|
317
|
+
console.print("\n[bold]Test Configuration:[/bold]")
|
|
318
|
+
console.print("• Run diagnostics: [cyan]mcp-ticketer diagnose[/cyan]")
|
|
319
|
+
console.print("• Test ticket creation: [cyan]mcp-ticketer create 'Test ticket'[/cyan]")
|
|
320
|
+
console.print("• List tickets: [cyan]mcp-ticketer list[/cyan]")
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def get_adapter_status() -> Dict[str, Any]:
|
|
324
|
+
"""Get current adapter status for programmatic use.
|
|
325
|
+
|
|
326
|
+
Returns:
|
|
327
|
+
Dictionary with adapter status information
|
|
328
|
+
"""
|
|
329
|
+
status = {
|
|
330
|
+
"adapter_type": None,
|
|
331
|
+
"configuration_source": None,
|
|
332
|
+
"credentials_valid": False,
|
|
333
|
+
"error_message": None,
|
|
334
|
+
"recommendations": []
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
try:
|
|
338
|
+
# Check .env files first
|
|
339
|
+
from ..mcp.server import _load_env_configuration
|
|
340
|
+
env_config = _load_env_configuration()
|
|
341
|
+
|
|
342
|
+
if env_config:
|
|
343
|
+
adapter_type = env_config["adapter_type"]
|
|
344
|
+
config = env_config["adapter_config"]
|
|
345
|
+
status["configuration_source"] = ".env files"
|
|
346
|
+
else:
|
|
347
|
+
# Try discovery system
|
|
348
|
+
discovered = discover_config(Path.cwd())
|
|
349
|
+
if discovered and discovered.adapters:
|
|
350
|
+
primary = discovered.get_primary_adapter()
|
|
351
|
+
if primary:
|
|
352
|
+
adapter_type = primary.adapter_type
|
|
353
|
+
status["configuration_source"] = primary.found_in
|
|
354
|
+
# Build basic config
|
|
355
|
+
from ..mcp.server import _build_adapter_config_from_env_vars
|
|
356
|
+
config = _build_adapter_config_from_env_vars(adapter_type, {})
|
|
357
|
+
else:
|
|
358
|
+
adapter_type = "aitrackdown"
|
|
359
|
+
config = {"base_path": ".aitrackdown"}
|
|
360
|
+
status["configuration_source"] = "default"
|
|
361
|
+
else:
|
|
362
|
+
adapter_type = "aitrackdown"
|
|
363
|
+
config = {"base_path": ".aitrackdown"}
|
|
364
|
+
status["configuration_source"] = "default"
|
|
365
|
+
|
|
366
|
+
status["adapter_type"] = adapter_type
|
|
367
|
+
|
|
368
|
+
# Test adapter instantiation
|
|
369
|
+
adapter = AdapterRegistry.get_adapter(adapter_type, config)
|
|
370
|
+
|
|
371
|
+
# Test credentials if possible
|
|
372
|
+
if hasattr(adapter, 'validate_credentials'):
|
|
373
|
+
is_valid, error_msg = adapter.validate_credentials()
|
|
374
|
+
status["credentials_valid"] = is_valid
|
|
375
|
+
if not is_valid:
|
|
376
|
+
status["error_message"] = error_msg
|
|
377
|
+
else:
|
|
378
|
+
status["credentials_valid"] = True # Assume valid if no validation method
|
|
379
|
+
|
|
380
|
+
except Exception as e:
|
|
381
|
+
status["error_message"] = str(e)
|
|
382
|
+
status["recommendations"].append("Check .env file configuration and credentials")
|
|
383
|
+
|
|
384
|
+
return status
|
mcp_ticketer/cli/main.py
CHANGED
|
@@ -317,13 +317,161 @@ def get_adapter(
|
|
|
317
317
|
return AdapterRegistry.get_adapter(adapter_type, adapter_config)
|
|
318
318
|
|
|
319
319
|
|
|
320
|
+
def _prompt_for_adapter_selection(console: Console) -> str:
|
|
321
|
+
"""Interactive prompt for adapter selection.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
console: Rich console for output
|
|
325
|
+
|
|
326
|
+
Returns:
|
|
327
|
+
Selected adapter type
|
|
328
|
+
"""
|
|
329
|
+
console.print("\n[bold blue]🚀 MCP Ticketer Setup[/bold blue]")
|
|
330
|
+
console.print("Choose which ticket system you want to connect to:\n")
|
|
331
|
+
|
|
332
|
+
# Define adapter options with descriptions
|
|
333
|
+
adapters = [
|
|
334
|
+
{
|
|
335
|
+
"name": "linear",
|
|
336
|
+
"title": "Linear",
|
|
337
|
+
"description": "Modern project management (linear.app)",
|
|
338
|
+
"requirements": "API key and team ID"
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
"name": "github",
|
|
342
|
+
"title": "GitHub Issues",
|
|
343
|
+
"description": "GitHub repository issues",
|
|
344
|
+
"requirements": "Personal access token, owner, and repo"
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
"name": "jira",
|
|
348
|
+
"title": "JIRA",
|
|
349
|
+
"description": "Atlassian JIRA project management",
|
|
350
|
+
"requirements": "Server URL, email, and API token"
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
"name": "aitrackdown",
|
|
354
|
+
"title": "Local Files (AITrackdown)",
|
|
355
|
+
"description": "Store tickets in local files (no external service)",
|
|
356
|
+
"requirements": "None - works offline"
|
|
357
|
+
}
|
|
358
|
+
]
|
|
359
|
+
|
|
360
|
+
# Display options
|
|
361
|
+
for i, adapter in enumerate(adapters, 1):
|
|
362
|
+
console.print(f"[cyan]{i}.[/cyan] [bold]{adapter['title']}[/bold]")
|
|
363
|
+
console.print(f" {adapter['description']}")
|
|
364
|
+
console.print(f" [dim]Requirements: {adapter['requirements']}[/dim]\n")
|
|
365
|
+
|
|
366
|
+
# Get user selection
|
|
367
|
+
while True:
|
|
368
|
+
try:
|
|
369
|
+
choice = typer.prompt(
|
|
370
|
+
"Select adapter (1-4)",
|
|
371
|
+
type=int,
|
|
372
|
+
default=1
|
|
373
|
+
)
|
|
374
|
+
if 1 <= choice <= len(adapters):
|
|
375
|
+
selected_adapter = adapters[choice - 1]
|
|
376
|
+
console.print(f"\n[green]✓ Selected: {selected_adapter['title']}[/green]")
|
|
377
|
+
return selected_adapter["name"]
|
|
378
|
+
else:
|
|
379
|
+
console.print(f"[red]Please enter a number between 1 and {len(adapters)}[/red]")
|
|
380
|
+
except (ValueError, typer.Abort):
|
|
381
|
+
console.print("[yellow]Setup cancelled.[/yellow]")
|
|
382
|
+
raise typer.Exit(0)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
@app.command()
|
|
386
|
+
def setup(
|
|
387
|
+
adapter: Optional[str] = typer.Option(
|
|
388
|
+
None,
|
|
389
|
+
"--adapter",
|
|
390
|
+
"-a",
|
|
391
|
+
help="Adapter type to use (interactive prompt if not specified)",
|
|
392
|
+
),
|
|
393
|
+
project_path: Optional[str] = typer.Option(
|
|
394
|
+
None, "--path", help="Project path (default: current directory)"
|
|
395
|
+
),
|
|
396
|
+
global_config: bool = typer.Option(
|
|
397
|
+
False,
|
|
398
|
+
"--global",
|
|
399
|
+
"-g",
|
|
400
|
+
help="Save to global config instead of project-specific",
|
|
401
|
+
),
|
|
402
|
+
base_path: Optional[str] = typer.Option(
|
|
403
|
+
None,
|
|
404
|
+
"--base-path",
|
|
405
|
+
"-p",
|
|
406
|
+
help="Base path for ticket storage (AITrackdown only)",
|
|
407
|
+
),
|
|
408
|
+
api_key: Optional[str] = typer.Option(
|
|
409
|
+
None, "--api-key", help="API key for Linear or API token for JIRA"
|
|
410
|
+
),
|
|
411
|
+
team_id: Optional[str] = typer.Option(
|
|
412
|
+
None, "--team-id", help="Linear team ID (required for Linear adapter)"
|
|
413
|
+
),
|
|
414
|
+
jira_server: Optional[str] = typer.Option(
|
|
415
|
+
None,
|
|
416
|
+
"--jira-server",
|
|
417
|
+
help="JIRA server URL (e.g., https://company.atlassian.net)",
|
|
418
|
+
),
|
|
419
|
+
jira_email: Optional[str] = typer.Option(
|
|
420
|
+
None, "--jira-email", help="JIRA user email for authentication"
|
|
421
|
+
),
|
|
422
|
+
jira_project: Optional[str] = typer.Option(
|
|
423
|
+
None, "--jira-project", help="Default JIRA project key"
|
|
424
|
+
),
|
|
425
|
+
github_owner: Optional[str] = typer.Option(
|
|
426
|
+
None, "--github-owner", help="GitHub repository owner"
|
|
427
|
+
),
|
|
428
|
+
github_repo: Optional[str] = typer.Option(
|
|
429
|
+
None, "--github-repo", help="GitHub repository name"
|
|
430
|
+
),
|
|
431
|
+
github_token: Optional[str] = typer.Option(
|
|
432
|
+
None, "--github-token", help="GitHub Personal Access Token"
|
|
433
|
+
),
|
|
434
|
+
) -> None:
|
|
435
|
+
"""Interactive setup wizard for MCP Ticketer (alias for init).
|
|
436
|
+
|
|
437
|
+
This command provides a user-friendly setup experience with prompts
|
|
438
|
+
to guide you through configuring MCP Ticketer for your preferred
|
|
439
|
+
ticket management system. It's identical to 'init' and 'install'.
|
|
440
|
+
|
|
441
|
+
Examples:
|
|
442
|
+
# Run interactive setup
|
|
443
|
+
mcp-ticketer setup
|
|
444
|
+
|
|
445
|
+
# Setup with specific adapter
|
|
446
|
+
mcp-ticketer setup --adapter linear
|
|
447
|
+
|
|
448
|
+
# Setup for different project
|
|
449
|
+
mcp-ticketer setup --path /path/to/project
|
|
450
|
+
"""
|
|
451
|
+
# Call init with all parameters
|
|
452
|
+
init(
|
|
453
|
+
adapter=adapter,
|
|
454
|
+
project_path=project_path,
|
|
455
|
+
global_config=global_config,
|
|
456
|
+
base_path=base_path,
|
|
457
|
+
api_key=api_key,
|
|
458
|
+
team_id=team_id,
|
|
459
|
+
jira_server=jira_server,
|
|
460
|
+
jira_email=jira_email,
|
|
461
|
+
jira_project=jira_project,
|
|
462
|
+
github_owner=github_owner,
|
|
463
|
+
github_repo=github_repo,
|
|
464
|
+
github_token=github_token,
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
|
|
320
468
|
@app.command()
|
|
321
469
|
def init(
|
|
322
470
|
adapter: Optional[str] = typer.Option(
|
|
323
471
|
None,
|
|
324
472
|
"--adapter",
|
|
325
473
|
"-a",
|
|
326
|
-
help="Adapter type to use (
|
|
474
|
+
help="Adapter type to use (interactive prompt if not specified)",
|
|
327
475
|
),
|
|
328
476
|
project_path: Optional[str] = typer.Option(
|
|
329
477
|
None, "--path", help="Project path (default: current directory)"
|
|
@@ -369,11 +517,17 @@ def init(
|
|
|
369
517
|
) -> None:
|
|
370
518
|
"""Initialize mcp-ticketer for the current project.
|
|
371
519
|
|
|
520
|
+
This command sets up MCP Ticketer configuration with interactive prompts
|
|
521
|
+
to guide you through the process. It auto-detects adapter configuration
|
|
522
|
+
from .env files or prompts for interactive setup if no configuration is found.
|
|
523
|
+
|
|
372
524
|
Creates .mcp-ticketer/config.json in the current directory with
|
|
373
525
|
auto-detected or specified adapter configuration.
|
|
374
526
|
|
|
527
|
+
Note: 'setup' and 'install' are synonyms for this command.
|
|
528
|
+
|
|
375
529
|
Examples:
|
|
376
|
-
#
|
|
530
|
+
# Interactive setup (same as 'setup' and 'install')
|
|
377
531
|
mcp-ticketer init
|
|
378
532
|
|
|
379
533
|
# Force specific adapter
|
|
@@ -429,16 +583,21 @@ def init(
|
|
|
429
583
|
f"\n[dim]Configuration found in: {primary.found_in}[/dim]"
|
|
430
584
|
)
|
|
431
585
|
console.print(f"[dim]Confidence: {primary.confidence:.0%}[/dim]")
|
|
586
|
+
|
|
587
|
+
# Ask user to confirm auto-detected adapter
|
|
588
|
+
if not typer.confirm(
|
|
589
|
+
f"Use detected {adapter_type} adapter?",
|
|
590
|
+
default=True,
|
|
591
|
+
):
|
|
592
|
+
adapter_type = None # Will trigger interactive selection
|
|
432
593
|
else:
|
|
433
|
-
adapter_type =
|
|
434
|
-
console.print(
|
|
435
|
-
"[yellow]⚠ No credentials found, defaulting to aitrackdown[/yellow]"
|
|
436
|
-
)
|
|
594
|
+
adapter_type = None # Will trigger interactive selection
|
|
437
595
|
else:
|
|
438
|
-
adapter_type =
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
596
|
+
adapter_type = None # Will trigger interactive selection
|
|
597
|
+
|
|
598
|
+
# If no adapter determined, show interactive selection
|
|
599
|
+
if not adapter_type:
|
|
600
|
+
adapter_type = _prompt_for_adapter_selection(console)
|
|
442
601
|
|
|
443
602
|
# 2. Create configuration based on adapter type
|
|
444
603
|
config = {"default_adapter": adapter_type, "adapters": {}}
|
|
@@ -462,59 +621,99 @@ def init(
|
|
|
462
621
|
}
|
|
463
622
|
|
|
464
623
|
elif adapter_type == "linear":
|
|
465
|
-
# If not auto-discovered, build from CLI params
|
|
624
|
+
# If not auto-discovered, build from CLI params or prompt
|
|
466
625
|
if adapter_type not in config["adapters"]:
|
|
467
626
|
linear_config = {}
|
|
468
627
|
|
|
469
|
-
# Team ID
|
|
470
|
-
if team_id:
|
|
471
|
-
linear_config["team_id"] = team_id
|
|
472
|
-
|
|
473
628
|
# API Key
|
|
474
629
|
linear_api_key = api_key or os.getenv("LINEAR_API_KEY")
|
|
630
|
+
if not linear_api_key and not discovered:
|
|
631
|
+
console.print("\n[bold]Linear Configuration[/bold]")
|
|
632
|
+
console.print("You need a Linear API key to connect to Linear.")
|
|
633
|
+
console.print("[dim]Get your API key at: https://linear.app/settings/api[/dim]\n")
|
|
634
|
+
|
|
635
|
+
linear_api_key = typer.prompt(
|
|
636
|
+
"Enter your Linear API key",
|
|
637
|
+
hide_input=True
|
|
638
|
+
)
|
|
639
|
+
|
|
475
640
|
if linear_api_key:
|
|
476
641
|
linear_config["api_key"] = linear_api_key
|
|
477
|
-
elif not discovered:
|
|
478
|
-
console.print("[yellow]Warning:[/yellow] No Linear API key provided.")
|
|
479
|
-
console.print(
|
|
480
|
-
"Set LINEAR_API_KEY environment variable or use --api-key option"
|
|
481
|
-
)
|
|
482
642
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
643
|
+
# Team ID
|
|
644
|
+
linear_team_id = team_id or os.getenv("LINEAR_TEAM_ID")
|
|
645
|
+
if not linear_team_id and not discovered:
|
|
646
|
+
console.print("\nYou need your Linear team ID.")
|
|
647
|
+
console.print("[dim]Find it in Linear settings or team URL[/dim]\n")
|
|
648
|
+
|
|
649
|
+
linear_team_id = typer.prompt("Enter your Linear team ID")
|
|
650
|
+
|
|
651
|
+
if linear_team_id:
|
|
652
|
+
linear_config["team_id"] = linear_team_id
|
|
653
|
+
|
|
654
|
+
if not linear_config.get("api_key") or not linear_config.get("team_id"):
|
|
655
|
+
console.print("[red]Error:[/red] Linear requires both API key and team ID")
|
|
656
|
+
console.print("Run 'mcp-ticketer init --adapter linear' with proper credentials")
|
|
657
|
+
raise typer.Exit(1)
|
|
658
|
+
|
|
659
|
+
linear_config["type"] = "linear"
|
|
660
|
+
config["adapters"]["linear"] = linear_config
|
|
486
661
|
|
|
487
662
|
elif adapter_type == "jira":
|
|
488
|
-
# If not auto-discovered, build from CLI params
|
|
663
|
+
# If not auto-discovered, build from CLI params or prompt
|
|
489
664
|
if adapter_type not in config["adapters"]:
|
|
490
665
|
server = jira_server or os.getenv("JIRA_SERVER")
|
|
491
666
|
email = jira_email or os.getenv("JIRA_EMAIL")
|
|
492
667
|
token = api_key or os.getenv("JIRA_API_TOKEN")
|
|
493
668
|
project = jira_project or os.getenv("JIRA_PROJECT_KEY")
|
|
494
669
|
|
|
670
|
+
# Interactive prompts for missing values
|
|
671
|
+
if not server and not discovered:
|
|
672
|
+
console.print("\n[bold]JIRA Configuration[/bold]")
|
|
673
|
+
console.print("Enter your JIRA server details.\n")
|
|
674
|
+
|
|
675
|
+
server = typer.prompt(
|
|
676
|
+
"JIRA server URL (e.g., https://company.atlassian.net)"
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
if not email and not discovered:
|
|
680
|
+
email = typer.prompt("Your JIRA email address")
|
|
681
|
+
|
|
682
|
+
if not token and not discovered:
|
|
683
|
+
console.print("\nYou need a JIRA API token.")
|
|
684
|
+
console.print("[dim]Generate one at: https://id.atlassian.com/manage/api-tokens[/dim]\n")
|
|
685
|
+
|
|
686
|
+
token = typer.prompt(
|
|
687
|
+
"Enter your JIRA API token",
|
|
688
|
+
hide_input=True
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
if not project and not discovered:
|
|
692
|
+
project = typer.prompt(
|
|
693
|
+
"Default JIRA project key (optional, press Enter to skip)",
|
|
694
|
+
default="",
|
|
695
|
+
show_default=False
|
|
696
|
+
)
|
|
697
|
+
|
|
698
|
+
# Validate required fields
|
|
495
699
|
if not server:
|
|
496
700
|
console.print("[red]Error:[/red] JIRA server URL is required")
|
|
497
|
-
console.print(
|
|
498
|
-
"Use --jira-server or set JIRA_SERVER environment variable"
|
|
499
|
-
)
|
|
500
701
|
raise typer.Exit(1)
|
|
501
702
|
|
|
502
703
|
if not email:
|
|
503
704
|
console.print("[red]Error:[/red] JIRA email is required")
|
|
504
|
-
console.print("Use --jira-email or set JIRA_EMAIL environment variable")
|
|
505
705
|
raise typer.Exit(1)
|
|
506
706
|
|
|
507
707
|
if not token:
|
|
508
708
|
console.print("[red]Error:[/red] JIRA API token is required")
|
|
509
|
-
console.print(
|
|
510
|
-
"Use --api-key or set JIRA_API_TOKEN environment variable"
|
|
511
|
-
)
|
|
512
|
-
console.print(
|
|
513
|
-
"[dim]Generate token at: https://id.atlassian.com/manage/api-tokens[/dim]"
|
|
514
|
-
)
|
|
515
709
|
raise typer.Exit(1)
|
|
516
710
|
|
|
517
|
-
jira_config = {
|
|
711
|
+
jira_config = {
|
|
712
|
+
"server": server,
|
|
713
|
+
"email": email,
|
|
714
|
+
"api_token": token,
|
|
715
|
+
"type": "jira"
|
|
716
|
+
}
|
|
518
717
|
|
|
519
718
|
if project:
|
|
520
719
|
jira_config["project_key"] = project
|
|
@@ -522,45 +721,50 @@ def init(
|
|
|
522
721
|
config["adapters"]["jira"] = jira_config
|
|
523
722
|
|
|
524
723
|
elif adapter_type == "github":
|
|
525
|
-
# If not auto-discovered, build from CLI params
|
|
724
|
+
# If not auto-discovered, build from CLI params or prompt
|
|
526
725
|
if adapter_type not in config["adapters"]:
|
|
527
726
|
owner = github_owner or os.getenv("GITHUB_OWNER")
|
|
528
727
|
repo = github_repo or os.getenv("GITHUB_REPO")
|
|
529
728
|
token = github_token or os.getenv("GITHUB_TOKEN")
|
|
530
729
|
|
|
730
|
+
# Interactive prompts for missing values
|
|
731
|
+
if not owner and not discovered:
|
|
732
|
+
console.print("\n[bold]GitHub Configuration[/bold]")
|
|
733
|
+
console.print("Enter your GitHub repository details.\n")
|
|
734
|
+
|
|
735
|
+
owner = typer.prompt("GitHub repository owner (username or organization)")
|
|
736
|
+
|
|
737
|
+
if not repo and not discovered:
|
|
738
|
+
repo = typer.prompt("GitHub repository name")
|
|
739
|
+
|
|
740
|
+
if not token and not discovered:
|
|
741
|
+
console.print("\nYou need a GitHub Personal Access Token.")
|
|
742
|
+
console.print("[dim]Create one at: https://github.com/settings/tokens/new[/dim]")
|
|
743
|
+
console.print("[dim]Required scopes: repo (for private repos) or public_repo (for public repos)[/dim]\n")
|
|
744
|
+
|
|
745
|
+
token = typer.prompt(
|
|
746
|
+
"Enter your GitHub Personal Access Token",
|
|
747
|
+
hide_input=True
|
|
748
|
+
)
|
|
749
|
+
|
|
750
|
+
# Validate required fields
|
|
531
751
|
if not owner:
|
|
532
752
|
console.print("[red]Error:[/red] GitHub repository owner is required")
|
|
533
|
-
console.print(
|
|
534
|
-
"Use --github-owner or set GITHUB_OWNER environment variable"
|
|
535
|
-
)
|
|
536
753
|
raise typer.Exit(1)
|
|
537
754
|
|
|
538
755
|
if not repo:
|
|
539
756
|
console.print("[red]Error:[/red] GitHub repository name is required")
|
|
540
|
-
console.print(
|
|
541
|
-
"Use --github-repo or set GITHUB_REPO environment variable"
|
|
542
|
-
)
|
|
543
757
|
raise typer.Exit(1)
|
|
544
758
|
|
|
545
759
|
if not token:
|
|
546
|
-
console.print(
|
|
547
|
-
"[red]Error:[/red] GitHub Personal Access Token is required"
|
|
548
|
-
)
|
|
549
|
-
console.print(
|
|
550
|
-
"Use --github-token or set GITHUB_TOKEN environment variable"
|
|
551
|
-
)
|
|
552
|
-
console.print(
|
|
553
|
-
"[dim]Create token at: https://github.com/settings/tokens/new[/dim]"
|
|
554
|
-
)
|
|
555
|
-
console.print(
|
|
556
|
-
"[dim]Required scopes: repo (for private repos) or public_repo (for public repos)[/dim]"
|
|
557
|
-
)
|
|
760
|
+
console.print("[red]Error:[/red] GitHub Personal Access Token is required")
|
|
558
761
|
raise typer.Exit(1)
|
|
559
762
|
|
|
560
763
|
config["adapters"]["github"] = {
|
|
561
764
|
"owner": owner,
|
|
562
765
|
"repo": repo,
|
|
563
766
|
"token": token,
|
|
767
|
+
"type": "github"
|
|
564
768
|
}
|
|
565
769
|
|
|
566
770
|
# 5. Save to appropriate location
|
|
@@ -600,6 +804,48 @@ def init(
|
|
|
600
804
|
f.write("# MCP Ticketer\n.mcp-ticketer/\n")
|
|
601
805
|
console.print("[dim]✓ Created .gitignore with .mcp-ticketer/[/dim]")
|
|
602
806
|
|
|
807
|
+
# Show next steps
|
|
808
|
+
_show_next_steps(console, adapter_type, config_file_path)
|
|
809
|
+
|
|
810
|
+
|
|
811
|
+
def _show_next_steps(console: Console, adapter_type: str, config_file_path: Path) -> None:
|
|
812
|
+
"""Show helpful next steps after initialization.
|
|
813
|
+
|
|
814
|
+
Args:
|
|
815
|
+
console: Rich console for output
|
|
816
|
+
adapter_type: Type of adapter that was configured
|
|
817
|
+
config_file_path: Path to the configuration file
|
|
818
|
+
"""
|
|
819
|
+
console.print("\n[bold green]🎉 Setup Complete![/bold green]")
|
|
820
|
+
console.print(f"MCP Ticketer is now configured to use {adapter_type.title()}.\n")
|
|
821
|
+
|
|
822
|
+
console.print("[bold]Next Steps:[/bold]")
|
|
823
|
+
console.print("1. [cyan]Test your configuration:[/cyan]")
|
|
824
|
+
console.print(" mcp-ticketer diagnose")
|
|
825
|
+
console.print("\n2. [cyan]Create a test ticket:[/cyan]")
|
|
826
|
+
console.print(" mcp-ticketer create 'Test ticket from MCP Ticketer'")
|
|
827
|
+
|
|
828
|
+
if adapter_type != "aitrackdown":
|
|
829
|
+
console.print(f"\n3. [cyan]Verify the ticket appears in {adapter_type.title()}[/cyan]")
|
|
830
|
+
|
|
831
|
+
if adapter_type == "linear":
|
|
832
|
+
console.print(" Check your Linear workspace for the new ticket")
|
|
833
|
+
elif adapter_type == "github":
|
|
834
|
+
console.print(" Check your GitHub repository's Issues tab")
|
|
835
|
+
elif adapter_type == "jira":
|
|
836
|
+
console.print(" Check your JIRA project for the new ticket")
|
|
837
|
+
else:
|
|
838
|
+
console.print("\n3. [cyan]Check local ticket storage:[/cyan]")
|
|
839
|
+
console.print(" ls .aitrackdown/")
|
|
840
|
+
|
|
841
|
+
console.print("\n4. [cyan]Configure MCP clients (optional):[/cyan]")
|
|
842
|
+
console.print(" mcp-ticketer mcp claude # For Claude Code")
|
|
843
|
+
console.print(" mcp-ticketer mcp auggie # For Auggie")
|
|
844
|
+
console.print(" mcp-ticketer mcp gemini # For Gemini CLI")
|
|
845
|
+
|
|
846
|
+
console.print(f"\n[dim]Configuration saved to: {config_file_path}[/dim]")
|
|
847
|
+
console.print("[dim]Run 'mcp-ticketer --help' for more commands[/dim]")
|
|
848
|
+
|
|
603
849
|
|
|
604
850
|
@app.command()
|
|
605
851
|
def install(
|
|
@@ -653,12 +899,12 @@ def install(
|
|
|
653
899
|
) -> None:
|
|
654
900
|
"""Initialize mcp-ticketer for the current project (alias for init).
|
|
655
901
|
|
|
656
|
-
This command is synonymous with 'init' and
|
|
657
|
-
|
|
658
|
-
|
|
902
|
+
This command is synonymous with 'init' and 'setup' - all three provide
|
|
903
|
+
identical functionality with interactive prompts to guide you through
|
|
904
|
+
configuring MCP Ticketer for your preferred ticket management system.
|
|
659
905
|
|
|
660
906
|
Examples:
|
|
661
|
-
#
|
|
907
|
+
# Interactive setup (same as 'init' and 'setup')
|
|
662
908
|
mcp-ticketer install
|
|
663
909
|
|
|
664
910
|
# Force specific adapter
|
|
@@ -1548,6 +1794,9 @@ def check(queue_id: str = typer.Argument(..., help="Queue ID to check")):
|
|
|
1548
1794
|
console.print(f"\nRetry Count: {item.retry_count}")
|
|
1549
1795
|
|
|
1550
1796
|
|
|
1797
|
+
|
|
1798
|
+
|
|
1799
|
+
|
|
1551
1800
|
@app.command()
|
|
1552
1801
|
def serve(
|
|
1553
1802
|
adapter: Optional[AdapterType] = typer.Option(
|
|
@@ -1575,16 +1824,27 @@ def serve(
|
|
|
1575
1824
|
# Load configuration (respects project-specific config in cwd)
|
|
1576
1825
|
config = load_config()
|
|
1577
1826
|
|
|
1578
|
-
# Determine adapter type
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1827
|
+
# Determine adapter type with priority: CLI arg > .env files > config > default
|
|
1828
|
+
if adapter:
|
|
1829
|
+
# Priority 1: Command line argument
|
|
1830
|
+
adapter_type = adapter.value
|
|
1831
|
+
# Get base config from config file
|
|
1832
|
+
adapters_config = config.get("adapters", {})
|
|
1833
|
+
adapter_config = adapters_config.get(adapter_type, {})
|
|
1834
|
+
else:
|
|
1835
|
+
# Priority 2: .env files
|
|
1836
|
+
from ..mcp.server import _load_env_configuration
|
|
1837
|
+
env_config = _load_env_configuration()
|
|
1838
|
+
if env_config:
|
|
1839
|
+
adapter_type = env_config["adapter_type"]
|
|
1840
|
+
adapter_config = env_config["adapter_config"]
|
|
1841
|
+
else:
|
|
1842
|
+
# Priority 3: Configuration file
|
|
1843
|
+
adapter_type = config.get("default_adapter", "aitrackdown")
|
|
1844
|
+
adapters_config = config.get("adapters", {})
|
|
1845
|
+
adapter_config = adapters_config.get(adapter_type, {})
|
|
1586
1846
|
|
|
1587
|
-
# Override with command line options if provided
|
|
1847
|
+
# Override with command line options if provided (highest priority)
|
|
1588
1848
|
if base_path and adapter_type == "aitrackdown":
|
|
1589
1849
|
adapter_config["base_path"] = base_path
|
|
1590
1850
|
|
mcp_ticketer/mcp/server.py
CHANGED
|
@@ -1652,56 +1652,191 @@ async def main():
|
|
|
1652
1652
|
# Load configuration
|
|
1653
1653
|
import json
|
|
1654
1654
|
import logging
|
|
1655
|
+
import os
|
|
1655
1656
|
from pathlib import Path
|
|
1656
1657
|
|
|
1657
1658
|
logger = logging.getLogger(__name__)
|
|
1658
1659
|
|
|
1659
|
-
#
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
#
|
|
1675
|
-
|
|
1660
|
+
# Initialize defaults
|
|
1661
|
+
adapter_type = "aitrackdown"
|
|
1662
|
+
adapter_config = {"base_path": ".aitrackdown"}
|
|
1663
|
+
|
|
1664
|
+
# Priority 1: Check .env files (highest priority for MCP)
|
|
1665
|
+
env_config = _load_env_configuration()
|
|
1666
|
+
if env_config and env_config.get("adapter_type"):
|
|
1667
|
+
adapter_type = env_config["adapter_type"]
|
|
1668
|
+
adapter_config = env_config["adapter_config"]
|
|
1669
|
+
logger.info(f"Using adapter from .env files: {adapter_type}")
|
|
1670
|
+
logger.info(f"Built adapter config from .env: {list(adapter_config.keys())}")
|
|
1671
|
+
else:
|
|
1672
|
+
# Priority 2: Check project-local config file
|
|
1673
|
+
config_file = Path.cwd() / ".mcp-ticketer" / "config.json"
|
|
1674
|
+
if config_file.exists():
|
|
1675
|
+
# Validate config is within project
|
|
1676
|
+
try:
|
|
1677
|
+
if not config_file.resolve().is_relative_to(Path.cwd().resolve()):
|
|
1678
|
+
logger.error(
|
|
1679
|
+
f"Security violation: Config file {config_file} "
|
|
1680
|
+
"is not within project directory"
|
|
1681
|
+
)
|
|
1682
|
+
raise ValueError(
|
|
1683
|
+
f"Security violation: Config file {config_file} "
|
|
1684
|
+
"is not within project directory"
|
|
1685
|
+
)
|
|
1686
|
+
except (ValueError, RuntimeError):
|
|
1687
|
+
# is_relative_to may raise ValueError in some cases
|
|
1688
|
+
pass
|
|
1676
1689
|
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1690
|
+
try:
|
|
1691
|
+
with open(config_file) as f:
|
|
1692
|
+
config = json.load(f)
|
|
1693
|
+
adapter_type = config.get("default_adapter", "aitrackdown")
|
|
1694
|
+
# Get adapter-specific config
|
|
1695
|
+
adapters_config = config.get("adapters", {})
|
|
1696
|
+
adapter_config = adapters_config.get(adapter_type, {})
|
|
1697
|
+
# Fallback to legacy config format
|
|
1698
|
+
if not adapter_config and "config" in config:
|
|
1699
|
+
adapter_config = config["config"]
|
|
1700
|
+
logger.info(
|
|
1701
|
+
f"Loaded MCP configuration from project-local: {config_file}"
|
|
1702
|
+
)
|
|
1703
|
+
except (OSError, json.JSONDecodeError) as e:
|
|
1704
|
+
logger.warning(f"Could not load project config: {e}, using defaults")
|
|
1705
|
+
adapter_type = "aitrackdown"
|
|
1706
|
+
adapter_config = {"base_path": ".aitrackdown"}
|
|
1707
|
+
else:
|
|
1708
|
+
# Priority 3: Default to aitrackdown
|
|
1709
|
+
logger.info("No configuration found, defaulting to aitrackdown adapter")
|
|
1692
1710
|
adapter_type = "aitrackdown"
|
|
1693
1711
|
adapter_config = {"base_path": ".aitrackdown"}
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
adapter_config = {"base_path": ".aitrackdown"}
|
|
1712
|
+
|
|
1713
|
+
# Log final configuration for debugging
|
|
1714
|
+
logger.info(f"Starting MCP server with adapter: {adapter_type}")
|
|
1715
|
+
logger.debug(f"Adapter config keys: {list(adapter_config.keys())}")
|
|
1699
1716
|
|
|
1700
1717
|
# Create and run server
|
|
1701
1718
|
server = MCPTicketServer(adapter_type, adapter_config)
|
|
1702
1719
|
await server.run()
|
|
1703
1720
|
|
|
1704
1721
|
|
|
1722
|
+
def _load_env_configuration() -> Optional[dict[str, Any]]:
|
|
1723
|
+
"""Load adapter configuration from .env files.
|
|
1724
|
+
|
|
1725
|
+
Checks .env.local first (highest priority), then .env.
|
|
1726
|
+
|
|
1727
|
+
Returns:
|
|
1728
|
+
Dictionary with 'adapter_type' and 'adapter_config' keys, or None if no config found
|
|
1729
|
+
"""
|
|
1730
|
+
from pathlib import Path
|
|
1731
|
+
|
|
1732
|
+
# Check for .env files in order of preference
|
|
1733
|
+
env_files = [".env.local", ".env"]
|
|
1734
|
+
env_vars = {}
|
|
1735
|
+
|
|
1736
|
+
for env_file in env_files:
|
|
1737
|
+
env_path = Path.cwd() / env_file
|
|
1738
|
+
if env_path.exists():
|
|
1739
|
+
try:
|
|
1740
|
+
# Parse .env file manually to avoid external dependencies
|
|
1741
|
+
with open(env_path, 'r') as f:
|
|
1742
|
+
for line in f:
|
|
1743
|
+
line = line.strip()
|
|
1744
|
+
if line and not line.startswith('#') and '=' in line:
|
|
1745
|
+
key, value = line.split('=', 1)
|
|
1746
|
+
key = key.strip()
|
|
1747
|
+
value = value.strip().strip('"').strip("'")
|
|
1748
|
+
if value: # Only add non-empty values
|
|
1749
|
+
env_vars[key] = value
|
|
1750
|
+
except Exception:
|
|
1751
|
+
continue
|
|
1752
|
+
|
|
1753
|
+
if not env_vars:
|
|
1754
|
+
return None
|
|
1755
|
+
|
|
1756
|
+
# Determine adapter type and build config
|
|
1757
|
+
adapter_type = env_vars.get("MCP_TICKETER_ADAPTER")
|
|
1758
|
+
if not adapter_type:
|
|
1759
|
+
# Auto-detect based on available keys
|
|
1760
|
+
if any(key.startswith("LINEAR_") for key in env_vars):
|
|
1761
|
+
adapter_type = "linear"
|
|
1762
|
+
elif any(key.startswith("GITHUB_") for key in env_vars):
|
|
1763
|
+
adapter_type = "github"
|
|
1764
|
+
elif any(key.startswith("JIRA_") for key in env_vars):
|
|
1765
|
+
adapter_type = "jira"
|
|
1766
|
+
else:
|
|
1767
|
+
return None
|
|
1768
|
+
|
|
1769
|
+
# Build adapter-specific configuration
|
|
1770
|
+
adapter_config = _build_adapter_config_from_env_vars(adapter_type, env_vars)
|
|
1771
|
+
|
|
1772
|
+
if not adapter_config:
|
|
1773
|
+
return None
|
|
1774
|
+
|
|
1775
|
+
return {
|
|
1776
|
+
"adapter_type": adapter_type,
|
|
1777
|
+
"adapter_config": adapter_config
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
|
|
1781
|
+
def _build_adapter_config_from_env_vars(adapter_type: str, env_vars: dict[str, str]) -> dict[str, Any]:
|
|
1782
|
+
"""Build adapter configuration from parsed environment variables.
|
|
1783
|
+
|
|
1784
|
+
Args:
|
|
1785
|
+
adapter_type: Type of adapter to configure
|
|
1786
|
+
env_vars: Dictionary of environment variables from .env files
|
|
1787
|
+
|
|
1788
|
+
Returns:
|
|
1789
|
+
Dictionary of adapter configuration
|
|
1790
|
+
"""
|
|
1791
|
+
config = {}
|
|
1792
|
+
|
|
1793
|
+
if adapter_type == "linear":
|
|
1794
|
+
# Linear adapter configuration
|
|
1795
|
+
if env_vars.get("LINEAR_API_KEY"):
|
|
1796
|
+
config["api_key"] = env_vars["LINEAR_API_KEY"]
|
|
1797
|
+
if env_vars.get("LINEAR_TEAM_ID"):
|
|
1798
|
+
config["team_id"] = env_vars["LINEAR_TEAM_ID"]
|
|
1799
|
+
if env_vars.get("LINEAR_TEAM_KEY"):
|
|
1800
|
+
config["team_key"] = env_vars["LINEAR_TEAM_KEY"]
|
|
1801
|
+
if env_vars.get("LINEAR_API_URL"):
|
|
1802
|
+
config["api_url"] = env_vars["LINEAR_API_URL"]
|
|
1803
|
+
|
|
1804
|
+
elif adapter_type == "github":
|
|
1805
|
+
# GitHub adapter configuration
|
|
1806
|
+
if env_vars.get("GITHUB_TOKEN"):
|
|
1807
|
+
config["token"] = env_vars["GITHUB_TOKEN"]
|
|
1808
|
+
if env_vars.get("GITHUB_OWNER"):
|
|
1809
|
+
config["owner"] = env_vars["GITHUB_OWNER"]
|
|
1810
|
+
if env_vars.get("GITHUB_REPO"):
|
|
1811
|
+
config["repo"] = env_vars["GITHUB_REPO"]
|
|
1812
|
+
|
|
1813
|
+
elif adapter_type == "jira":
|
|
1814
|
+
# JIRA adapter configuration
|
|
1815
|
+
if env_vars.get("JIRA_SERVER"):
|
|
1816
|
+
config["server"] = env_vars["JIRA_SERVER"]
|
|
1817
|
+
if env_vars.get("JIRA_EMAIL"):
|
|
1818
|
+
config["email"] = env_vars["JIRA_EMAIL"]
|
|
1819
|
+
if env_vars.get("JIRA_API_TOKEN"):
|
|
1820
|
+
config["api_token"] = env_vars["JIRA_API_TOKEN"]
|
|
1821
|
+
if env_vars.get("JIRA_PROJECT_KEY"):
|
|
1822
|
+
config["project_key"] = env_vars["JIRA_PROJECT_KEY"]
|
|
1823
|
+
|
|
1824
|
+
elif adapter_type == "aitrackdown":
|
|
1825
|
+
# AITrackdown adapter configuration
|
|
1826
|
+
base_path = env_vars.get("MCP_TICKETER_BASE_PATH", ".aitrackdown")
|
|
1827
|
+
config["base_path"] = base_path
|
|
1828
|
+
config["auto_create_dirs"] = True
|
|
1829
|
+
|
|
1830
|
+
# Add any generic overrides
|
|
1831
|
+
if env_vars.get("MCP_TICKETER_API_KEY"):
|
|
1832
|
+
config["api_key"] = env_vars["MCP_TICKETER_API_KEY"]
|
|
1833
|
+
|
|
1834
|
+
return config
|
|
1835
|
+
|
|
1836
|
+
|
|
1837
|
+
|
|
1838
|
+
|
|
1839
|
+
|
|
1705
1840
|
# Add diagnostic handler methods to MCPTicketServer class
|
|
1706
1841
|
async def _handle_system_health(self, arguments: dict[str, Any]) -> dict[str, Any]:
|
|
1707
1842
|
"""Handle system health check."""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcp-ticketer
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
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=Xx4WaprO5PXhVPbYi1L6tBmwmJMkYS-lMyG4ieN6QP0,717
|
|
2
|
-
mcp_ticketer/__version__.py,sha256=
|
|
2
|
+
mcp_ticketer/__version__.py,sha256=GoFXSWeevjribCSU_VeE2nxxgWFRp71QCl67iMs_z50,1117
|
|
3
3
|
mcp_ticketer/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
4
|
mcp_ticketer/adapters/__init__.py,sha256=B5DFllWn23hkhmrLykNO5uMMSdcFuuPHXyLw_jyFzuE,358
|
|
5
5
|
mcp_ticketer/adapters/aitrackdown.py,sha256=stlbge8K6w-EyQkw_vEQNSXQgCOWN5tOlQUgGWZQNMQ,17936
|
|
@@ -16,6 +16,7 @@ mcp_ticketer/adapters/linear/types.py,sha256=VuGPu1Z5jGHtbI2zkCyL5YFnwQNukGW-UV7
|
|
|
16
16
|
mcp_ticketer/cache/__init__.py,sha256=Xcd-cKnt-Cx7jBzvfzUUUPaGkmyXFi5XUFWw3Z4b7d4,138
|
|
17
17
|
mcp_ticketer/cache/memory.py,sha256=2yBqGi9i0SanlUhJoOC7nijWjoMa3_ntPe-V-AV-LfU,5042
|
|
18
18
|
mcp_ticketer/cli/__init__.py,sha256=l9Q8iKmfGkTu0cssHBVqNZTsL4eAtFzOB25AED_0G6g,89
|
|
19
|
+
mcp_ticketer/cli/adapter_diagnostics.py,sha256=t7RyesBRSyJBoVvft2scdNRcugJDIWvqYS9NszPV5ok,14820
|
|
19
20
|
mcp_ticketer/cli/auggie_configure.py,sha256=MXKzLtqe3K_UTQ2GacHAWbvf_B0779KL325smiAKE0Q,8212
|
|
20
21
|
mcp_ticketer/cli/codex_configure.py,sha256=xDppHouT6_-cYXswyAggoPX5bSlRXMvCoM_x9PQ-42A,9086
|
|
21
22
|
mcp_ticketer/cli/configure.py,sha256=BsA_pSHQMQS0t1bJO_wMM8LWsd5sWJDASjEPRHvwC18,16198
|
|
@@ -23,7 +24,7 @@ mcp_ticketer/cli/diagnostics.py,sha256=AC7cMQHVWdHfrYH2Y1tkhmRezM2-mID5E_Dhfil7F
|
|
|
23
24
|
mcp_ticketer/cli/discover.py,sha256=AF_qlQc1Oo0UkWayoF5pmRChS5J3fJjH6f2YZzd_k8w,13188
|
|
24
25
|
mcp_ticketer/cli/gemini_configure.py,sha256=ZNSA1lBW-itVToza-JxW95Po7daVXKiZAh7lp6pmXMU,9343
|
|
25
26
|
mcp_ticketer/cli/linear_commands.py,sha256=b5v4c9uBNPwj_vdy314JkLZbyC1fXU6IcY2VoG0z7gI,17193
|
|
26
|
-
mcp_ticketer/cli/main.py,sha256=
|
|
27
|
+
mcp_ticketer/cli/main.py,sha256=EK2MgphV1Anw_vAtkTl5iC-vzPWIOyu3SV7zOTAY_Vw,72131
|
|
27
28
|
mcp_ticketer/cli/mcp_configure.py,sha256=RzV50UjXgOmvMp-9S0zS39psuvjffVByaMrqrUaAGAM,9594
|
|
28
29
|
mcp_ticketer/cli/migrate_config.py,sha256=MYsr_C5ZxsGg0P13etWTWNrJ_lc6ElRCkzfQADYr3DM,5956
|
|
29
30
|
mcp_ticketer/cli/queue_commands.py,sha256=mm-3H6jmkUGJDyU_E46o9iRpek8tvFCm77F19OtHiZI,7884
|
|
@@ -41,7 +42,7 @@ mcp_ticketer/core/models.py,sha256=a_2AbL3NlN0pfdZad-hXus_zb4bSi9zISHcsNYl0sng,1
|
|
|
41
42
|
mcp_ticketer/core/project_config.py,sha256=yYxlgxjcEPeOwx-b-SXFpe0k9pW9xzBRAK72PsItG-o,23346
|
|
42
43
|
mcp_ticketer/core/registry.py,sha256=ShYLDPE62KFJpB0kj_zFyQzRxSH3LkQEEuo1jaakb1k,3483
|
|
43
44
|
mcp_ticketer/mcp/__init__.py,sha256=Y05eTzsPk0wH8yKNIM-ekpGjgSDO0bQr0EME-vOP4GE,123
|
|
44
|
-
mcp_ticketer/mcp/server.py,sha256=
|
|
45
|
+
mcp_ticketer/mcp/server.py,sha256=PCNerYqLMq5et3ZYAIdd_q_-4LGLBhDuvPAYTuWxDiQ,81351
|
|
45
46
|
mcp_ticketer/queue/__init__.py,sha256=1YIaCpZpFqPcqvDEQXiEvDLiw94DXRdCJkBaVIFQrms,231
|
|
46
47
|
mcp_ticketer/queue/__main__.py,sha256=gc_tE9NUdK07OJfTZuD4t6KeBD_vxFQIhknGTQUG_jk,109
|
|
47
48
|
mcp_ticketer/queue/health_monitor.py,sha256=aQrlBzfbLWu8-fV2b5CuHs4oqyTqGGcntKIHM3r-dDI,11844
|
|
@@ -50,9 +51,9 @@ mcp_ticketer/queue/queue.py,sha256=jSAkYNEIbNH1cbYuF8s6eFuZmXqn8WHXx3mbfMU2Ud8,1
|
|
|
50
51
|
mcp_ticketer/queue/run_worker.py,sha256=F7anuhdkgZF9lXZntHuJ7rEzuEkAfAZO1qvGh3R57bw,1033
|
|
51
52
|
mcp_ticketer/queue/ticket_registry.py,sha256=k8FYg2cFYsI4POb94-o-fTrIVr-ttfi60r0O5YhJYck,15321
|
|
52
53
|
mcp_ticketer/queue/worker.py,sha256=zXJpyhRJ99be0VLaez3YPtC9OU17vVNu5qhr1dCGaLg,19992
|
|
53
|
-
mcp_ticketer-0.
|
|
54
|
-
mcp_ticketer-0.
|
|
55
|
-
mcp_ticketer-0.
|
|
56
|
-
mcp_ticketer-0.
|
|
57
|
-
mcp_ticketer-0.
|
|
58
|
-
mcp_ticketer-0.
|
|
54
|
+
mcp_ticketer-0.3.0.dist-info/licenses/LICENSE,sha256=KOVrunjtILSzY-2N8Lqa3-Q8dMaZIG4LrlLTr9UqL08,1073
|
|
55
|
+
mcp_ticketer-0.3.0.dist-info/METADATA,sha256=qUgh1KFBMCfrmkev4QBVmCEpbCAbutnjGDv5pTrfyJ8,13219
|
|
56
|
+
mcp_ticketer-0.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
57
|
+
mcp_ticketer-0.3.0.dist-info/entry_points.txt,sha256=o1IxVhnHnBNG7FZzbFq-Whcs1Djbofs0qMjiUYBLx2s,60
|
|
58
|
+
mcp_ticketer-0.3.0.dist-info/top_level.txt,sha256=WnAG4SOT1Vm9tIwl70AbGG_nA217YyV3aWFhxLH2rxw,13
|
|
59
|
+
mcp_ticketer-0.3.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|