open-edison 0.1.34__py3-none-any.whl → 0.1.37__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: open-edison
3
- Version: 0.1.34
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
@@ -20,17 +20,13 @@ Requires-Dist: pyyaml>=6.0.2
20
20
  Requires-Dist: sqlalchemy>=2.0.41
21
21
  Requires-Dist: starlette>=0.47.1
22
22
  Requires-Dist: uvicorn>=0.35.0
23
- Provides-Extra: dev
24
- Requires-Dist: pytest-asyncio>=1.0.0; extra == 'dev'
25
- Requires-Dist: pytest>=8.3.3; extra == 'dev'
26
- Requires-Dist: ruff>=0.12.3; extra == 'dev'
27
23
  Description-Content-Type: text/markdown
28
24
 
29
25
  # OpenEdison 🔒⚡️
30
26
 
31
27
  > The Secure MCP Control Panel
32
28
 
33
- 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.
34
30
 
35
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.
36
32
 
@@ -76,6 +72,13 @@ curl -fsSL https://raw.githubusercontent.com/Edison-Watch/open-edison/main/curl_
76
72
 
77
73
  Run locally with uvx: `uvx open-edison`
78
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
+
79
82
  <details>
80
83
  <summary>⬇️ Install Node.js/npm (optional for MCP tools)</summary>
81
84
 
@@ -125,6 +128,45 @@ OPEN_EDISON_CONFIG_DIR=~/edison-config open-edison run
125
128
 
126
129
  </details>
127
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
+
128
170
  <details>
129
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>
130
172
 
@@ -0,0 +1,35 @@
1
+ src/__init__.py,sha256=bEYMwBiuW9jzF07iWhas4Vb30EcpnqfpNfz_Q6yO1jU,209
2
+ src/__main__.py,sha256=kQsaVyzRa_ESC57JpKDSQJAHExuXme0rM5beJsYxFeA,161
3
+ src/cli.py,sha256=P38IWER41S5oAfbd_7p89hBpnjClsNHpmE5pSsJc6uU,9733
4
+ src/config.py,sha256=RSsAYzl8cj6eaDN1RORMcfKKWBcp4bKTQp2BdhAL9mg,10258
5
+ src/config.pyi,sha256=FgehEGli8ZXSjGlANBgMGv5497q4XskQciOc1fUcxqM,2033
6
+ src/events.py,sha256=aFQrVXDIZwt55Dz6OtyoXu2yi9evqo-8jZzo3CR2Tto,4965
7
+ src/oauth_manager.py,sha256=qcQa5BDRZr4bjqiXNflCnrXOh9mo9JVjvP2Caseg2Uc,9943
8
+ src/permissions.py,sha256=NGAnlG_z59HEiVA-k3cYvwmmiuHzxuNb5Tbd5umbL00,10483
9
+ src/server.py,sha256=cnO5bgxT-lrfuwk9AIvB_HBV8SWOtFClfGUn5_zFWyo,45652
10
+ src/single_user_mcp.py,sha256=rJrlqHcIubGkos_24ux5rb3OoKYDzvagCHghhfDeXTI,18535
11
+ src/telemetry.py,sha256=-RZPIjpI53zbsKmp-63REeZ1JirWHV5WvpSRa2nqZEk,11321
12
+ src/frontend_dist/index.html,sha256=s95FMkH8VLisvawLH7bZxbLzRUFvMhHkH6ZMzpVBngs,673
13
+ src/frontend_dist/sw.js,sha256=rihX1es-vWwjmtnXyaksJjs2dio6MVAOTAWwQPeJUYw,2164
14
+ src/frontend_dist/assets/index-BUUcUfTt.js,sha256=awoyPI6u0v6ao2iarZdSkrSDUvyU8aNkMLqHMvgVgyY,257666
15
+ src/frontend_dist/assets/index-o6_8mdM8.css,sha256=nwmX_6q55mB9463XN2JM8BdeihjkALpQK83Fc3_iGvE,15936
16
+ src/mcp_importer/__init__.py,sha256=Yqr4NVAbKRVIuDzOj-yXzyB8HWLl-I4KmP5pVIRxs1o,271
17
+ src/mcp_importer/__main__.py,sha256=_2LUxAFFGJ9ECg5OoUqUZMxE6QBzhYPrfpLfKvCQM7k,507
18
+ src/mcp_importer/api.py,sha256=8BwPCeve-rY6T9Xhn-9FptBR2K_v_UBxe7m6CaOUMnw,3354
19
+ src/mcp_importer/cli.py,sha256=Pe0GLWm1nMd1VuNXOSkxIrFZuGNFc9dNvfBsvf-bdBI,3487
20
+ src/mcp_importer/export_cli.py,sha256=daEadB6nL8P4OpEGFx0GshuN1a091L7BhiitpV1bPqA,6294
21
+ src/mcp_importer/exporters.py,sha256=fSgl6seduoXFp7YnKH26UEaC1sFBnd4whSut7CJLBQs,11348
22
+ src/mcp_importer/import_api.py,sha256=xWaKoE3vibSWpA5roVL7qEMS73vcmAC0tcHP6CsZw6E,95
23
+ src/mcp_importer/importers.py,sha256=zGN8lT7qQJ95jDTd-ck09j_w5PSvH-uj33TILoHfHbs,2191
24
+ src/mcp_importer/merge.py,sha256=KIGT7UgbAm07-LdyoUXEJ7ABSIiPTFlj_qjz669yFxg,1569
25
+ src/mcp_importer/parsers.py,sha256=JRE7y_Gg-QmlAARvZdrI9CmUyy-ODvDPbS695pb3Aw8,4856
26
+ src/mcp_importer/paths.py,sha256=4L-cPr7KCM9X9gAUP7Da6ictLNrPWuQ_IM419zqY-2I,2700
27
+ src/mcp_importer/quick_cli.py,sha256=MCkHr_ljyUPS0pzTwJf4bW-UpknQP8NPzIWMxjUu5Nc,1907
28
+ src/mcp_importer/types.py,sha256=h03TbAnJbap6OWWd0dT0QcFWNvSaiVFWH9V9PD6x4s0,138
29
+ src/middleware/data_access_tracker.py,sha256=bArBffWgYmvxOx9z_pgXQhogvnWQcc1m6WvEblDD4gw,15039
30
+ src/middleware/session_tracking.py,sha256=5W1VH9HNqIZeX0HNxDEm41U4GY6SqKSXtApDEeZK2qo,23084
31
+ open_edison-0.1.37.dist-info/METADATA,sha256=41uTPLASX7MIufzvxIz_MQ7zAVkTfmNrrmz3afWqAu4,13198
32
+ open_edison-0.1.37.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
33
+ open_edison-0.1.37.dist-info/entry_points.txt,sha256=qUjYGPEfqSQyra9dTe1aRvHVAAzLzoNvZqNDk1t75IA,163
34
+ open_edison-0.1.37.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
35
+ open_edison-0.1.37.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ [console_scripts]
2
+ mcp-importer = mcp_importer.__main__:main
3
+ mcp-importer-quick = mcp_importer.quick_cli:main
4
+ open-edison = src.cli:main
5
+ open_edison = src.cli:main
src/__init__.py CHANGED
@@ -1,11 +1,8 @@
1
1
  """
2
- Open Edison Source Package
2
+ Open Edison Source Package.
3
3
 
4
- Main source code package for the Open Edison single-user MCP proxy server.
5
-
6
- This package exposes a CLI via `open-edison` / `open_edison` entrypoints.
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.
7
6
  """
8
7
 
9
- from .server import OpenEdisonProxy
10
-
11
- __all__ = ["OpenEdisonProxy"]
8
+ __all__: list[str] = []
src/cli.py CHANGED
@@ -4,8 +4,6 @@ CLI entrypoint for Open Edison.
4
4
  Provides `open-edison` executable when installed via pip/uvx/pipx.
5
5
  """
6
6
 
7
- from __future__ import annotations
8
-
9
7
  import argparse
10
8
  import asyncio
11
9
  import os
@@ -69,11 +67,6 @@ def _parse_args(argv: list[str] | None = None) -> argparse.Namespace:
69
67
  default="interactive",
70
68
  help="Source application to import from",
71
69
  )
72
- sp_import.add_argument(
73
- "--project-dir",
74
- type=Path,
75
- help="When --source=cursor, path to the project containing .cursor/mcp.json",
76
- )
77
70
  sp_import.add_argument(
78
71
  "--config-dir",
79
72
  type=Path,
@@ -250,11 +243,6 @@ def main(argv: list[str] | None = None) -> NoReturn: # noqa: C901
250
243
  importer_argv: list[str] = []
251
244
  if args.source:
252
245
  importer_argv += ["--source", str(args.source)]
253
- if getattr(args, "project_dir", None):
254
- importer_argv += [
255
- "--project-dir",
256
- str(Path(args.project_dir).expanduser().resolve()),
257
- ]
258
246
  if getattr(args, "config_dir", None):
259
247
  importer_argv += [
260
248
  "--config-dir",
@@ -262,8 +250,6 @@ def main(argv: list[str] | None = None) -> NoReturn: # noqa: C901
262
250
  ]
263
251
  if args.merge:
264
252
  importer_argv += ["--merge", str(args.merge)]
265
- if bool(getattr(args, "enable_imported", False)):
266
- importer_argv += ["--enable-imported"]
267
253
  if bool(getattr(args, "dry_run", False)):
268
254
  importer_argv += ["--dry-run"]
269
255
 
src/config.py CHANGED
@@ -142,11 +142,16 @@ class TelemetryConfig:
142
142
  def load_json_file(path: Path) -> dict[str, Any]:
143
143
  """Load a JSON file from the given path.
144
144
  Kept as a separate function because we want to manually clear cache sometimes (update in config)"""
145
- log.info(f"Loading configuration from {path}")
145
+ log.trace(f"Loading configuration from {path}")
146
146
  with open(path) as f:
147
147
  return json.load(f)
148
148
 
149
149
 
150
+ def clear_json_file_cache() -> None:
151
+ """Clear the cache for the given JSON file path"""
152
+ load_json_file.cache_clear()
153
+
154
+
150
155
  @dataclass
151
156
  class Config:
152
157
  """Main configuration class"""
src/config.pyi ADDED
@@ -0,0 +1,80 @@
1
+ from pathlib import Path
2
+ from typing import Any, overload
3
+
4
+ # Module constants
5
+ DEFAULT_OTLP_METRICS_ENDPOINT: str
6
+ root_dir: Path
7
+
8
+ def get_config_dir() -> Path: ...
9
+ def get_config_json_path() -> Path: ...
10
+
11
+ class ServerConfig:
12
+ host: str
13
+ port: int
14
+ api_key: str
15
+
16
+ class LoggingConfig:
17
+ level: str
18
+ database_path: str
19
+
20
+ class MCPServerConfig:
21
+ name: str
22
+ command: str
23
+ args: list[str]
24
+ env: dict[str, str] | None
25
+ enabled: bool
26
+ roots: list[str] | None
27
+ oauth_scopes: list[str] | None
28
+ oauth_client_name: str | None
29
+
30
+ def __init__(
31
+ self,
32
+ *,
33
+ name: str,
34
+ command: str,
35
+ args: list[str],
36
+ env: dict[str, str] | None = None,
37
+ enabled: bool = True,
38
+ roots: list[str] | None = None,
39
+ oauth_scopes: list[str] | None = None,
40
+ oauth_client_name: str | None = None,
41
+ ) -> None: ...
42
+ def is_remote_server(self) -> bool: ...
43
+ def get_remote_url(self) -> str | None: ...
44
+
45
+ class TelemetryConfig:
46
+ enabled: bool
47
+ otlp_endpoint: str | None
48
+ headers: dict[str, str] | None
49
+ export_interval_ms: int
50
+ def __init__(
51
+ self,
52
+ *,
53
+ enabled: bool = True,
54
+ otlp_endpoint: str | None = None,
55
+ headers: dict[str, str] | None = None,
56
+ export_interval_ms: int = 60000,
57
+ ) -> None: ...
58
+
59
+ def load_json_file(path: Path) -> dict[str, Any]: ...
60
+ def clear_json_file_cache() -> None: ...
61
+
62
+ class Config:
63
+ @property
64
+ def version(self) -> str: ...
65
+ server: ServerConfig
66
+ logging: LoggingConfig
67
+ mcp_servers: list[MCPServerConfig]
68
+ telemetry: TelemetryConfig | None
69
+ @overload
70
+ def __init__(self, config_path: Path | None = None) -> None: ...
71
+ @overload
72
+ def __init__(
73
+ self,
74
+ server: ServerConfig,
75
+ logging: LoggingConfig,
76
+ mcp_servers: list[MCPServerConfig],
77
+ telemetry: TelemetryConfig | None = None,
78
+ ) -> None: ...
79
+ def save(self, config_path: Path | None = None) -> None: ...
80
+ def create_default(self) -> None: ...
src/events.py CHANGED
@@ -5,8 +5,6 @@ Provides a simple publisher/subscriber model to stream JSON events to
5
5
  connected dashboard clients over Server-Sent Events (SSE).
6
6
  """
7
7
 
8
- from __future__ import annotations
9
-
10
8
  import asyncio
11
9
  import json
12
10
  from collections.abc import AsyncIterator, Callable
@@ -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())