open-edison 0.1.36__tar.gz → 0.1.37__tar.gz

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.
Files changed (55) hide show
  1. {open_edison-0.1.36 → open_edison-0.1.37}/PKG-INFO +48 -2
  2. {open_edison-0.1.36 → open_edison-0.1.37}/README.md +47 -1
  3. {open_edison-0.1.36 → open_edison-0.1.37}/pyproject.toml +3 -1
  4. open_edison-0.1.37/src/__init__.py +8 -0
  5. open_edison-0.1.37/src/mcp_importer/__init__.py +15 -0
  6. open_edison-0.1.37/src/mcp_importer/__main__.py +19 -0
  7. open_edison-0.1.37/src/mcp_importer/api.py +106 -0
  8. open_edison-0.1.37/src/mcp_importer/cli.py +113 -0
  9. open_edison-0.1.37/src/mcp_importer/export_cli.py +201 -0
  10. open_edison-0.1.37/src/mcp_importer/exporters.py +393 -0
  11. open_edison-0.1.37/src/mcp_importer/import_api.py +3 -0
  12. open_edison-0.1.37/src/mcp_importer/importers.py +63 -0
  13. open_edison-0.1.37/src/mcp_importer/merge.py +47 -0
  14. open_edison-0.1.37/src/mcp_importer/parsers.py +148 -0
  15. open_edison-0.1.37/src/mcp_importer/paths.py +92 -0
  16. open_edison-0.1.37/src/mcp_importer/quick_cli.py +65 -0
  17. open_edison-0.1.37/src/mcp_importer/types.py +5 -0
  18. open_edison-0.1.36/src/__init__.py +0 -11
  19. {open_edison-0.1.36 → open_edison-0.1.37}/.gitignore +0 -0
  20. {open_edison-0.1.36 → open_edison-0.1.37}/LICENSE +0 -0
  21. {open_edison-0.1.36 → open_edison-0.1.37}/config.json +0 -0
  22. {open_edison-0.1.36 → open_edison-0.1.37}/desktop_ext/README.md +0 -0
  23. {open_edison-0.1.36 → open_edison-0.1.37}/docs/README.md +0 -0
  24. {open_edison-0.1.36 → open_edison-0.1.37}/docs/architecture/single_user_design.md +0 -0
  25. {open_edison-0.1.36 → open_edison-0.1.37}/docs/core/configuration.md +0 -0
  26. {open_edison-0.1.36 → open_edison-0.1.37}/docs/core/project_structure.md +0 -0
  27. {open_edison-0.1.36 → open_edison-0.1.37}/docs/core/proxy_usage.md +0 -0
  28. {open_edison-0.1.36 → open_edison-0.1.37}/docs/deployment/docker.md +0 -0
  29. {open_edison-0.1.36 → open_edison-0.1.37}/docs/deployment/local.md +0 -0
  30. {open_edison-0.1.36 → open_edison-0.1.37}/docs/development/contributing.md +0 -0
  31. {open_edison-0.1.36 → open_edison-0.1.37}/docs/development/development_guide.md +0 -0
  32. {open_edison-0.1.36 → open_edison-0.1.37}/docs/development/testing.md +0 -0
  33. {open_edison-0.1.36 → open_edison-0.1.37}/docs/quick-reference/api_reference.md +0 -0
  34. {open_edison-0.1.36 → open_edison-0.1.37}/docs/quick-reference/config_quick_start.md +0 -0
  35. {open_edison-0.1.36 → open_edison-0.1.37}/hatch_build.py +0 -0
  36. {open_edison-0.1.36 → open_edison-0.1.37}/installation_test/README.md +0 -0
  37. {open_edison-0.1.36 → open_edison-0.1.37}/prompt_permissions.json +0 -0
  38. {open_edison-0.1.36 → open_edison-0.1.37}/resource_permissions.json +0 -0
  39. {open_edison-0.1.36 → open_edison-0.1.37}/src/__main__.py +0 -0
  40. {open_edison-0.1.36 → open_edison-0.1.37}/src/cli.py +0 -0
  41. {open_edison-0.1.36 → open_edison-0.1.37}/src/config.py +0 -0
  42. {open_edison-0.1.36 → open_edison-0.1.37}/src/config.pyi +0 -0
  43. {open_edison-0.1.36 → open_edison-0.1.37}/src/events.py +0 -0
  44. {open_edison-0.1.36 → open_edison-0.1.37}/src/frontend_dist/assets/index-BUUcUfTt.js +0 -0
  45. {open_edison-0.1.36 → open_edison-0.1.37}/src/frontend_dist/assets/index-o6_8mdM8.css +0 -0
  46. {open_edison-0.1.36 → open_edison-0.1.37}/src/frontend_dist/index.html +0 -0
  47. {open_edison-0.1.36 → open_edison-0.1.37}/src/frontend_dist/sw.js +0 -0
  48. {open_edison-0.1.36 → open_edison-0.1.37}/src/middleware/data_access_tracker.py +0 -0
  49. {open_edison-0.1.36 → open_edison-0.1.37}/src/middleware/session_tracking.py +0 -0
  50. {open_edison-0.1.36 → open_edison-0.1.37}/src/oauth_manager.py +0 -0
  51. {open_edison-0.1.36 → open_edison-0.1.37}/src/permissions.py +0 -0
  52. {open_edison-0.1.36 → open_edison-0.1.37}/src/server.py +0 -0
  53. {open_edison-0.1.36 → open_edison-0.1.37}/src/single_user_mcp.py +0 -0
  54. {open_edison-0.1.36 → open_edison-0.1.37}/src/telemetry.py +0 -0
  55. {open_edison-0.1.36 → open_edison-0.1.37}/tool_permissions.json +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: open-edison
3
- Version: 0.1.36
3
+ Version: 0.1.37
4
4
  Summary: Open-source MCP security, aggregation, and monitoring. Single-user, self-hosted MCP proxy.
5
5
  Author-email: Hugo Berg <hugo@edison.watch>
6
6
  License-File: LICENSE
@@ -26,7 +26,7 @@ Description-Content-Type: text/markdown
26
26
 
27
27
  > The Secure MCP Control Panel
28
28
 
29
- Connect AI to your data/software securely without risk of data exfiltration. Gain visibility, block threats, and get alerts on the data your agent is reading/writing.
29
+ Connect AI to your data/software securely without risk of data exfiltration. Gain visibility, block threats, and get alerts on the data your agent is reading/writing.
30
30
 
31
31
  OpenEdison solves the [lethal trifecta problem](https://simonwillison.net/2025/Jun/16/the-lethal-trifecta/), which can cause agent hijacking & data exfiltration by malicious actors.
32
32
 
@@ -72,6 +72,13 @@ curl -fsSL https://raw.githubusercontent.com/Edison-Watch/open-edison/main/curl_
72
72
 
73
73
  Run locally with uvx: `uvx open-edison`
74
74
 
75
+ Optionally, import your existing MCP configs from Cursor, VS Code, or Claude Code with:
76
+
77
+ ```bash
78
+ # From source (no install) — quick one-liner (add --dry-run to preview)
79
+ uv run python -m mcp_importer.quick_cli --yes
80
+ ```
81
+
75
82
  <details>
76
83
  <summary>⬇️ Install Node.js/npm (optional for MCP tools)</summary>
77
84
 
@@ -121,6 +128,45 @@ OPEN_EDISON_CONFIG_DIR=~/edison-config open-edison run
121
128
 
122
129
  </details>
123
130
 
131
+ <details>
132
+ <summary>🔄 Import from Cursor/VS Code/Claude Code</summary>
133
+
134
+ ### Import from Cursor/VS Code/Claude Code
135
+
136
+ - **CLI**
137
+
138
+ - Import & configure to use edison as your MCP server:
139
+
140
+ ```bash
141
+ # From source (no install)
142
+ uv run python -m mcp_importer.quick_cli --yes
143
+ # After install: mcp-importer-quick --yes
144
+ ```
145
+
146
+ - Preview what will be imported (no writes):
147
+
148
+ ```bash
149
+ uv run python -m mcp_importer --source cursor --dry-run
150
+ ```
151
+
152
+ - Import servers into Open Edison `config.json` (merge policy defaults to `skip`):
153
+
154
+ ```bash
155
+ uv run python -m mcp_importer --source cursor
156
+ uv run python -m mcp_importer --source vscode
157
+ uv run python -m mcp_importer --source claude-code
158
+ ```
159
+
160
+ - Point your editor to Open Edison (backup original config and replace with a single Open Edison server):
161
+
162
+ ```bash
163
+ uv run python -m mcp_importer export --target cursor --yes
164
+ uv run python -m mcp_importer export --target vscode --yes
165
+ uv run python -m mcp_importer export --target claude-code --yes
166
+ ```
167
+
168
+ </details>
169
+
124
170
  <details>
125
171
  <summary><img src="https://img.shields.io/badge/Docker-2CA5E0?style=for-the-badge&logo=docker&logoColor=white" alt="Docker"> Run with Docker</summary>
126
172
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  > The Secure MCP Control Panel
4
4
 
5
- Connect AI to your data/software securely without risk of data exfiltration. Gain visibility, block threats, and get alerts on the data your agent is reading/writing.
5
+ Connect AI to your data/software securely without risk of data exfiltration. Gain visibility, block threats, and get alerts on the data your agent is reading/writing.
6
6
 
7
7
  OpenEdison solves the [lethal trifecta problem](https://simonwillison.net/2025/Jun/16/the-lethal-trifecta/), which can cause agent hijacking & data exfiltration by malicious actors.
8
8
 
@@ -48,6 +48,13 @@ curl -fsSL https://raw.githubusercontent.com/Edison-Watch/open-edison/main/curl_
48
48
 
49
49
  Run locally with uvx: `uvx open-edison`
50
50
 
51
+ Optionally, import your existing MCP configs from Cursor, VS Code, or Claude Code with:
52
+
53
+ ```bash
54
+ # From source (no install) — quick one-liner (add --dry-run to preview)
55
+ uv run python -m mcp_importer.quick_cli --yes
56
+ ```
57
+
51
58
  <details>
52
59
  <summary>⬇️ Install Node.js/npm (optional for MCP tools)</summary>
53
60
 
@@ -97,6 +104,45 @@ OPEN_EDISON_CONFIG_DIR=~/edison-config open-edison run
97
104
 
98
105
  </details>
99
106
 
107
+ <details>
108
+ <summary>🔄 Import from Cursor/VS Code/Claude Code</summary>
109
+
110
+ ### Import from Cursor/VS Code/Claude Code
111
+
112
+ - **CLI**
113
+
114
+ - Import & configure to use edison as your MCP server:
115
+
116
+ ```bash
117
+ # From source (no install)
118
+ uv run python -m mcp_importer.quick_cli --yes
119
+ # After install: mcp-importer-quick --yes
120
+ ```
121
+
122
+ - Preview what will be imported (no writes):
123
+
124
+ ```bash
125
+ uv run python -m mcp_importer --source cursor --dry-run
126
+ ```
127
+
128
+ - Import servers into Open Edison `config.json` (merge policy defaults to `skip`):
129
+
130
+ ```bash
131
+ uv run python -m mcp_importer --source cursor
132
+ uv run python -m mcp_importer --source vscode
133
+ uv run python -m mcp_importer --source claude-code
134
+ ```
135
+
136
+ - Point your editor to Open Edison (backup original config and replace with a single Open Edison server):
137
+
138
+ ```bash
139
+ uv run python -m mcp_importer export --target cursor --yes
140
+ uv run python -m mcp_importer export --target vscode --yes
141
+ uv run python -m mcp_importer export --target claude-code --yes
142
+ ```
143
+
144
+ </details>
145
+
100
146
  <details>
101
147
  <summary><img src="https://img.shields.io/badge/Docker-2CA5E0?style=for-the-badge&logo=docker&logoColor=white" alt="Docker"> Run with Docker</summary>
102
148
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "open-edison"
3
- version = "0.1.36"
3
+ version = "0.1.37"
4
4
  description = "Open-source MCP security, aggregation, and monitoring. Single-user, self-hosted MCP proxy."
5
5
  readme = "README.md"
6
6
  authors = [
@@ -28,6 +28,8 @@ requires-python = ">= 3.12"
28
28
  [project.scripts]
29
29
  open-edison = "src.cli:main"
30
30
  open_edison = "src.cli:main"
31
+ mcp-importer = "mcp_importer.__main__:main"
32
+ mcp-importer-quick = "mcp_importer.quick_cli:main"
31
33
 
32
34
  [build-system]
33
35
  requires = ["hatchling"]
@@ -0,0 +1,8 @@
1
+ """
2
+ Open Edison Source Package.
3
+
4
+ Note: Avoid importing heavy submodules at package import time to keep
5
+ packaging/import of light utilities (e.g., mcp_importer) side‑effect free.
6
+ """
7
+
8
+ __all__: list[str] = []
@@ -0,0 +1,15 @@
1
+ """MCP importer package for Open Edison scripts.
2
+
3
+ Import submodules explicitly as needed, e.g. `from mcp_importer import cli`.
4
+ """
5
+
6
+ # pyright: reportUnsupportedDunderAll=false
7
+
8
+ __all__ = [
9
+ "paths",
10
+ "parsers",
11
+ "importers",
12
+ "merge",
13
+ "cli",
14
+ "api",
15
+ ]
@@ -0,0 +1,19 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+
5
+ from mcp_importer.cli import run_cli as import_run_cli
6
+ from mcp_importer.export_cli import run_cli as export_run_cli
7
+
8
+
9
+ def main() -> int:
10
+ # Usage:
11
+ # python -m mcp_importer -> import CLI
12
+ # python -m mcp_importer export ... -> export CLI
13
+ if len(sys.argv) > 1 and sys.argv[1] == "export":
14
+ return export_run_cli(sys.argv[2:])
15
+ return import_run_cli(sys.argv[1:])
16
+
17
+
18
+ if __name__ == "__main__":
19
+ raise SystemExit(main())
@@ -0,0 +1,106 @@
1
+ # pyright: reportMissingImports=false, reportUnknownVariableType=false, reportUnknownMemberType=false, reportUnknownArgumentType=false, reportUnknownParameterType=false
2
+ from enum import Enum
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ import mcp_importer.paths as _paths
7
+ from mcp_importer.exporters import export_to_claude_code, export_to_cursor, export_to_vscode
8
+ from mcp_importer.importers import (
9
+ import_from_claude_code as _import_from_claude_code,
10
+ )
11
+ from mcp_importer.importers import (
12
+ import_from_cursor as _import_from_cursor,
13
+ )
14
+ from mcp_importer.importers import (
15
+ import_from_vscode as _import_from_vscode,
16
+ )
17
+ from mcp_importer.merge import MergePolicy, merge_servers
18
+ from src.config import Config, MCPServerConfig, get_config_json_path
19
+
20
+
21
+ class CLIENT(str, Enum):
22
+ CURSOR = "cursor"
23
+ VSCODE = "vscode"
24
+ CLAUDE_CODE = "claude-code"
25
+
26
+
27
+ def detect_clients() -> set[CLIENT]:
28
+ detected: set[CLIENT] = set()
29
+ if _paths.detect_cursor_config_path() is not None:
30
+ detected.add(CLIENT.CURSOR)
31
+ if _paths.detect_vscode_config_path() is not None:
32
+ detected.add(CLIENT.VSCODE)
33
+ if _paths.detect_claude_code_config_path() is not None:
34
+ detected.add(CLIENT.CLAUDE_CODE)
35
+ return detected
36
+
37
+
38
+ import_cursor = _import_from_cursor
39
+ import_vscode = _import_from_vscode
40
+ import_claude_code = _import_from_claude_code
41
+
42
+
43
+ def import_from(client: CLIENT) -> list[MCPServerConfig]:
44
+ if client == CLIENT.CURSOR:
45
+ return import_cursor()
46
+ if client == CLIENT.VSCODE:
47
+ return import_vscode()
48
+ if client == CLIENT.CLAUDE_CODE:
49
+ return import_claude_code()
50
+ raise ValueError(f"Unsupported client: {client}")
51
+
52
+
53
+ def save_imported_servers(
54
+ servers: list[MCPServerConfig],
55
+ *,
56
+ merge_policy: str = MergePolicy.SKIP,
57
+ config_dir: Path | None = None,
58
+ ) -> Path:
59
+ target_path: Path = (
60
+ get_config_json_path() if config_dir is None else (Path(config_dir) / "config.json")
61
+ )
62
+ cfg: Config = Config(target_path)
63
+ merged = merge_servers(existing=cfg.mcp_servers, imported=servers, policy=merge_policy)
64
+ cfg.mcp_servers = merged
65
+ cfg.save(target_path)
66
+ return target_path
67
+
68
+
69
+ def export_edison_to(
70
+ client: CLIENT,
71
+ *,
72
+ url: str = "http://localhost:3000/mcp/",
73
+ api_key: str = "dev-api-key-change-me",
74
+ server_name: str = "open-edison",
75
+ dry_run: bool = False,
76
+ force: bool = False,
77
+ create_if_missing: bool = False,
78
+ ) -> Any:
79
+ match client:
80
+ case CLIENT.CURSOR:
81
+ return export_to_cursor(
82
+ url=url,
83
+ api_key=api_key,
84
+ server_name=server_name,
85
+ dry_run=dry_run,
86
+ force=force,
87
+ create_if_missing=create_if_missing,
88
+ )
89
+ case CLIENT.VSCODE:
90
+ return export_to_vscode(
91
+ url=url,
92
+ api_key=api_key,
93
+ server_name=server_name,
94
+ dry_run=dry_run,
95
+ force=force,
96
+ create_if_missing=create_if_missing,
97
+ )
98
+ case CLIENT.CLAUDE_CODE:
99
+ return export_to_claude_code(
100
+ url=url,
101
+ api_key=api_key,
102
+ server_name=server_name,
103
+ dry_run=dry_run,
104
+ force=force,
105
+ create_if_missing=create_if_missing,
106
+ )
@@ -0,0 +1,113 @@
1
+ # pyright: reportMissingImports=false, reportUnknownVariableType=false, reportUnknownArgumentType=false, reportUnknownMemberType=false, reportUnknownParameterType=false
2
+ import argparse
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ from loguru import logger as log
7
+
8
+ from src.config import Config, get_config_dir
9
+
10
+ from .importers import IMPORTERS
11
+ from .merge import MergePolicy, merge_servers
12
+
13
+ ## Ensure import of src config (place src on sys.path before import)
14
+ # THIS_FILE = Path(__file__).resolve()
15
+ # REPO_ROOT = THIS_FILE.parents[2]
16
+ # SRC_DIR = REPO_ROOT / "src"
17
+ # if str(SRC_DIR) not in sys.path:
18
+ # sys.path.insert(0, str(SRC_DIR))
19
+
20
+
21
+ def build_arg_parser() -> argparse.ArgumentParser:
22
+ p = argparse.ArgumentParser(
23
+ description="Import MCP servers from other tools into Open Edison config.json"
24
+ )
25
+ p.add_argument(
26
+ "--source",
27
+ choices=["cursor", "vscode", "claude-code"],
28
+ required=True,
29
+ )
30
+ p.add_argument(
31
+ "--config-dir",
32
+ type=Path,
33
+ help="Directory containing target config.json (default: OPEN_EDISON_CONFIG_DIR or repo root)",
34
+ )
35
+ p.add_argument(
36
+ "--merge",
37
+ choices=[MergePolicy.SKIP, MergePolicy.OVERWRITE, MergePolicy.RENAME],
38
+ default=MergePolicy.SKIP,
39
+ )
40
+ p.add_argument(
41
+ "--dry-run", action="store_true", help="Show changes without writing to config.json"
42
+ )
43
+ return p
44
+
45
+
46
+ def run_cli(argv: list[str] | None = None) -> int: # noqa: C901
47
+ parser = build_arg_parser()
48
+ args = parser.parse_args(argv)
49
+
50
+ source: str = args.source
51
+
52
+ importer = IMPORTERS.get(source)
53
+ if not importer:
54
+ print(f"Unsupported source: {source}", file=sys.stderr)
55
+ return 2
56
+
57
+ # Resolve target config path
58
+ target_dir: Path = args.config_dir or get_config_dir()
59
+ target_path = target_dir / "config.json"
60
+
61
+ # Load existing config (auto-creates default if missing via Config.load)
62
+ config_obj: Config = Config(target_dir)
63
+
64
+ # Import
65
+ imported_servers = importer()
66
+
67
+ if not imported_servers:
68
+ log.warning("No servers found to import from source '{}'", source)
69
+ return 0
70
+
71
+ # Merge
72
+ merged = merge_servers(
73
+ existing=config_obj.mcp_servers,
74
+ imported=imported_servers,
75
+ policy=args.merge,
76
+ )
77
+
78
+ existing_names: set[str] = {str(getattr(s, "name", "")) for s in config_obj.mcp_servers}
79
+ merged_names: set[str] = {str(getattr(s, "name", "")) for s in merged}
80
+ added = merged_names - existing_names
81
+ replaced: set[str] = set()
82
+ if args.merge == MergePolicy.OVERWRITE:
83
+ replaced = existing_names & {s.name for s in imported_servers}
84
+
85
+ log.info("Imported {} server(s) from '{}'", len(imported_servers), source)
86
+ try:
87
+ names_preview = ", ".join(sorted(getattr(s, "name", "") for s in imported_servers))
88
+ if names_preview:
89
+ log.info("Detected servers: {}", names_preview)
90
+ except Exception:
91
+ pass
92
+ if added:
93
+ log.info("Added: {}", ", ".join(sorted(added)))
94
+ if replaced:
95
+ log.info("Overwrote: {}", ", ".join(sorted(replaced)))
96
+
97
+ if args.dry_run:
98
+ log.info("Dry-run enabled; not writing changes to {}", target_path)
99
+ log.debug("Merged servers: {}", merged)
100
+ return 0
101
+
102
+ config_obj.mcp_servers = merged
103
+ config_obj.save(target_path)
104
+ log.info("Configuration updated: {}", target_path)
105
+ return 0
106
+
107
+
108
+ def main() -> int:
109
+ return run_cli()
110
+
111
+
112
+ if __name__ == "__main__":
113
+ raise SystemExit(main())
@@ -0,0 +1,201 @@
1
+ import argparse
2
+ from pathlib import Path
3
+
4
+ from loguru import logger as log
5
+
6
+ from .exporters import ExportError, export_to_claude_code, export_to_cursor, export_to_vscode
7
+ from .paths import (
8
+ detect_cursor_config_path,
9
+ detect_vscode_config_path,
10
+ get_default_cursor_config_path,
11
+ get_default_vscode_config_path,
12
+ )
13
+
14
+
15
+ def _prompt_yes_no(message: str, *, default_no: bool = True) -> bool:
16
+ suffix = "[y/N]" if default_no else "[Y/n]"
17
+ while True:
18
+ resp = input(f"{message} {suffix} ").strip().lower()
19
+ if resp == "y" or resp == "yes":
20
+ return True
21
+ if resp == "n" or resp == "no":
22
+ return False
23
+ if resp == "" and default_no:
24
+ return False
25
+ if resp == "" and not default_no:
26
+ return True
27
+
28
+
29
+ def build_arg_parser() -> argparse.ArgumentParser:
30
+ p = argparse.ArgumentParser(
31
+ description="Export editor MCP config to use Open Edison (Cursor support)",
32
+ )
33
+ p.add_argument("--target", choices=["cursor", "vscode", "claude-code"], default="cursor")
34
+ p.add_argument("--dry-run", action="store_true", help="Show actions without writing")
35
+ p.add_argument("--force", action="store_true", help="Rewrite even if already configured")
36
+ p.add_argument(
37
+ "--yes",
38
+ action="store_true",
39
+ help="Automatic yes to prompts (create missing files without confirmation)",
40
+ )
41
+ p.add_argument("--url", default="http://localhost:3000/mcp/", help="MCP URL")
42
+ p.add_argument(
43
+ "--api-key",
44
+ default="dev-api-key-change-me",
45
+ help="API key for Authorization header",
46
+ )
47
+ p.add_argument("--name", default="open-edison", help="Name of the server entry")
48
+ return p
49
+
50
+
51
+ def _handle_cursor(args: argparse.Namespace) -> int:
52
+ detected = detect_cursor_config_path()
53
+ target_path: Path = detected if detected else get_default_cursor_config_path()
54
+
55
+ create_if_missing = False
56
+ if not target_path.exists():
57
+ if args.yes:
58
+ create_if_missing = True
59
+ else:
60
+ confirmed = _prompt_yes_no(
61
+ f"Cursor config not found at {target_path}. Create it?", default_no=False
62
+ )
63
+ if not confirmed:
64
+ log.info("Aborted: user declined to create missing file")
65
+ return 0
66
+ create_if_missing = True
67
+
68
+ try:
69
+ result = export_to_cursor(
70
+ url=args.url,
71
+ api_key=args.api_key,
72
+ server_name=args.name,
73
+ dry_run=args.dry_run,
74
+ force=args.force,
75
+ create_if_missing=create_if_missing,
76
+ )
77
+ except ExportError as e:
78
+ log.error(str(e))
79
+ return 1
80
+
81
+ if result.dry_run:
82
+ log.info("Dry-run complete. No changes written.")
83
+ return 0
84
+
85
+ if result.wrote_changes:
86
+ if result.backup_path is not None:
87
+ log.info("Backup created at {}", result.backup_path)
88
+ log.info("Updated {}", result.target_path)
89
+ else:
90
+ log.info("No changes were necessary.")
91
+ return 0
92
+
93
+
94
+ def _handle_vscode(args: argparse.Namespace) -> int:
95
+ detected = detect_vscode_config_path()
96
+ target_path: Path = detected if detected else get_default_vscode_config_path()
97
+
98
+ create_if_missing = False
99
+ if not target_path.exists():
100
+ if args.yes:
101
+ create_if_missing = True
102
+ else:
103
+ confirmed = _prompt_yes_no(
104
+ f"VS Code MCP config not found at {target_path}. Create it?", default_no=False
105
+ )
106
+ if not confirmed:
107
+ log.info("Aborted: user declined to create missing file")
108
+ return 0
109
+ create_if_missing = True
110
+
111
+ try:
112
+ result = export_to_vscode(
113
+ url=args.url,
114
+ api_key=args.api_key,
115
+ server_name=args.name,
116
+ dry_run=args.dry_run,
117
+ force=args.force,
118
+ create_if_missing=create_if_missing,
119
+ )
120
+ except ExportError as e:
121
+ log.error(str(e))
122
+ return 1
123
+
124
+ if result.dry_run:
125
+ log.info("Dry-run complete. No changes written.")
126
+ return 0
127
+
128
+ if result.wrote_changes:
129
+ if result.backup_path is not None:
130
+ log.info("Backup created at {}", result.backup_path)
131
+ log.info("Updated {}", result.target_path)
132
+ else:
133
+ log.info("No changes were necessary.")
134
+ return 0
135
+
136
+
137
+ def _handle_claude_code(args: argparse.Namespace) -> int:
138
+ from .paths import detect_claude_code_config_path, get_default_claude_code_config_path
139
+
140
+ detected = detect_claude_code_config_path()
141
+ target_path: Path = detected if detected else get_default_claude_code_config_path()
142
+
143
+ create_if_missing = False
144
+ if not target_path.exists():
145
+ if args.yes:
146
+ create_if_missing = True
147
+ else:
148
+ confirmed = _prompt_yes_no(
149
+ f"Claude Code config not found at {target_path}. Create it?", default_no=False
150
+ )
151
+ if not confirmed:
152
+ log.info("Aborted: user declined to create missing file")
153
+ return 0
154
+ create_if_missing = True
155
+
156
+ try:
157
+ result = export_to_claude_code(
158
+ url=args.url,
159
+ api_key=args.api_key,
160
+ server_name=args.name,
161
+ dry_run=args.dry_run,
162
+ force=args.force,
163
+ create_if_missing=create_if_missing,
164
+ )
165
+ except ExportError as e:
166
+ log.error(str(e))
167
+ return 1
168
+
169
+ if result.dry_run:
170
+ log.info("Dry-run complete. No changes written.")
171
+ return 0
172
+
173
+ if result.wrote_changes:
174
+ if result.backup_path is not None:
175
+ log.info("Backup created at {}", result.backup_path)
176
+ log.info("Updated {}", result.target_path)
177
+ else:
178
+ log.info("No changes were necessary.")
179
+ return 0
180
+
181
+
182
+ def run_cli(argv: list[str] | None = None) -> int:
183
+ parser = build_arg_parser()
184
+ args = parser.parse_args(argv)
185
+
186
+ if args.target == "cursor":
187
+ return _handle_cursor(args)
188
+ if args.target == "vscode":
189
+ return _handle_vscode(args)
190
+ if args.target == "claude-code":
191
+ return _handle_claude_code(args)
192
+ log.error("Unsupported target: {}", args.target)
193
+ return 2
194
+
195
+
196
+ def main() -> int:
197
+ return run_cli()
198
+
199
+
200
+ if __name__ == "__main__":
201
+ raise SystemExit(main())