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.
- {open_edison-0.1.34.dist-info → open_edison-0.1.37.dist-info}/METADATA +48 -6
- open_edison-0.1.37.dist-info/RECORD +35 -0
- open_edison-0.1.37.dist-info/entry_points.txt +5 -0
- src/__init__.py +4 -7
- src/cli.py +0 -14
- src/config.py +6 -1
- src/config.pyi +80 -0
- src/events.py +0 -2
- src/mcp_importer/__init__.py +15 -0
- src/mcp_importer/__main__.py +19 -0
- src/mcp_importer/api.py +106 -0
- src/mcp_importer/cli.py +113 -0
- src/mcp_importer/export_cli.py +201 -0
- src/mcp_importer/exporters.py +393 -0
- src/mcp_importer/import_api.py +3 -0
- src/mcp_importer/importers.py +63 -0
- src/mcp_importer/merge.py +47 -0
- src/mcp_importer/parsers.py +148 -0
- src/mcp_importer/paths.py +92 -0
- src/mcp_importer/quick_cli.py +65 -0
- src/mcp_importer/types.py +5 -0
- src/server.py +2 -2
- open_edison-0.1.34.dist-info/RECORD +0 -21
- open_edison-0.1.34.dist-info/entry_points.txt +0 -3
- {open_edison-0.1.34.dist-info → open_edison-0.1.37.dist-info}/WHEEL +0 -0
- {open_edison-0.1.34.dist-info → open_edison-0.1.37.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,148 @@
|
|
1
|
+
# pyright: reportUnknownArgumentType=false, reportUnknownVariableType=false, reportMissingImports=false, reportUnknownMemberType=false
|
2
|
+
|
3
|
+
import json
|
4
|
+
from pathlib import Path
|
5
|
+
from typing import Any, cast
|
6
|
+
|
7
|
+
from loguru import logger as log
|
8
|
+
|
9
|
+
from src.config import MCPServerConfig
|
10
|
+
|
11
|
+
|
12
|
+
# Import Open Edison types
|
13
|
+
def _new_mcp_server_config(
|
14
|
+
*,
|
15
|
+
name: str,
|
16
|
+
command: str,
|
17
|
+
args: list[str],
|
18
|
+
env: dict[str, str] | None,
|
19
|
+
enabled: bool,
|
20
|
+
roots: list[str] | None,
|
21
|
+
) -> Any:
|
22
|
+
"""Runtime-constructed MCPServerConfig without static import coupling."""
|
23
|
+
|
24
|
+
return MCPServerConfig(
|
25
|
+
name=name,
|
26
|
+
command=command,
|
27
|
+
args=args,
|
28
|
+
env=env or {},
|
29
|
+
enabled=enabled,
|
30
|
+
roots=roots,
|
31
|
+
)
|
32
|
+
|
33
|
+
|
34
|
+
class ImportErrorDetails(Exception): # noqa: N818
|
35
|
+
def __init__(self, message: str, path: Path | None = None):
|
36
|
+
super().__init__(message)
|
37
|
+
self.path = path
|
38
|
+
|
39
|
+
|
40
|
+
def safe_read_json(path: Path) -> dict[str, Any]:
|
41
|
+
try:
|
42
|
+
with open(path, encoding="utf-8") as f:
|
43
|
+
loaded = json.load(f)
|
44
|
+
except Exception as e:
|
45
|
+
raise ImportErrorDetails(f"Failed to read JSON from {path}: {e}", path) from e
|
46
|
+
|
47
|
+
if not isinstance(loaded, dict):
|
48
|
+
raise ImportErrorDetails(f"Expected JSON object at {path}", path)
|
49
|
+
data: dict[str, Any] = cast(dict[str, Any], loaded)
|
50
|
+
return data
|
51
|
+
|
52
|
+
|
53
|
+
def _coerce_server_entry(name: str, node: dict[str, Any], default_enabled: bool) -> Any:
|
54
|
+
command_val = node.get("command", "")
|
55
|
+
command = str(command_val) if isinstance(command_val, str) else ""
|
56
|
+
|
57
|
+
args_raw = node.get("args", [])
|
58
|
+
if not isinstance(args_raw, list):
|
59
|
+
args_raw = []
|
60
|
+
|
61
|
+
# Some tools provide combined commandWithArgs
|
62
|
+
if command == "" and isinstance(node.get("commandWithArgs"), list):
|
63
|
+
cmd_with_args = [str(p) for p in node["commandWithArgs"]]
|
64
|
+
if cmd_with_args:
|
65
|
+
command = cmd_with_args[0]
|
66
|
+
args_raw = cmd_with_args[1:]
|
67
|
+
|
68
|
+
args: list[str] = [str(a) for a in args_raw]
|
69
|
+
|
70
|
+
env_raw = node.get("env") or node.get("environment") or {}
|
71
|
+
env: dict[str, str] = {}
|
72
|
+
if isinstance(env_raw, dict):
|
73
|
+
for k, v in env_raw.items():
|
74
|
+
env[str(k)] = str(v)
|
75
|
+
|
76
|
+
enabled = bool(node.get("enabled", default_enabled))
|
77
|
+
|
78
|
+
roots_raw = node.get("roots") or node.get("rootPaths") or []
|
79
|
+
roots: list[str] | None = None
|
80
|
+
if isinstance(roots_raw, list):
|
81
|
+
roots = [str(r) for r in roots_raw]
|
82
|
+
if len(roots) == 0:
|
83
|
+
roots = None
|
84
|
+
|
85
|
+
return _new_mcp_server_config(
|
86
|
+
name=name,
|
87
|
+
command=command,
|
88
|
+
args=args,
|
89
|
+
env=env,
|
90
|
+
enabled=enabled,
|
91
|
+
roots=roots,
|
92
|
+
)
|
93
|
+
|
94
|
+
|
95
|
+
def _collect_from_dict(node_dict: dict[str, Any], default_enabled: bool) -> list[Any]:
|
96
|
+
results: list[Any] = []
|
97
|
+
for name_key, spec_obj in node_dict.items():
|
98
|
+
if isinstance(spec_obj, dict):
|
99
|
+
results.append(_coerce_server_entry(str(name_key), spec_obj, default_enabled))
|
100
|
+
return results
|
101
|
+
|
102
|
+
|
103
|
+
def _collect_from_list(node_list: list[Any], default_enabled: bool) -> list[Any]:
|
104
|
+
results: list[Any] = []
|
105
|
+
for spec_obj in node_list:
|
106
|
+
if isinstance(spec_obj, dict) and "name" in spec_obj:
|
107
|
+
name_val_obj = spec_obj.get("name")
|
108
|
+
name_str = str(name_val_obj) if name_val_obj is not None else ""
|
109
|
+
results.append(_coerce_server_entry(name_str, spec_obj, default_enabled))
|
110
|
+
return results
|
111
|
+
|
112
|
+
|
113
|
+
def _collect_top_level(data: dict[str, Any], default_enabled: bool) -> list[Any]:
|
114
|
+
results: list[Any] = []
|
115
|
+
for key in ("mcpServers", "servers"):
|
116
|
+
node = data.get(key)
|
117
|
+
if isinstance(node, dict):
|
118
|
+
results.extend(_collect_from_dict(node, default_enabled))
|
119
|
+
elif isinstance(node, list):
|
120
|
+
results.extend(_collect_from_list(node, default_enabled))
|
121
|
+
return results
|
122
|
+
|
123
|
+
|
124
|
+
def _collect_nested(data: dict[str, Any], default_enabled: bool) -> list[Any]:
|
125
|
+
results: list[Any] = []
|
126
|
+
for _k, v in data.items():
|
127
|
+
# If nested dict, recurse regardless of key to catch structures like 'projects'
|
128
|
+
if isinstance(v, dict):
|
129
|
+
results.extend(parse_mcp_like_json(v, default_enabled=default_enabled))
|
130
|
+
# If nested list, recurse into dict items
|
131
|
+
elif isinstance(v, list):
|
132
|
+
for item in v:
|
133
|
+
if isinstance(item, dict):
|
134
|
+
results.extend(parse_mcp_like_json(item, default_enabled=default_enabled))
|
135
|
+
return results
|
136
|
+
|
137
|
+
|
138
|
+
def parse_mcp_like_json(data: dict[str, Any], default_enabled: bool = True) -> list[Any]:
|
139
|
+
# First, try top-level keys
|
140
|
+
top_level = _collect_top_level(data, default_enabled)
|
141
|
+
if top_level:
|
142
|
+
return top_level
|
143
|
+
|
144
|
+
# Then, try nested structures heuristically
|
145
|
+
nested = _collect_nested(data, default_enabled)
|
146
|
+
if not nested:
|
147
|
+
log.debug("No MCP-like entries detected in provided data")
|
148
|
+
return nested
|
@@ -0,0 +1,92 @@
|
|
1
|
+
import os
|
2
|
+
import sys
|
3
|
+
from pathlib import Path
|
4
|
+
|
5
|
+
|
6
|
+
def is_windows() -> bool:
|
7
|
+
return os.name == "nt"
|
8
|
+
|
9
|
+
|
10
|
+
def is_macos() -> bool:
|
11
|
+
return sys.platform == "darwin"
|
12
|
+
|
13
|
+
|
14
|
+
def find_cursor_user_file() -> list[Path]:
|
15
|
+
"""Find user-level Cursor MCP config (~/.cursor/mcp.json)."""
|
16
|
+
p = (Path.home() / ".cursor" / "mcp.json").resolve()
|
17
|
+
return [p] if p.exists() else []
|
18
|
+
|
19
|
+
|
20
|
+
def find_vscode_user_mcp_file() -> list[Path]:
|
21
|
+
"""Find VSCode user-level MCP config (User/mcp.json) on macOS or Linux."""
|
22
|
+
if is_macos():
|
23
|
+
p = Path.home() / "Library" / "Application Support" / "Code" / "User" / "mcp.json"
|
24
|
+
else:
|
25
|
+
p = Path.home() / ".config" / "Code" / "User" / "mcp.json"
|
26
|
+
p = p.resolve()
|
27
|
+
return [p] if p.exists() else []
|
28
|
+
|
29
|
+
|
30
|
+
def find_claude_code_user_settings_file() -> list[Path]:
|
31
|
+
"""Find Claude Code user-level settings (~/.claude/settings.json)."""
|
32
|
+
p = (Path.home() / ".claude" / "settings.json").resolve()
|
33
|
+
return [p] if p.exists() else []
|
34
|
+
|
35
|
+
|
36
|
+
def find_claude_code_user_all_candidates() -> list[Path]:
|
37
|
+
"""Return ordered list of Claude Code user-level MCP config candidates.
|
38
|
+
|
39
|
+
Based on docs, check in priority order:
|
40
|
+
- ~/.claude.json (primary user-level)
|
41
|
+
- ~/.claude/settings.json
|
42
|
+
- ~/.claude/settings.local.json
|
43
|
+
- ~/.claude/mcp_servers.json
|
44
|
+
"""
|
45
|
+
home = Path.home()
|
46
|
+
candidates: list[Path] = [
|
47
|
+
home / ".claude.json",
|
48
|
+
home / ".claude" / "settings.json",
|
49
|
+
home / ".claude" / "settings.local.json",
|
50
|
+
home / ".claude" / "mcp_servers.json",
|
51
|
+
]
|
52
|
+
existing: list[Path] = []
|
53
|
+
for p in candidates:
|
54
|
+
rp = p.resolve()
|
55
|
+
if rp.exists():
|
56
|
+
existing.append(rp)
|
57
|
+
return existing
|
58
|
+
|
59
|
+
|
60
|
+
# Shared utils for CLI import/export
|
61
|
+
|
62
|
+
|
63
|
+
def detect_cursor_config_path() -> Path | None:
|
64
|
+
files = find_cursor_user_file()
|
65
|
+
return files[0] if files else None
|
66
|
+
|
67
|
+
|
68
|
+
def detect_vscode_config_path() -> Path | None:
|
69
|
+
files = find_vscode_user_mcp_file()
|
70
|
+
return files[0] if files else None
|
71
|
+
|
72
|
+
|
73
|
+
def get_default_vscode_config_path() -> Path:
|
74
|
+
if is_macos():
|
75
|
+
return (
|
76
|
+
Path.home() / "Library" / "Application Support" / "Code" / "User" / "mcp.json"
|
77
|
+
).resolve()
|
78
|
+
return (Path.home() / ".config" / "Code" / "User" / "mcp.json").resolve()
|
79
|
+
|
80
|
+
|
81
|
+
def get_default_cursor_config_path() -> Path:
|
82
|
+
return (Path.home() / ".cursor" / "mcp.json").resolve()
|
83
|
+
|
84
|
+
|
85
|
+
def detect_claude_code_config_path() -> Path | None:
|
86
|
+
candidates = find_claude_code_user_all_candidates()
|
87
|
+
return candidates[0] if candidates else None
|
88
|
+
|
89
|
+
|
90
|
+
def get_default_claude_code_config_path() -> Path:
|
91
|
+
# Prefer top-level ~/.claude.json as default create target
|
92
|
+
return (Path.home() / ".claude.json").resolve()
|
@@ -0,0 +1,65 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import argparse
|
4
|
+
from collections.abc import Iterable
|
5
|
+
|
6
|
+
from mcp_importer.api import (
|
7
|
+
CLIENT,
|
8
|
+
detect_clients,
|
9
|
+
export_edison_to,
|
10
|
+
import_from,
|
11
|
+
save_imported_servers,
|
12
|
+
)
|
13
|
+
|
14
|
+
|
15
|
+
def _pick_first(iterable: Iterable[CLIENT]) -> CLIENT | None:
|
16
|
+
for item in iterable:
|
17
|
+
return item
|
18
|
+
return None
|
19
|
+
|
20
|
+
|
21
|
+
def run_cli(argv: list[str] | None = None) -> int:
|
22
|
+
parser = argparse.ArgumentParser(
|
23
|
+
description=(
|
24
|
+
"Detect a client, import its servers into Open Edison, and export Open Edison back to it."
|
25
|
+
)
|
26
|
+
)
|
27
|
+
parser.add_argument("--dry-run", action="store_true", help="Preview actions without writing")
|
28
|
+
parser.add_argument("--yes", action="store_true", help="Skip confirmations (no effect here)")
|
29
|
+
args = parser.parse_args(argv)
|
30
|
+
|
31
|
+
detected = detect_clients()
|
32
|
+
client = _pick_first(detected)
|
33
|
+
if client is None:
|
34
|
+
print("No supported clients detected.")
|
35
|
+
return 2
|
36
|
+
|
37
|
+
servers = import_from(client)
|
38
|
+
if not servers:
|
39
|
+
print(f"No servers found to import from '{client.value}'.")
|
40
|
+
return 0
|
41
|
+
|
42
|
+
if args.dry_run:
|
43
|
+
print(
|
44
|
+
f"[dry-run] Would import {len(servers)} server(s) from '{client.value}' and save to config.json"
|
45
|
+
)
|
46
|
+
# Exercise export path safely (no writes)
|
47
|
+
export_edison_to(client, dry_run=True, force=True, create_if_missing=True)
|
48
|
+
print(
|
49
|
+
f"[dry-run] Would export Open Edison to '{client.value}' (backup and replace editor MCP config)"
|
50
|
+
)
|
51
|
+
print("Dry-run complete.")
|
52
|
+
return 0
|
53
|
+
|
54
|
+
save_imported_servers(servers)
|
55
|
+
export_edison_to(client, dry_run=False, force=True, create_if_missing=True)
|
56
|
+
print(f"Completed quick import/export for {client.value}.")
|
57
|
+
return 0
|
58
|
+
|
59
|
+
|
60
|
+
def main(argv: list[str] | None = None) -> int:
|
61
|
+
return run_cli(argv)
|
62
|
+
|
63
|
+
|
64
|
+
if __name__ == "__main__":
|
65
|
+
raise SystemExit(main())
|
src/server.py
CHANGED
@@ -30,7 +30,7 @@ from loguru import logger as log
|
|
30
30
|
from pydantic import BaseModel, Field
|
31
31
|
|
32
32
|
from src import events
|
33
|
-
from src.config import Config, MCPServerConfig,
|
33
|
+
from src.config import Config, MCPServerConfig, clear_json_file_cache
|
34
34
|
from src.config import get_config_dir as _get_cfg_dir # type: ignore[attr-defined]
|
35
35
|
from src.middleware.session_tracking import (
|
36
36
|
MCPSessionModel,
|
@@ -275,7 +275,7 @@ class OpenEdisonProxy:
|
|
275
275
|
|
276
276
|
# Clear cache for the config file, if it was config.json
|
277
277
|
if name == "config.json":
|
278
|
-
|
278
|
+
clear_json_file_cache()
|
279
279
|
|
280
280
|
return {"status": "ok"}
|
281
281
|
except Exception as e: # noqa: BLE001
|
@@ -1,21 +0,0 @@
|
|
1
|
-
src/__init__.py,sha256=QWeZdjAm2D2B0eWhd8m2-DPpWvIP26KcNJxwEoU1oEQ,254
|
2
|
-
src/__main__.py,sha256=kQsaVyzRa_ESC57JpKDSQJAHExuXme0rM5beJsYxFeA,161
|
3
|
-
src/cli.py,sha256=Mz8nMlCHJk0Il8Kr6wB5iKPPMb3WpbBfU2PPLsnYicE,10235
|
4
|
-
src/config.py,sha256=CHrKta7WZbwhKy7FK9y1FEmhbg6VyI5mXhRJ0GHGM6s,10130
|
5
|
-
src/events.py,sha256=rBH7rnaSWZ7GIC8zyBTwpcvIKWmKYCki-DNGgJhxPow,5001
|
6
|
-
src/oauth_manager.py,sha256=qcQa5BDRZr4bjqiXNflCnrXOh9mo9JVjvP2Caseg2Uc,9943
|
7
|
-
src/permissions.py,sha256=NGAnlG_z59HEiVA-k3cYvwmmiuHzxuNb5Tbd5umbL00,10483
|
8
|
-
src/server.py,sha256=DLs-ak35cyEDROctDjzI6-C_ymWe3RekETW0XA5ag_M,45650
|
9
|
-
src/single_user_mcp.py,sha256=rJrlqHcIubGkos_24ux5rb3OoKYDzvagCHghhfDeXTI,18535
|
10
|
-
src/telemetry.py,sha256=-RZPIjpI53zbsKmp-63REeZ1JirWHV5WvpSRa2nqZEk,11321
|
11
|
-
src/frontend_dist/index.html,sha256=s95FMkH8VLisvawLH7bZxbLzRUFvMhHkH6ZMzpVBngs,673
|
12
|
-
src/frontend_dist/sw.js,sha256=rihX1es-vWwjmtnXyaksJjs2dio6MVAOTAWwQPeJUYw,2164
|
13
|
-
src/frontend_dist/assets/index-BUUcUfTt.js,sha256=awoyPI6u0v6ao2iarZdSkrSDUvyU8aNkMLqHMvgVgyY,257666
|
14
|
-
src/frontend_dist/assets/index-o6_8mdM8.css,sha256=nwmX_6q55mB9463XN2JM8BdeihjkALpQK83Fc3_iGvE,15936
|
15
|
-
src/middleware/data_access_tracker.py,sha256=bArBffWgYmvxOx9z_pgXQhogvnWQcc1m6WvEblDD4gw,15039
|
16
|
-
src/middleware/session_tracking.py,sha256=5W1VH9HNqIZeX0HNxDEm41U4GY6SqKSXtApDEeZK2qo,23084
|
17
|
-
open_edison-0.1.34.dist-info/METADATA,sha256=UkIWgEvTC1MzzM9xu2sTg4Ik2MVoG99pAE5W2UOa1d4,12077
|
18
|
-
open_edison-0.1.34.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
19
|
-
open_edison-0.1.34.dist-info/entry_points.txt,sha256=qNAkJcnoTXRhj8J--3PDmXz_TQKdB8H_0C9wiCtDIyA,72
|
20
|
-
open_edison-0.1.34.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
21
|
-
open_edison-0.1.34.dist-info/RECORD,,
|
File without changes
|
File without changes
|