mcp-ticketer 0.3.0__py3-none-any.whl → 2.2.9__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/__init__.py +10 -10
- mcp_ticketer/__version__.py +3 -3
- mcp_ticketer/_version_scm.py +1 -0
- mcp_ticketer/adapters/__init__.py +2 -0
- mcp_ticketer/adapters/aitrackdown.py +930 -52
- mcp_ticketer/adapters/asana/__init__.py +15 -0
- mcp_ticketer/adapters/asana/adapter.py +1537 -0
- mcp_ticketer/adapters/asana/client.py +292 -0
- mcp_ticketer/adapters/asana/mappers.py +348 -0
- mcp_ticketer/adapters/asana/types.py +146 -0
- mcp_ticketer/adapters/github/__init__.py +26 -0
- mcp_ticketer/adapters/github/adapter.py +3229 -0
- mcp_ticketer/adapters/github/client.py +335 -0
- mcp_ticketer/adapters/github/mappers.py +797 -0
- mcp_ticketer/adapters/github/queries.py +692 -0
- mcp_ticketer/adapters/github/types.py +460 -0
- mcp_ticketer/adapters/hybrid.py +58 -16
- mcp_ticketer/adapters/jira/__init__.py +35 -0
- mcp_ticketer/adapters/jira/adapter.py +1351 -0
- mcp_ticketer/adapters/jira/client.py +271 -0
- mcp_ticketer/adapters/jira/mappers.py +246 -0
- mcp_ticketer/adapters/jira/queries.py +216 -0
- mcp_ticketer/adapters/jira/types.py +304 -0
- mcp_ticketer/adapters/linear/__init__.py +1 -1
- mcp_ticketer/adapters/linear/adapter.py +3810 -462
- mcp_ticketer/adapters/linear/client.py +312 -69
- mcp_ticketer/adapters/linear/mappers.py +305 -85
- mcp_ticketer/adapters/linear/queries.py +317 -17
- mcp_ticketer/adapters/linear/types.py +187 -64
- mcp_ticketer/adapters/linear.py +2 -2
- mcp_ticketer/analysis/__init__.py +56 -0
- mcp_ticketer/analysis/dependency_graph.py +255 -0
- mcp_ticketer/analysis/health_assessment.py +304 -0
- mcp_ticketer/analysis/orphaned.py +218 -0
- mcp_ticketer/analysis/project_status.py +594 -0
- mcp_ticketer/analysis/similarity.py +224 -0
- mcp_ticketer/analysis/staleness.py +266 -0
- mcp_ticketer/automation/__init__.py +11 -0
- mcp_ticketer/automation/project_updates.py +378 -0
- mcp_ticketer/cache/memory.py +9 -8
- mcp_ticketer/cli/adapter_diagnostics.py +91 -54
- mcp_ticketer/cli/auggie_configure.py +116 -15
- mcp_ticketer/cli/codex_configure.py +274 -82
- mcp_ticketer/cli/configure.py +1323 -151
- mcp_ticketer/cli/cursor_configure.py +314 -0
- mcp_ticketer/cli/diagnostics.py +209 -114
- mcp_ticketer/cli/discover.py +297 -26
- mcp_ticketer/cli/gemini_configure.py +119 -26
- mcp_ticketer/cli/init_command.py +880 -0
- mcp_ticketer/cli/install_mcp_server.py +418 -0
- mcp_ticketer/cli/instruction_commands.py +435 -0
- mcp_ticketer/cli/linear_commands.py +256 -130
- mcp_ticketer/cli/main.py +140 -1544
- mcp_ticketer/cli/mcp_configure.py +1013 -100
- mcp_ticketer/cli/mcp_server_commands.py +415 -0
- mcp_ticketer/cli/migrate_config.py +12 -8
- mcp_ticketer/cli/platform_commands.py +123 -0
- mcp_ticketer/cli/platform_detection.py +477 -0
- mcp_ticketer/cli/platform_installer.py +545 -0
- mcp_ticketer/cli/project_update_commands.py +350 -0
- mcp_ticketer/cli/python_detection.py +126 -0
- mcp_ticketer/cli/queue_commands.py +15 -15
- mcp_ticketer/cli/setup_command.py +794 -0
- mcp_ticketer/cli/simple_health.py +84 -59
- mcp_ticketer/cli/ticket_commands.py +1375 -0
- mcp_ticketer/cli/update_checker.py +313 -0
- mcp_ticketer/cli/utils.py +195 -72
- mcp_ticketer/core/__init__.py +64 -1
- mcp_ticketer/core/adapter.py +618 -18
- mcp_ticketer/core/config.py +77 -68
- mcp_ticketer/core/env_discovery.py +75 -16
- mcp_ticketer/core/env_loader.py +121 -97
- mcp_ticketer/core/exceptions.py +32 -24
- mcp_ticketer/core/http_client.py +26 -26
- mcp_ticketer/core/instructions.py +405 -0
- mcp_ticketer/core/label_manager.py +732 -0
- mcp_ticketer/core/mappers.py +42 -30
- mcp_ticketer/core/milestone_manager.py +252 -0
- mcp_ticketer/core/models.py +566 -19
- mcp_ticketer/core/onepassword_secrets.py +379 -0
- mcp_ticketer/core/priority_matcher.py +463 -0
- mcp_ticketer/core/project_config.py +189 -49
- mcp_ticketer/core/project_utils.py +281 -0
- mcp_ticketer/core/project_validator.py +376 -0
- mcp_ticketer/core/registry.py +3 -3
- mcp_ticketer/core/session_state.py +176 -0
- mcp_ticketer/core/state_matcher.py +592 -0
- mcp_ticketer/core/url_parser.py +425 -0
- mcp_ticketer/core/validators.py +69 -0
- mcp_ticketer/defaults/ticket_instructions.md +644 -0
- mcp_ticketer/mcp/__init__.py +29 -1
- mcp_ticketer/mcp/__main__.py +60 -0
- mcp_ticketer/mcp/server/__init__.py +25 -0
- mcp_ticketer/mcp/server/__main__.py +60 -0
- mcp_ticketer/mcp/server/constants.py +58 -0
- mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
- mcp_ticketer/mcp/server/dto.py +195 -0
- mcp_ticketer/mcp/server/main.py +1343 -0
- mcp_ticketer/mcp/server/response_builder.py +206 -0
- mcp_ticketer/mcp/server/routing.py +723 -0
- mcp_ticketer/mcp/server/server_sdk.py +151 -0
- mcp_ticketer/mcp/server/tools/__init__.py +69 -0
- mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
- mcp_ticketer/mcp/server/tools/attachment_tools.py +224 -0
- mcp_ticketer/mcp/server/tools/bulk_tools.py +330 -0
- mcp_ticketer/mcp/server/tools/comment_tools.py +152 -0
- mcp_ticketer/mcp/server/tools/config_tools.py +1564 -0
- mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
- mcp_ticketer/mcp/server/tools/hierarchy_tools.py +942 -0
- mcp_ticketer/mcp/server/tools/instruction_tools.py +295 -0
- mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
- mcp_ticketer/mcp/server/tools/milestone_tools.py +338 -0
- mcp_ticketer/mcp/server/tools/pr_tools.py +150 -0
- mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
- mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
- mcp_ticketer/mcp/server/tools/search_tools.py +318 -0
- mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
- mcp_ticketer/mcp/server/tools/ticket_tools.py +1413 -0
- mcp_ticketer/mcp/server/tools/user_ticket_tools.py +364 -0
- mcp_ticketer/queue/__init__.py +1 -0
- mcp_ticketer/queue/health_monitor.py +168 -136
- mcp_ticketer/queue/manager.py +78 -63
- mcp_ticketer/queue/queue.py +108 -21
- mcp_ticketer/queue/run_worker.py +2 -2
- mcp_ticketer/queue/ticket_registry.py +213 -155
- mcp_ticketer/queue/worker.py +96 -58
- mcp_ticketer/utils/__init__.py +5 -0
- mcp_ticketer/utils/token_utils.py +246 -0
- mcp_ticketer-2.2.9.dist-info/METADATA +1396 -0
- mcp_ticketer-2.2.9.dist-info/RECORD +158 -0
- mcp_ticketer-2.2.9.dist-info/top_level.txt +2 -0
- py_mcp_installer/examples/phase3_demo.py +178 -0
- py_mcp_installer/scripts/manage_version.py +54 -0
- py_mcp_installer/setup.py +6 -0
- py_mcp_installer/src/py_mcp_installer/__init__.py +153 -0
- py_mcp_installer/src/py_mcp_installer/command_builder.py +445 -0
- py_mcp_installer/src/py_mcp_installer/config_manager.py +541 -0
- py_mcp_installer/src/py_mcp_installer/exceptions.py +243 -0
- py_mcp_installer/src/py_mcp_installer/installation_strategy.py +617 -0
- py_mcp_installer/src/py_mcp_installer/installer.py +656 -0
- py_mcp_installer/src/py_mcp_installer/mcp_inspector.py +750 -0
- py_mcp_installer/src/py_mcp_installer/platform_detector.py +451 -0
- py_mcp_installer/src/py_mcp_installer/platforms/__init__.py +26 -0
- py_mcp_installer/src/py_mcp_installer/platforms/claude_code.py +225 -0
- py_mcp_installer/src/py_mcp_installer/platforms/codex.py +181 -0
- py_mcp_installer/src/py_mcp_installer/platforms/cursor.py +191 -0
- py_mcp_installer/src/py_mcp_installer/types.py +222 -0
- py_mcp_installer/src/py_mcp_installer/utils.py +463 -0
- py_mcp_installer/tests/__init__.py +0 -0
- py_mcp_installer/tests/platforms/__init__.py +0 -0
- py_mcp_installer/tests/test_platform_detector.py +17 -0
- mcp_ticketer/adapters/github.py +0 -1354
- mcp_ticketer/adapters/jira.py +0 -1011
- mcp_ticketer/mcp/server.py +0 -2030
- mcp_ticketer-0.3.0.dist-info/METADATA +0 -414
- mcp_ticketer-0.3.0.dist-info/RECORD +0 -59
- mcp_ticketer-0.3.0.dist-info/top_level.txt +0 -1
- {mcp_ticketer-0.3.0.dist-info → mcp_ticketer-2.2.9.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.3.0.dist-info → mcp_ticketer-2.2.9.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.3.0.dist-info → mcp_ticketer-2.2.9.dist-info}/licenses/LICENSE +0 -0
mcp_ticketer/cli/discover.py
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
"""CLI command for auto-discovering configuration from .env files."""
|
|
2
2
|
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
from typing import Optional
|
|
5
4
|
|
|
6
5
|
import typer
|
|
7
6
|
from rich.console import Console
|
|
7
|
+
from rich.panel import Panel
|
|
8
|
+
from rich.table import Table
|
|
8
9
|
|
|
9
10
|
from ..core.env_discovery import DiscoveredAdapter, EnvDiscovery
|
|
11
|
+
from ..core.onepassword_secrets import (
|
|
12
|
+
OnePasswordConfig,
|
|
13
|
+
OnePasswordSecretsLoader,
|
|
14
|
+
check_op_cli_status,
|
|
15
|
+
)
|
|
10
16
|
from ..core.project_config import (
|
|
11
17
|
AdapterConfig,
|
|
12
18
|
ConfigResolver,
|
|
@@ -22,10 +28,12 @@ def _mask_sensitive(value: str, key: str) -> str:
|
|
|
22
28
|
"""Mask sensitive values for display.
|
|
23
29
|
|
|
24
30
|
Args:
|
|
31
|
+
----
|
|
25
32
|
value: Value to potentially mask
|
|
26
33
|
key: Key name to determine if masking needed
|
|
27
34
|
|
|
28
35
|
Returns:
|
|
36
|
+
-------
|
|
29
37
|
Masked or original value
|
|
30
38
|
|
|
31
39
|
"""
|
|
@@ -55,6 +63,7 @@ def _display_discovered_adapter(
|
|
|
55
63
|
"""Display information about a discovered adapter.
|
|
56
64
|
|
|
57
65
|
Args:
|
|
66
|
+
----
|
|
58
67
|
adapter: Discovered adapter to display
|
|
59
68
|
discovery: EnvDiscovery instance for validation
|
|
60
69
|
|
|
@@ -93,7 +102,7 @@ def _display_discovered_adapter(
|
|
|
93
102
|
|
|
94
103
|
@app.command()
|
|
95
104
|
def show(
|
|
96
|
-
project_path:
|
|
105
|
+
project_path: Path | None = typer.Option(
|
|
97
106
|
None,
|
|
98
107
|
"--path",
|
|
99
108
|
"-p",
|
|
@@ -148,7 +157,7 @@ def show(
|
|
|
148
157
|
|
|
149
158
|
@app.command()
|
|
150
159
|
def save(
|
|
151
|
-
adapter:
|
|
160
|
+
adapter: str | None = typer.Option(
|
|
152
161
|
None, "--adapter", "-a", help="Which adapter to save (defaults to recommended)"
|
|
153
162
|
),
|
|
154
163
|
global_config: bool = typer.Option(
|
|
@@ -157,7 +166,7 @@ def save(
|
|
|
157
166
|
dry_run: bool = typer.Option(
|
|
158
167
|
False, "--dry-run", help="Show what would be saved without saving"
|
|
159
168
|
),
|
|
160
|
-
project_path:
|
|
169
|
+
project_path: Path | None = typer.Option(
|
|
161
170
|
None,
|
|
162
171
|
"--path",
|
|
163
172
|
"-p",
|
|
@@ -182,7 +191,7 @@ def save(
|
|
|
182
191
|
console.print(
|
|
183
192
|
"[dim]Make sure your .env file contains adapter credentials[/dim]"
|
|
184
193
|
)
|
|
185
|
-
raise typer.Exit(1)
|
|
194
|
+
raise typer.Exit(1) from None
|
|
186
195
|
|
|
187
196
|
# Determine which adapter to save
|
|
188
197
|
if adapter:
|
|
@@ -192,13 +201,13 @@ def save(
|
|
|
192
201
|
console.print(
|
|
193
202
|
f"[dim]Available: {', '.join(a.adapter_type for a in result.adapters)}[/dim]"
|
|
194
203
|
)
|
|
195
|
-
raise typer.Exit(1)
|
|
204
|
+
raise typer.Exit(1) from None
|
|
196
205
|
else:
|
|
197
206
|
# Use recommended adapter
|
|
198
207
|
discovered_adapter = result.get_primary_adapter()
|
|
199
208
|
if not discovered_adapter:
|
|
200
209
|
console.print("[red]Could not determine recommended adapter[/red]")
|
|
201
|
-
raise typer.Exit(1)
|
|
210
|
+
raise typer.Exit(1) from None
|
|
202
211
|
|
|
203
212
|
console.print(
|
|
204
213
|
f"[bold]Using recommended adapter:[/bold] {discovered_adapter.adapter_type}"
|
|
@@ -217,7 +226,7 @@ def save(
|
|
|
217
226
|
console.print(
|
|
218
227
|
"[dim]Fix the configuration in your .env file and try again[/dim]"
|
|
219
228
|
)
|
|
220
|
-
raise typer.Exit(1)
|
|
229
|
+
raise typer.Exit(1) from None
|
|
221
230
|
|
|
222
231
|
if dry_run:
|
|
223
232
|
console.print("\n[yellow]Dry run - no changes made[/yellow]")
|
|
@@ -240,14 +249,15 @@ def save(
|
|
|
240
249
|
# Add to config
|
|
241
250
|
config.adapters[discovered_adapter.adapter_type] = adapter_config
|
|
242
251
|
|
|
243
|
-
# Save
|
|
252
|
+
# Save (always to project config for security)
|
|
244
253
|
try:
|
|
254
|
+
resolver.save_project_config(config, proj_path)
|
|
255
|
+
config_location = proj_path / resolver.PROJECT_CONFIG_SUBPATH
|
|
256
|
+
|
|
245
257
|
if global_config:
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
resolver.save_project_config(config, proj_path)
|
|
250
|
-
config_location = proj_path / resolver.PROJECT_CONFIG_SUBPATH
|
|
258
|
+
console.print(
|
|
259
|
+
"[yellow]Note: Global config deprecated for security. Saved to project config instead.[/yellow]"
|
|
260
|
+
)
|
|
251
261
|
|
|
252
262
|
console.print(f"\n[green]✅ Configuration saved to:[/green] {config_location}")
|
|
253
263
|
console.print(
|
|
@@ -256,12 +266,12 @@ def save(
|
|
|
256
266
|
|
|
257
267
|
except Exception as e:
|
|
258
268
|
console.print(f"\n[red]Failed to save configuration:[/red] {e}")
|
|
259
|
-
raise typer.Exit(1)
|
|
269
|
+
raise typer.Exit(1) from None
|
|
260
270
|
|
|
261
271
|
|
|
262
272
|
@app.command()
|
|
263
273
|
def interactive(
|
|
264
|
-
project_path:
|
|
274
|
+
project_path: Path | None = typer.Option(
|
|
265
275
|
None,
|
|
266
276
|
"--path",
|
|
267
277
|
"-p",
|
|
@@ -284,7 +294,7 @@ def interactive(
|
|
|
284
294
|
console.print(f" ✅ {env_file}")
|
|
285
295
|
else:
|
|
286
296
|
console.print("[red]No .env files found[/red]")
|
|
287
|
-
raise typer.Exit(1)
|
|
297
|
+
raise typer.Exit(1) from None
|
|
288
298
|
|
|
289
299
|
# Show discovered adapters
|
|
290
300
|
if not result.adapters:
|
|
@@ -292,7 +302,7 @@ def interactive(
|
|
|
292
302
|
console.print(
|
|
293
303
|
"[dim]Make sure your .env file contains adapter credentials[/dim]"
|
|
294
304
|
)
|
|
295
|
-
raise typer.Exit(1)
|
|
305
|
+
raise typer.Exit(1) from None
|
|
296
306
|
|
|
297
307
|
console.print("\n[bold]Detected adapter configurations:[/bold]")
|
|
298
308
|
for i, adapter in enumerate(result.adapters, 1):
|
|
@@ -332,7 +342,7 @@ def interactive(
|
|
|
332
342
|
if choice in [1, 2]:
|
|
333
343
|
if not primary:
|
|
334
344
|
console.print("[red]No recommended adapter found[/red]")
|
|
335
|
-
raise typer.Exit(1)
|
|
345
|
+
raise typer.Exit(1) from None
|
|
336
346
|
adapters_to_save = [primary]
|
|
337
347
|
default_adapter = primary.adapter_type
|
|
338
348
|
elif choice == 3:
|
|
@@ -348,7 +358,7 @@ def interactive(
|
|
|
348
358
|
default_adapter = selected.adapter_type
|
|
349
359
|
else:
|
|
350
360
|
console.print("[red]Invalid choice[/red]")
|
|
351
|
-
raise typer.Exit(1)
|
|
361
|
+
raise typer.Exit(1) from None
|
|
352
362
|
else: # choice == 4
|
|
353
363
|
adapters_to_save = result.adapters
|
|
354
364
|
default_adapter = (
|
|
@@ -389,22 +399,283 @@ def interactive(
|
|
|
389
399
|
|
|
390
400
|
console.print(f" ✅ Added {discovered_adapter.adapter_type}")
|
|
391
401
|
|
|
392
|
-
# Save
|
|
402
|
+
# Save (always to project config for security)
|
|
393
403
|
try:
|
|
404
|
+
resolver.save_project_config(config, proj_path)
|
|
405
|
+
config_location = proj_path / resolver.PROJECT_CONFIG_SUBPATH
|
|
406
|
+
|
|
394
407
|
if save_global:
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
resolver.save_project_config(config, proj_path)
|
|
399
|
-
config_location = proj_path / resolver.PROJECT_CONFIG_SUBPATH
|
|
408
|
+
console.print(
|
|
409
|
+
"[yellow]Note: Global config deprecated for security. Saved to project config instead.[/yellow]"
|
|
410
|
+
)
|
|
400
411
|
|
|
401
412
|
console.print(f"\n[green]✅ Configuration saved to:[/green] {config_location}")
|
|
402
413
|
console.print(f"[green]✅ Default adapter:[/green] {config.default_adapter}")
|
|
403
414
|
|
|
404
415
|
except Exception as e:
|
|
405
416
|
console.print(f"\n[red]Failed to save configuration:[/red] {e}")
|
|
417
|
+
raise typer.Exit(1) from None
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
@app.command(name="1password-status")
|
|
421
|
+
def onepassword_status() -> None:
|
|
422
|
+
"""Check 1Password CLI installation and authentication status."""
|
|
423
|
+
console.print(
|
|
424
|
+
Panel.fit(
|
|
425
|
+
"[bold cyan]1Password CLI Status[/bold cyan]\n"
|
|
426
|
+
"Checking 1Password integration...",
|
|
427
|
+
border_style="cyan",
|
|
428
|
+
)
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
status = check_op_cli_status()
|
|
432
|
+
|
|
433
|
+
# Create status table
|
|
434
|
+
table = Table(title="1Password CLI Status")
|
|
435
|
+
table.add_column("Component", style="cyan")
|
|
436
|
+
table.add_column("Status", style="white")
|
|
437
|
+
|
|
438
|
+
# CLI installed
|
|
439
|
+
if status["installed"]:
|
|
440
|
+
table.add_row(
|
|
441
|
+
"CLI Installed", f"[green]✓ Yes[/green] (version {status['version']})"
|
|
442
|
+
)
|
|
443
|
+
else:
|
|
444
|
+
table.add_row("CLI Installed", "[red]✗ No[/red]")
|
|
445
|
+
console.print(table)
|
|
446
|
+
console.print(
|
|
447
|
+
"\n[yellow]Install 1Password CLI:[/yellow]\n"
|
|
448
|
+
" macOS: brew install 1password-cli\n"
|
|
449
|
+
" Linux: See https://developer.1password.com/docs/cli/get-started/\n"
|
|
450
|
+
" Windows: See https://developer.1password.com/docs/cli/get-started/"
|
|
451
|
+
)
|
|
452
|
+
return
|
|
453
|
+
|
|
454
|
+
# Authentication
|
|
455
|
+
if status["authenticated"]:
|
|
456
|
+
table.add_row("Authentication", "[green]✓ Signed in[/green]")
|
|
457
|
+
|
|
458
|
+
# Show accounts
|
|
459
|
+
if status["accounts"]:
|
|
460
|
+
for account in status["accounts"]:
|
|
461
|
+
account_url = account.get("url", "N/A")
|
|
462
|
+
account_email = account.get("email", "N/A")
|
|
463
|
+
table.add_row(" Account", f"{account_email} ({account_url})")
|
|
464
|
+
else:
|
|
465
|
+
table.add_row("Authentication", "[yellow]⚠ Not signed in[/yellow]")
|
|
466
|
+
|
|
467
|
+
console.print(table)
|
|
468
|
+
|
|
469
|
+
if not status["authenticated"]:
|
|
470
|
+
console.print("\n[yellow]Sign in to 1Password:[/yellow]\n" " Run: op signin\n")
|
|
471
|
+
else:
|
|
472
|
+
console.print(
|
|
473
|
+
"\n[green]✓ 1Password CLI is ready to use![/green]\n\n"
|
|
474
|
+
"You can now use .env files with op:// secret references.\n"
|
|
475
|
+
"Run 'mcp-ticketer discover 1password-template' to create template files."
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
@app.command(name="1password-template")
|
|
480
|
+
def onepassword_template(
|
|
481
|
+
adapter: str = typer.Argument(
|
|
482
|
+
...,
|
|
483
|
+
help="Adapter type (linear, github, jira, aitrackdown)",
|
|
484
|
+
),
|
|
485
|
+
vault: str = typer.Option(
|
|
486
|
+
"Development",
|
|
487
|
+
"--vault",
|
|
488
|
+
"-v",
|
|
489
|
+
help="1Password vault name for secret references",
|
|
490
|
+
),
|
|
491
|
+
item: str | None = typer.Option(
|
|
492
|
+
None,
|
|
493
|
+
"--item",
|
|
494
|
+
"-i",
|
|
495
|
+
help="1Password item name (defaults to adapter name)",
|
|
496
|
+
),
|
|
497
|
+
output: Path | None = typer.Option(
|
|
498
|
+
None,
|
|
499
|
+
"--output",
|
|
500
|
+
"-o",
|
|
501
|
+
help="Output file path (defaults to .env.1password)",
|
|
502
|
+
),
|
|
503
|
+
) -> None:
|
|
504
|
+
"""Create a .env template file with 1Password secret references.
|
|
505
|
+
|
|
506
|
+
This creates a template file that uses op:// secret references,
|
|
507
|
+
which can be used with: op run --env-file=.env.1password -- <command>
|
|
508
|
+
|
|
509
|
+
Examples:
|
|
510
|
+
--------
|
|
511
|
+
# Create Linear template
|
|
512
|
+
mcp-ticketer discover 1password-template linear
|
|
513
|
+
|
|
514
|
+
# Create GitHub template with custom vault
|
|
515
|
+
mcp-ticketer discover 1password-template github --vault=Production
|
|
516
|
+
|
|
517
|
+
# Create template with custom item name
|
|
518
|
+
mcp-ticketer discover 1password-template jira --item="JIRA API Keys"
|
|
519
|
+
|
|
520
|
+
"""
|
|
521
|
+
# Check if op CLI is available
|
|
522
|
+
status = check_op_cli_status()
|
|
523
|
+
if not status["installed"]:
|
|
524
|
+
console.print(
|
|
525
|
+
"[red]1Password CLI not installed.[/red]\n\n"
|
|
526
|
+
"Install it first:\n"
|
|
527
|
+
" macOS: brew install 1password-cli\n"
|
|
528
|
+
" Other: https://developer.1password.com/docs/cli/get-started/"
|
|
529
|
+
)
|
|
406
530
|
raise typer.Exit(1)
|
|
407
531
|
|
|
532
|
+
# Set default output path
|
|
533
|
+
if output is None:
|
|
534
|
+
output = Path(f".env.1password.{adapter.lower()}")
|
|
535
|
+
|
|
536
|
+
# Create loader and generate template
|
|
537
|
+
loader = OnePasswordSecretsLoader(OnePasswordConfig())
|
|
538
|
+
loader.create_template_file(output, adapter, vault, item)
|
|
539
|
+
|
|
540
|
+
console.print(
|
|
541
|
+
Panel.fit(
|
|
542
|
+
f"[bold green]✓ Template created![/bold green]\n\n"
|
|
543
|
+
f"File: {output}\n"
|
|
544
|
+
f"Vault: {vault}\n"
|
|
545
|
+
f"Item: {item or adapter.upper()}\n\n"
|
|
546
|
+
f"[bold]Next steps:[/bold]\n"
|
|
547
|
+
f"1. Create item '{item or adapter.upper()}' in 1Password vault '{vault}'\n"
|
|
548
|
+
f"2. Add the required fields to the item\n"
|
|
549
|
+
f"3. Test with: op run --env-file={output} -- mcp-ticketer discover show\n"
|
|
550
|
+
f"4. Save config: op run --env-file={output} -- mcp-ticketer discover save",
|
|
551
|
+
border_style="green",
|
|
552
|
+
)
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
# Show template contents
|
|
556
|
+
console.print("\n[bold]Template contents:[/bold]\n")
|
|
557
|
+
console.print(Panel(output.read_text(), border_style="dim"))
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
@app.command(name="1password-test")
|
|
561
|
+
def onepassword_test(
|
|
562
|
+
env_file: Path = typer.Option(
|
|
563
|
+
".env.1password",
|
|
564
|
+
"--file",
|
|
565
|
+
"-f",
|
|
566
|
+
help="Path to .env file with op:// references",
|
|
567
|
+
),
|
|
568
|
+
) -> None:
|
|
569
|
+
"""Test 1Password secret resolution from .env file.
|
|
570
|
+
|
|
571
|
+
This command loads secrets from the specified .env file and
|
|
572
|
+
displays the resolved values (with sensitive data masked).
|
|
573
|
+
|
|
574
|
+
Example:
|
|
575
|
+
-------
|
|
576
|
+
mcp-ticketer discover 1password-test --file=.env.1password.linear
|
|
577
|
+
|
|
578
|
+
"""
|
|
579
|
+
# Check if file exists
|
|
580
|
+
if not env_file.exists():
|
|
581
|
+
console.print(f"[red]File not found:[/red] {env_file}")
|
|
582
|
+
raise typer.Exit(1)
|
|
583
|
+
|
|
584
|
+
# Check if op CLI is available and authenticated
|
|
585
|
+
status = check_op_cli_status()
|
|
586
|
+
if not status["installed"]:
|
|
587
|
+
console.print("[red]1Password CLI not installed.[/red]")
|
|
588
|
+
raise typer.Exit(1)
|
|
589
|
+
|
|
590
|
+
if not status["authenticated"]:
|
|
591
|
+
console.print(
|
|
592
|
+
"[red]1Password CLI not authenticated.[/red]\n\n" "Run: op signin"
|
|
593
|
+
)
|
|
594
|
+
raise typer.Exit(1)
|
|
595
|
+
|
|
596
|
+
console.print(
|
|
597
|
+
Panel.fit(
|
|
598
|
+
f"[bold cyan]Testing 1Password Secret Resolution[/bold cyan]\n"
|
|
599
|
+
f"File: {env_file}",
|
|
600
|
+
border_style="cyan",
|
|
601
|
+
)
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
# Load secrets
|
|
605
|
+
loader = OnePasswordSecretsLoader(OnePasswordConfig())
|
|
606
|
+
|
|
607
|
+
try:
|
|
608
|
+
secrets = loader.load_secrets_from_env_file(env_file)
|
|
609
|
+
|
|
610
|
+
# Display resolved secrets
|
|
611
|
+
table = Table(title="Resolved Secrets")
|
|
612
|
+
table.add_column("Variable", style="cyan")
|
|
613
|
+
table.add_column("Value", style="green")
|
|
614
|
+
|
|
615
|
+
for key, value in secrets.items():
|
|
616
|
+
# Mask sensitive values
|
|
617
|
+
display_value = _mask_sensitive(value, key)
|
|
618
|
+
table.add_row(key, display_value)
|
|
619
|
+
|
|
620
|
+
console.print(table)
|
|
621
|
+
|
|
622
|
+
console.print(
|
|
623
|
+
f"\n[green]✓ Successfully resolved {len(secrets)} secrets![/green]"
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
# Test discovery with these secrets
|
|
627
|
+
console.print("\n[bold]Testing configuration discovery...[/bold]")
|
|
628
|
+
EnvDiscovery(enable_1password=False) # Already resolved
|
|
629
|
+
|
|
630
|
+
# Temporarily write resolved secrets to test discovery
|
|
631
|
+
import tempfile
|
|
632
|
+
|
|
633
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".env", delete=False) as tmp:
|
|
634
|
+
for key, value in secrets.items():
|
|
635
|
+
tmp.write(f"{key}={value}\n")
|
|
636
|
+
tmp_path = Path(tmp.name)
|
|
637
|
+
|
|
638
|
+
try:
|
|
639
|
+
# Mock the env file loading by directly providing secrets
|
|
640
|
+
from ..core.env_discovery import DiscoveryResult
|
|
641
|
+
|
|
642
|
+
DiscoveryResult()
|
|
643
|
+
|
|
644
|
+
# Try to detect adapters from the resolved secrets
|
|
645
|
+
from ..core.env_discovery import EnvDiscovery as ED
|
|
646
|
+
|
|
647
|
+
ed = ED(enable_1password=False)
|
|
648
|
+
ed.project_path = Path.cwd()
|
|
649
|
+
|
|
650
|
+
# Manually detect from secrets dict
|
|
651
|
+
linear_adapter = ed._detect_linear(secrets, str(env_file))
|
|
652
|
+
if linear_adapter:
|
|
653
|
+
console.print("\n[green]✓ Detected Linear configuration[/green]")
|
|
654
|
+
_display_discovered_adapter(linear_adapter, ed)
|
|
655
|
+
|
|
656
|
+
github_adapter = ed._detect_github(secrets, str(env_file))
|
|
657
|
+
if github_adapter:
|
|
658
|
+
console.print("\n[green]✓ Detected GitHub configuration[/green]")
|
|
659
|
+
_display_discovered_adapter(github_adapter, ed)
|
|
660
|
+
|
|
661
|
+
jira_adapter = ed._detect_jira(secrets, str(env_file))
|
|
662
|
+
if jira_adapter:
|
|
663
|
+
console.print("\n[green]✓ Detected JIRA configuration[/green]")
|
|
664
|
+
_display_discovered_adapter(jira_adapter, ed)
|
|
665
|
+
finally:
|
|
666
|
+
tmp_path.unlink()
|
|
667
|
+
|
|
668
|
+
except Exception as e:
|
|
669
|
+
console.print(f"\n[red]Failed to resolve secrets:[/red] {e}")
|
|
670
|
+
console.print(
|
|
671
|
+
"\n[yellow]Troubleshooting:[/yellow]\n"
|
|
672
|
+
"1. Check that the item exists in 1Password\n"
|
|
673
|
+
"2. Verify the vault name is correct\n"
|
|
674
|
+
"3. Ensure all field names match\n"
|
|
675
|
+
f"4. Run: op inject --in-file={env_file} (to see detailed errors)"
|
|
676
|
+
)
|
|
677
|
+
raise typer.Exit(1) from None
|
|
678
|
+
|
|
408
679
|
|
|
409
680
|
if __name__ == "__main__":
|
|
410
681
|
app()
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from typing import Literal
|
|
5
|
+
from typing import Literal
|
|
6
6
|
|
|
7
7
|
from rich.console import Console
|
|
8
8
|
|
|
9
|
-
from .mcp_configure import
|
|
9
|
+
from .mcp_configure import load_project_config
|
|
10
|
+
from .python_detection import get_mcp_ticketer_python
|
|
10
11
|
|
|
11
12
|
console = Console()
|
|
12
13
|
|
|
@@ -73,19 +74,26 @@ def save_gemini_config(config_path: Path, config: dict) -> None:
|
|
|
73
74
|
|
|
74
75
|
|
|
75
76
|
def create_gemini_server_config(
|
|
76
|
-
|
|
77
|
+
python_path: str, project_config: dict, project_path: str | None = None
|
|
77
78
|
) -> dict:
|
|
78
79
|
"""Create Gemini MCP server configuration for mcp-ticketer.
|
|
79
80
|
|
|
81
|
+
Uses the CLI command (mcp-ticketer mcp) which implements proper
|
|
82
|
+
Content-Length framing via FastMCP SDK, required for modern MCP clients.
|
|
83
|
+
|
|
80
84
|
Args:
|
|
81
|
-
|
|
85
|
+
python_path: Path to Python executable in mcp-ticketer venv
|
|
82
86
|
project_config: Project configuration from .mcp-ticketer/config.json
|
|
83
|
-
|
|
87
|
+
project_path: Project directory path (optional)
|
|
84
88
|
|
|
85
89
|
Returns:
|
|
86
90
|
Gemini MCP server configuration dict
|
|
87
91
|
|
|
88
92
|
"""
|
|
93
|
+
# IMPORTANT: Use CLI command, NOT Python module invocation
|
|
94
|
+
# The CLI uses FastMCP SDK which implements proper Content-Length framing
|
|
95
|
+
# Legacy python -m mcp_ticketer.mcp.server uses line-delimited JSON (incompatible)
|
|
96
|
+
|
|
89
97
|
# Get adapter configuration
|
|
90
98
|
adapter = project_config.get("default_adapter", "aitrackdown")
|
|
91
99
|
adapters_config = project_config.get("adapters", {})
|
|
@@ -94,9 +102,9 @@ def create_gemini_server_config(
|
|
|
94
102
|
# Build environment variables
|
|
95
103
|
env_vars = {}
|
|
96
104
|
|
|
97
|
-
# Add PYTHONPATH
|
|
98
|
-
if
|
|
99
|
-
env_vars["PYTHONPATH"] =
|
|
105
|
+
# Add PYTHONPATH for project context
|
|
106
|
+
if project_path:
|
|
107
|
+
env_vars["PYTHONPATH"] = project_path
|
|
100
108
|
|
|
101
109
|
# Add adapter type
|
|
102
110
|
env_vars["MCP_TICKETER_ADAPTER"] = adapter
|
|
@@ -105,9 +113,9 @@ def create_gemini_server_config(
|
|
|
105
113
|
if adapter == "aitrackdown":
|
|
106
114
|
# Set base path for local adapter
|
|
107
115
|
base_path = adapter_config.get("base_path", ".aitrackdown")
|
|
108
|
-
if
|
|
109
|
-
# Use absolute path if
|
|
110
|
-
env_vars["MCP_TICKETER_BASE_PATH"] = str(Path(
|
|
116
|
+
if project_path:
|
|
117
|
+
# Use absolute path if project_path is provided
|
|
118
|
+
env_vars["MCP_TICKETER_BASE_PATH"] = str(Path(project_path) / base_path)
|
|
111
119
|
else:
|
|
112
120
|
env_vars["MCP_TICKETER_BASE_PATH"] = base_path
|
|
113
121
|
|
|
@@ -135,10 +143,22 @@ def create_gemini_server_config(
|
|
|
135
143
|
if "project_key" in adapter_config:
|
|
136
144
|
env_vars["JIRA_PROJECT_KEY"] = adapter_config["project_key"]
|
|
137
145
|
|
|
146
|
+
# Get mcp-ticketer CLI path from Python path
|
|
147
|
+
# If python_path is /path/to/venv/bin/python, CLI is /path/to/venv/bin/mcp-ticketer
|
|
148
|
+
python_dir = Path(python_path).parent
|
|
149
|
+
cli_path = str(python_dir / "mcp-ticketer")
|
|
150
|
+
|
|
151
|
+
# Build CLI arguments
|
|
152
|
+
args = ["mcp"]
|
|
153
|
+
if project_path:
|
|
154
|
+
args.extend(["--path", project_path])
|
|
155
|
+
|
|
138
156
|
# Create server configuration with Gemini-specific options
|
|
157
|
+
# NOTE: Environment variables below are optional fallbacks
|
|
158
|
+
# The CLI loads config from .mcp-ticketer/config.json
|
|
139
159
|
config = {
|
|
140
|
-
"command":
|
|
141
|
-
"args":
|
|
160
|
+
"command": cli_path,
|
|
161
|
+
"args": args,
|
|
142
162
|
"env": env_vars,
|
|
143
163
|
"timeout": 15000, # 15 seconds timeout
|
|
144
164
|
"trust": False, # Don't trust by default (security)
|
|
@@ -147,6 +167,72 @@ def create_gemini_server_config(
|
|
|
147
167
|
return config
|
|
148
168
|
|
|
149
169
|
|
|
170
|
+
def remove_gemini_mcp(
|
|
171
|
+
scope: Literal["project", "user"] = "project", dry_run: bool = False
|
|
172
|
+
) -> None:
|
|
173
|
+
"""Remove mcp-ticketer from Gemini CLI configuration.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
scope: Configuration scope - "project" or "user"
|
|
177
|
+
dry_run: Show what would be removed without making changes
|
|
178
|
+
|
|
179
|
+
"""
|
|
180
|
+
# Step 1: Find Gemini config location
|
|
181
|
+
config_type = "user-level" if scope == "user" else "project-level"
|
|
182
|
+
console.print(f"[cyan]🔍 Removing {config_type} Gemini CLI configuration...[/cyan]")
|
|
183
|
+
|
|
184
|
+
gemini_config_path = find_gemini_config(scope)
|
|
185
|
+
console.print(f"[dim]Config location: {gemini_config_path}[/dim]")
|
|
186
|
+
|
|
187
|
+
# Step 2: Check if config file exists
|
|
188
|
+
if not gemini_config_path.exists():
|
|
189
|
+
console.print(
|
|
190
|
+
f"[yellow]⚠ No configuration found at {gemini_config_path}[/yellow]"
|
|
191
|
+
)
|
|
192
|
+
console.print("[dim]mcp-ticketer is not configured for Gemini CLI[/dim]")
|
|
193
|
+
return
|
|
194
|
+
|
|
195
|
+
# Step 3: Load existing Gemini configuration
|
|
196
|
+
gemini_config = load_gemini_config(gemini_config_path)
|
|
197
|
+
|
|
198
|
+
# Step 4: Check if mcp-ticketer is configured
|
|
199
|
+
if "mcp-ticketer" not in gemini_config.get("mcpServers", {}):
|
|
200
|
+
console.print("[yellow]⚠ mcp-ticketer is not configured[/yellow]")
|
|
201
|
+
console.print(f"[dim]No mcp-ticketer entry found in {gemini_config_path}[/dim]")
|
|
202
|
+
return
|
|
203
|
+
|
|
204
|
+
# Step 5: Show what would be removed (dry run or actual removal)
|
|
205
|
+
if dry_run:
|
|
206
|
+
console.print("\n[cyan]DRY RUN - Would remove:[/cyan]")
|
|
207
|
+
console.print(" Server name: mcp-ticketer")
|
|
208
|
+
console.print(f" From: {gemini_config_path}")
|
|
209
|
+
console.print(f" Scope: {config_type}")
|
|
210
|
+
return
|
|
211
|
+
|
|
212
|
+
# Step 6: Remove mcp-ticketer from configuration
|
|
213
|
+
del gemini_config["mcpServers"]["mcp-ticketer"]
|
|
214
|
+
|
|
215
|
+
# Step 7: Save updated configuration
|
|
216
|
+
try:
|
|
217
|
+
save_gemini_config(gemini_config_path, gemini_config)
|
|
218
|
+
console.print("\n[green]✓ Successfully removed mcp-ticketer[/green]")
|
|
219
|
+
console.print(f"[dim]Configuration updated: {gemini_config_path}[/dim]")
|
|
220
|
+
|
|
221
|
+
# Next steps
|
|
222
|
+
console.print("\n[bold cyan]Next Steps:[/bold cyan]")
|
|
223
|
+
if scope == "user":
|
|
224
|
+
console.print("1. Gemini CLI global configuration updated")
|
|
225
|
+
console.print("2. mcp-ticketer will no longer be available in any project")
|
|
226
|
+
else:
|
|
227
|
+
console.print("1. Gemini CLI project configuration updated")
|
|
228
|
+
console.print("2. mcp-ticketer will no longer be available in this project")
|
|
229
|
+
console.print("3. Restart Gemini CLI if currently running")
|
|
230
|
+
|
|
231
|
+
except Exception as e:
|
|
232
|
+
console.print(f"\n[red]✗ Failed to update configuration:[/red] {e}")
|
|
233
|
+
raise
|
|
234
|
+
|
|
235
|
+
|
|
150
236
|
def configure_gemini_mcp(
|
|
151
237
|
scope: Literal["project", "user"] = "project", force: bool = False
|
|
152
238
|
) -> None:
|
|
@@ -157,18 +243,22 @@ def configure_gemini_mcp(
|
|
|
157
243
|
force: Overwrite existing configuration
|
|
158
244
|
|
|
159
245
|
Raises:
|
|
160
|
-
FileNotFoundError: If
|
|
246
|
+
FileNotFoundError: If Python executable or project config not found
|
|
161
247
|
ValueError: If configuration is invalid
|
|
162
248
|
|
|
163
249
|
"""
|
|
164
|
-
# Step 1: Find
|
|
165
|
-
console.print("[cyan]🔍 Finding mcp-ticketer
|
|
250
|
+
# Step 1: Find Python executable
|
|
251
|
+
console.print("[cyan]🔍 Finding mcp-ticketer Python executable...[/cyan]")
|
|
166
252
|
try:
|
|
167
|
-
|
|
168
|
-
console.print(f"[green]✓[/green] Found: {
|
|
169
|
-
except
|
|
170
|
-
console.print(f"[red]✗[/red] {e}")
|
|
171
|
-
raise
|
|
253
|
+
python_path = get_mcp_ticketer_python()
|
|
254
|
+
console.print(f"[green]✓[/green] Found: {python_path}")
|
|
255
|
+
except Exception as e:
|
|
256
|
+
console.print(f"[red]✗[/red] Could not find Python executable: {e}")
|
|
257
|
+
raise FileNotFoundError(
|
|
258
|
+
"Could not find mcp-ticketer Python executable. "
|
|
259
|
+
"Please ensure mcp-ticketer is installed.\n"
|
|
260
|
+
"Install with: pip install mcp-ticketer or pipx install mcp-ticketer"
|
|
261
|
+
) from e
|
|
172
262
|
|
|
173
263
|
# Step 2: Load project configuration
|
|
174
264
|
console.print("\n[cyan]📖 Reading project configuration...[/cyan]")
|
|
@@ -200,9 +290,11 @@ def configure_gemini_mcp(
|
|
|
200
290
|
console.print("[yellow]⚠ Overwriting existing configuration[/yellow]")
|
|
201
291
|
|
|
202
292
|
# Step 6: Create mcp-ticketer server config
|
|
203
|
-
|
|
293
|
+
project_path = str(Path.cwd()) if scope == "project" else None
|
|
204
294
|
server_config = create_gemini_server_config(
|
|
205
|
-
|
|
295
|
+
python_path=python_path,
|
|
296
|
+
project_config=project_config,
|
|
297
|
+
project_path=project_path,
|
|
206
298
|
)
|
|
207
299
|
|
|
208
300
|
# Step 7: Update Gemini configuration
|
|
@@ -221,11 +313,12 @@ def configure_gemini_mcp(
|
|
|
221
313
|
console.print("\n[bold]Configuration Details:[/bold]")
|
|
222
314
|
console.print(" Server name: mcp-ticketer")
|
|
223
315
|
console.print(f" Adapter: {adapter}")
|
|
224
|
-
console.print(f"
|
|
316
|
+
console.print(f" Python: {python_path}")
|
|
317
|
+
console.print(" Command: python -m mcp_ticketer.mcp.server")
|
|
225
318
|
console.print(f" Timeout: {server_config['timeout']}ms")
|
|
226
319
|
console.print(f" Trust: {server_config['trust']}")
|
|
227
|
-
if
|
|
228
|
-
console.print(f"
|
|
320
|
+
if project_path:
|
|
321
|
+
console.print(f" Project path: {project_path}")
|
|
229
322
|
if "env" in server_config:
|
|
230
323
|
console.print(
|
|
231
324
|
f" Environment variables: {list(server_config['env'].keys())}"
|