open-edison 0.1.80rc1__py3-none-any.whl → 0.1.84__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.80rc1.dist-info → open_edison-0.1.84.dist-info}/METADATA +2 -1
- {open_edison-0.1.80rc1.dist-info → open_edison-0.1.84.dist-info}/RECORD +16 -15
- src/cli.py +27 -4
- src/config.py +57 -0
- src/config.pyi +5 -0
- src/demos/trifecta.py +110 -0
- src/mcp_importer/importers.py +19 -7
- src/mcp_importer/parsers.py +6 -5
- src/mcp_importer/paths.py +31 -12
- src/middleware/session_tracking.py +24 -1
- src/permissions.py +4 -20
- src/server.py +8 -21
- src/setup_tui/main.py +39 -5
- {open_edison-0.1.80rc1.dist-info → open_edison-0.1.84.dist-info}/WHEEL +0 -0
- {open_edison-0.1.80rc1.dist-info → open_edison-0.1.84.dist-info}/entry_points.txt +0 -0
- {open_edison-0.1.80rc1.dist-info → open_edison-0.1.84.dist-info}/licenses/LICENSE +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: open-edison
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.84
|
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
|
@@ -16,6 +16,7 @@ Requires-Dist: opentelemetry-api>=1.36.0
|
|
16
16
|
Requires-Dist: opentelemetry-exporter-otlp>=1.36.0
|
17
17
|
Requires-Dist: opentelemetry-sdk>=1.36.0
|
18
18
|
Requires-Dist: python-dotenv>=1.0.1
|
19
|
+
Requires-Dist: python-rapidjson>=1.21
|
19
20
|
Requires-Dist: pyyaml>=6.0.2
|
20
21
|
Requires-Dist: questionary>=2.1.1
|
21
22
|
Requires-Dist: sqlalchemy>=2.0.41
|
@@ -1,17 +1,18 @@
|
|
1
1
|
src/__init__.py,sha256=bEYMwBiuW9jzF07iWhas4Vb30EcpnqfpNfz_Q6yO1jU,209
|
2
2
|
src/__main__.py,sha256=kQsaVyzRa_ESC57JpKDSQJAHExuXme0rM5beJsYxFeA,161
|
3
|
-
src/cli.py,sha256=
|
4
|
-
src/config.py,sha256=
|
5
|
-
src/config.pyi,sha256=
|
3
|
+
src/cli.py,sha256=Ki7XCqNlVzUumfELkNM4zGhWB6_ay6ZTlg0pF9wmlWQ,5628
|
4
|
+
src/config.py,sha256=wLjU18HzmA34rGD6FII3pTlOQvnY4KcnfoIOZ3QXHhQ,13491
|
5
|
+
src/config.pyi,sha256=HJ5l-Kk8W_-qW_4y4kw4aowHSPLf5SOZxB_yJvtmqXc,2260
|
6
6
|
src/events.py,sha256=KkhrQ9CE5-WBBCeDkUgdCZURKsXakNP3Kj3gP91NYQM,5046
|
7
7
|
src/mcp_stdio_capture.py,sha256=SpMnUqQYm207WpiEwBahUxq4JFG9bnmNkUQVotZX7cc,5458
|
8
8
|
src/oauth_manager.py,sha256=qw87VxfRLfvd3YI1EhMmEJJ51N_WJsNpo17GUCvi13c,9971
|
9
9
|
src/oauth_override.py,sha256=C7QS8sPA6JqJDiNZA0FGeXcB7jU-yYu-k8V56QVpsqU,393
|
10
|
-
src/permissions.py,sha256=
|
11
|
-
src/server.py,sha256=
|
10
|
+
src/permissions.py,sha256=Tsfxce3qAeswDLAtOx5961oO89wVDoPpID4DPtoumdU,13802
|
11
|
+
src/server.py,sha256=ixQLW7FO2xDC3jG1lrTi0_Kpi1-Dgz3x-zM-ztlWq1c,46934
|
12
12
|
src/single_user_mcp.py,sha256=veemh0RnwEvvkxI-lA2it8rtnPrCbn3VDR3t4DrE7Kc,25016
|
13
13
|
src/telemetry.py,sha256=-RZPIjpI53zbsKmp-63REeZ1JirWHV5WvpSRa2nqZEk,11321
|
14
14
|
src/vulture_whitelist.py,sha256=CjBOSsarbzbQt_9ATWc8MbruBsYX3hJVa_ysbRD9ZYM,135
|
15
|
+
src/demos/trifecta.py,sha256=y-7ofZ9uu0FZgMGRgDZ2bMQYGbRFSEURk5voJBayOyI,3676
|
15
16
|
src/frontend_dist/index.html,sha256=eCG8DLoWNQc5GImBg0Z9dT8HZUMPhAw-4Wc6dEqgKCU,673
|
16
17
|
src/frontend_dist/sw.js,sha256=YXwdeSQVg0BaSyhS2REI2DorNEO1kGUn_YCFw14D8VI,3321
|
17
18
|
src/frontend_dist/assets/index-D05VN_1l.css,sha256=TVt1HETNMBbfMcrUfYuEE0a-tfpxCsATihxmA8Jp890,17953
|
@@ -23,19 +24,19 @@ src/mcp_importer/cli.py,sha256=Pe0GLWm1nMd1VuNXOSkxIrFZuGNFc9dNvfBsvf-bdBI,3487
|
|
23
24
|
src/mcp_importer/export_cli.py,sha256=Fw0jDQCI8gGW4BDrJLzWjLUtV4q6v0h2QZ7HF1V2Jcg,6279
|
24
25
|
src/mcp_importer/exporters.py,sha256=NsXBa1FvwPmIak0AJUrfbCwhnBIdGc5oXjTtMblFRwk,11345
|
25
26
|
src/mcp_importer/import_api.py,sha256=wD5yqxWwFfn1MQNKE79rEeyZODdmPgUDhsRYdCJYh4Q,59
|
26
|
-
src/mcp_importer/importers.py,sha256=
|
27
|
+
src/mcp_importer/importers.py,sha256=NSZURilb-pmCZQ6ydyHIoMrZ-18CvREG2UN3MvuX3yY,2677
|
27
28
|
src/mcp_importer/merge.py,sha256=KIGT7UgbAm07-LdyoUXEJ7ABSIiPTFlj_qjz669yFxg,1569
|
28
|
-
src/mcp_importer/parsers.py,sha256=
|
29
|
-
src/mcp_importer/paths.py,sha256=
|
29
|
+
src/mcp_importer/parsers.py,sha256=IkLRtoqP9i6rNngb_mNE_YuEoOCMxR2rkyC_DtOgFIg,6366
|
30
|
+
src/mcp_importer/paths.py,sha256=JDSB5SLjk16J2qxo921HHUAst0s3EcGlrwqYoruJ11k,3408
|
30
31
|
src/mcp_importer/quick_cli.py,sha256=Vv2vjNzpSOaic0YHFbPAuX0nZByawS2kDw6KiCtEX3A,1798
|
31
32
|
src/mcp_importer/types.py,sha256=nSaOLGqpCmA3R14QCO6wrpgX75VaLz9HfslUWzw_GPQ,102
|
32
33
|
src/middleware/data_access_tracker.py,sha256=ZW-E44U_iOE4jRArRmnQ1HK_zRKs_WZvz4Aa49wXaZk,17105
|
33
|
-
src/middleware/session_tracking.py,sha256=
|
34
|
+
src/middleware/session_tracking.py,sha256=cmmeDzI19uQ3WBUbKkIOGfVEDNaFkpI9M2Q3-J3xhGE,27945
|
34
35
|
src/setup_tui/__init__.py,sha256=mDFrQoiOtQOHc0sFfGKrNXVLEDeB1S0O5aISBVzfxYo,184
|
35
|
-
src/setup_tui/main.py,sha256=
|
36
|
+
src/setup_tui/main.py,sha256=Cc1T3qn4V4WST9Q_ToOJLBjP16mZRNFUBmQ9HEkYQHU,12341
|
36
37
|
src/tools/io.py,sha256=hhc4pv3eUzYWSZ7BbThclxSMwWBQaGMoGsItIPf_pco,1047
|
37
|
-
open_edison-0.1.
|
38
|
-
open_edison-0.1.
|
39
|
-
open_edison-0.1.
|
40
|
-
open_edison-0.1.
|
41
|
-
open_edison-0.1.
|
38
|
+
open_edison-0.1.84.dist-info/METADATA,sha256=bwslnOnwoCbrWqN1Omnv5r340Z5kOPG5RpGeVO0f1L4,12031
|
39
|
+
open_edison-0.1.84.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
40
|
+
open_edison-0.1.84.dist-info/entry_points.txt,sha256=YiGNm9x2I00hgT10HDyB4gxC1LcaV_mu8bXFjolu0Yw,171
|
41
|
+
open_edison-0.1.84.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
42
|
+
open_edison-0.1.84.dist-info/RECORD,,
|
src/cli.py
CHANGED
@@ -6,18 +6,18 @@ Provides the `open-edison` executable when installed via pip/uvx/pipx.
|
|
6
6
|
import argparse
|
7
7
|
import asyncio
|
8
8
|
import os
|
9
|
+
import sys
|
9
10
|
from pathlib import Path
|
10
11
|
from typing import Any, NoReturn
|
11
12
|
|
12
|
-
from loguru import logger as
|
13
|
+
from loguru import logger as log
|
13
14
|
|
14
15
|
from src.config import Config, get_config_dir, get_config_json_path
|
16
|
+
from src.demos.trifecta import demo_config_dir, run_trifecta_demo
|
15
17
|
from src.mcp_importer.cli import run_cli
|
16
18
|
from src.server import OpenEdisonProxy
|
17
19
|
from src.setup_tui.main import run_import_tui
|
18
20
|
|
19
|
-
log: Any = _log
|
20
|
-
|
21
21
|
|
22
22
|
def _parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
23
23
|
parser: Any = argparse.ArgumentParser(
|
@@ -98,6 +98,13 @@ def _parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
98
98
|
action="store_true",
|
99
99
|
help="Show changes without writing to config.json",
|
100
100
|
)
|
101
|
+
_ = subparsers.add_parser(
|
102
|
+
"demo-trifecta",
|
103
|
+
help="Run the Simple Trifecta Demo setup and print the prompt",
|
104
|
+
description=(
|
105
|
+
"Seeds a demo secret file in /tmp, checks config hints, and prints the demo prompt."
|
106
|
+
),
|
107
|
+
)
|
101
108
|
|
102
109
|
return parser.parse_args(argv)
|
103
110
|
|
@@ -132,8 +139,24 @@ def main(argv: list[str] | None = None) -> NoReturn: # noqa: C901
|
|
132
139
|
args.command = "run"
|
133
140
|
|
134
141
|
if args.command == "import-mcp":
|
135
|
-
|
142
|
+
# Forward only the subcommand args (exclude the 'import-mcp' token itself)
|
143
|
+
if argv is None:
|
144
|
+
argv = sys.argv[1:]
|
145
|
+
raw_args = list(argv)
|
146
|
+
try:
|
147
|
+
subcmd_index = raw_args.index("import-mcp")
|
148
|
+
sub_argv = raw_args[subcmd_index + 1 :]
|
149
|
+
except ValueError:
|
150
|
+
sub_argv = raw_args
|
151
|
+
|
152
|
+
result_code = run_cli(sub_argv)
|
136
153
|
raise SystemExit(result_code)
|
154
|
+
if args.command == "demo-trifecta":
|
155
|
+
run_trifecta_demo()
|
156
|
+
with demo_config_dir() as demo_dir:
|
157
|
+
os.environ["OPEN_EDISON_CONFIG_DIR"] = str(demo_dir)
|
158
|
+
asyncio.run(_run_server(args))
|
159
|
+
raise SystemExit(0)
|
137
160
|
|
138
161
|
# Run import tui if necessary
|
139
162
|
tui_success = run_import_tui(args, force=args.wizard_force)
|
src/config.py
CHANGED
@@ -63,6 +63,63 @@ def get_config_json_path() -> Path:
|
|
63
63
|
return get_config_dir() / "config.json"
|
64
64
|
|
65
65
|
|
66
|
+
def repo_defaults_path(filename: str) -> Path:
|
67
|
+
"""Path to repository/package-default JSON next to src/.
|
68
|
+
|
69
|
+
Example: repo_defaults_path("tool_permissions.json") -> /.../src/../tool_permissions.json
|
70
|
+
"""
|
71
|
+
return root_dir / filename
|
72
|
+
|
73
|
+
|
74
|
+
def ensure_permissions_file(file_path: Path, *, default_json: dict[str, Any] | None = None) -> Path:
|
75
|
+
"""Ensure a permissions JSON file exists at file_path.
|
76
|
+
|
77
|
+
Behavior:
|
78
|
+
- If file exists: return it
|
79
|
+
- Else: try to copy repo default (next to src/) into place; if not present, write minimal stub
|
80
|
+
- Returns the final path (which should exist unless write failed)
|
81
|
+
"""
|
82
|
+
if file_path.exists():
|
83
|
+
return file_path
|
84
|
+
|
85
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
86
|
+
|
87
|
+
repo_candidate = repo_defaults_path(file_path.name)
|
88
|
+
if repo_candidate.exists():
|
89
|
+
file_path.write_text(repo_candidate.read_text(encoding="utf-8"), encoding="utf-8")
|
90
|
+
log.info(f"Bootstrapped permissions file from defaults: {file_path}")
|
91
|
+
return file_path
|
92
|
+
|
93
|
+
# Minimal stub when no repo default available
|
94
|
+
minimal_obj: dict[str, Any] = default_json if default_json is not None else {"_metadata": {}}
|
95
|
+
file_path.write_text(json.dumps(minimal_obj, indent=2), encoding="utf-8")
|
96
|
+
log.info(f"Created minimal permissions file: {file_path}")
|
97
|
+
return file_path
|
98
|
+
|
99
|
+
|
100
|
+
def resolve_json_path_with_bootstrap(filename: str) -> Path:
|
101
|
+
"""Resolve a JSON config file path with repo bootstrap fallback.
|
102
|
+
|
103
|
+
Precedence:
|
104
|
+
1) Return config-dir path if it already exists
|
105
|
+
2) If repo default exists, attempt to copy it into config-dir and return the copied path
|
106
|
+
3) Return the config-dir target path (may not exist yet)
|
107
|
+
"""
|
108
|
+
base = get_config_dir()
|
109
|
+
target = base / filename
|
110
|
+
if target.exists():
|
111
|
+
return target
|
112
|
+
|
113
|
+
repo_candidate = repo_defaults_path(filename)
|
114
|
+
if repo_candidate.exists():
|
115
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
116
|
+
target.write_text(repo_candidate.read_text(encoding="utf-8"), encoding="utf-8")
|
117
|
+
return target
|
118
|
+
raise FileNotFoundError(
|
119
|
+
f"File {filename} not found in the src repo and could not be bootstrapped"
|
120
|
+
)
|
121
|
+
|
122
|
+
|
66
123
|
@dataclass
|
67
124
|
class ServerConfig:
|
68
125
|
"""Server configuration"""
|
src/config.pyi
CHANGED
@@ -7,6 +7,11 @@ root_dir: Path
|
|
7
7
|
|
8
8
|
def get_config_dir() -> Path: ...
|
9
9
|
def get_config_json_path() -> Path: ...
|
10
|
+
def repo_defaults_path(filename: str) -> Path: ...
|
11
|
+
def ensure_permissions_file(
|
12
|
+
file_path: Path, *, default_json: dict[str, Any] | None = None
|
13
|
+
) -> Path: ...
|
14
|
+
def resolve_json_path_with_bootstrap(filename: str) -> Path: ...
|
10
15
|
|
11
16
|
class ServerConfig:
|
12
17
|
host: str
|
src/demos/trifecta.py
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
"""
|
2
|
+
Trifecta demo runner used by the CLI and the standalone script.
|
3
|
+
|
4
|
+
This module seeds a secret file under the tmp directory, checks for basic
|
5
|
+
config hints, and prints the user prompt found at demo/trifecta_user_prompt.txt.
|
6
|
+
|
7
|
+
Additionally, it provides a helper to create a temporary configuration
|
8
|
+
directory containing `config.json` and the three permissions JSON files
|
9
|
+
so the demo can run with a known-good configuration without touching the
|
10
|
+
user's real config directory.
|
11
|
+
"""
|
12
|
+
|
13
|
+
import sys
|
14
|
+
from collections.abc import Generator
|
15
|
+
from contextlib import contextmanager
|
16
|
+
from datetime import UTC, datetime
|
17
|
+
from pathlib import Path
|
18
|
+
from tempfile import TemporaryDirectory
|
19
|
+
|
20
|
+
import questionary
|
21
|
+
from loguru import logger as log
|
22
|
+
|
23
|
+
|
24
|
+
def _get_tmp_root() -> Path:
|
25
|
+
if sys.platform == "darwin":
|
26
|
+
return Path("/private/tmp")
|
27
|
+
return Path("/tmp")
|
28
|
+
|
29
|
+
|
30
|
+
TMP_ROOT = _get_tmp_root()
|
31
|
+
SECRET_DIR = TMP_ROOT / "open-edison"
|
32
|
+
SECRET_FILE = SECRET_DIR / "mysecretdetails.txt"
|
33
|
+
|
34
|
+
|
35
|
+
def _load_project_version(pyproject_path: Path) -> str:
|
36
|
+
try:
|
37
|
+
import tomllib
|
38
|
+
|
39
|
+
with pyproject_path.open("rb") as f:
|
40
|
+
data = tomllib.load(f)
|
41
|
+
project = data.get("project", {})
|
42
|
+
return str(project.get("version", "unknown"))
|
43
|
+
except Exception:
|
44
|
+
return "unknown"
|
45
|
+
|
46
|
+
|
47
|
+
def _seed_secret_file(version: str) -> None:
|
48
|
+
SECRET_DIR.mkdir(parents=True, exist_ok=True)
|
49
|
+
installed_ts = datetime.now(UTC).isoformat()
|
50
|
+
lines = [
|
51
|
+
"Open Edison Demo Secret",
|
52
|
+
f"version={version}",
|
53
|
+
f"installed_utc={installed_ts}",
|
54
|
+
]
|
55
|
+
SECRET_FILE.write_text("\n".join(lines), encoding="utf-8")
|
56
|
+
|
57
|
+
|
58
|
+
@contextmanager
|
59
|
+
def demo_config_dir() -> Generator[Path, None, None]:
|
60
|
+
"""Create a temporary config directory for the demo with JSONs.
|
61
|
+
|
62
|
+
Copies `config.json`, `tool_permissions.json`, `resource_permissions.json`,
|
63
|
+
and `prompt_permissions.json` from the repository root into a new directory
|
64
|
+
under the system tmp path. Ensures the `fetch` and `filesystem` servers are
|
65
|
+
enabled in the copied `config.json`. Returns the created directory path.
|
66
|
+
"""
|
67
|
+
repo_root = Path(__file__).resolve().parents[2]
|
68
|
+
filenames = [
|
69
|
+
"config.json",
|
70
|
+
"tool_permissions.json",
|
71
|
+
"resource_permissions.json",
|
72
|
+
"prompt_permissions.json",
|
73
|
+
]
|
74
|
+
|
75
|
+
with TemporaryDirectory(prefix="open-edison-demo-") as tmp_dir:
|
76
|
+
demo_dir = Path(tmp_dir)
|
77
|
+
for filename in filenames:
|
78
|
+
log.debug(f"Copying {filename} to {demo_dir / filename}")
|
79
|
+
(demo_dir / filename).write_text(
|
80
|
+
(repo_root / filename).read_text(encoding="utf-8"), encoding="utf-8"
|
81
|
+
)
|
82
|
+
|
83
|
+
log.info(f"Created temporary demo config directory: {demo_dir}")
|
84
|
+
yield demo_dir
|
85
|
+
|
86
|
+
|
87
|
+
def run_trifecta_demo() -> None:
|
88
|
+
repo_root = Path(__file__).resolve().parents[2]
|
89
|
+
pyproject = repo_root / "pyproject.toml"
|
90
|
+
version = _load_project_version(pyproject)
|
91
|
+
|
92
|
+
_seed_secret_file(version)
|
93
|
+
|
94
|
+
print("\n=== Open Edison: Simple Trifecta Demo Setup ===")
|
95
|
+
print(f"Seeded secret file at: {SECRET_FILE}")
|
96
|
+
print(f"Project version detected: {version}")
|
97
|
+
|
98
|
+
print("\nNext step: Copy/paste this prompt into your MCP client:")
|
99
|
+
print("----------------------------------------------------")
|
100
|
+
prompt_path = repo_root / "demo" / "trifecta_user_prompt.txt"
|
101
|
+
prompt_text = prompt_path.read_text(encoding="utf-8").strip()
|
102
|
+
print(prompt_text)
|
103
|
+
print("----------------------------------------------------")
|
104
|
+
|
105
|
+
questionary.confirm(
|
106
|
+
"Have you copied the prompt into your MCP client and are ready to launch the server now?",
|
107
|
+
default=True,
|
108
|
+
).ask()
|
109
|
+
|
110
|
+
# On return, the cli.py handles launching the server
|
src/mcp_importer/importers.py
CHANGED
@@ -5,7 +5,7 @@ from loguru import logger as log
|
|
5
5
|
|
6
6
|
from src.config import MCPServerConfig
|
7
7
|
|
8
|
-
from .parsers import ImportErrorDetails, parse_mcp_like_json,
|
8
|
+
from .parsers import ImportErrorDetails, parse_mcp_like_json, permissive_read_json
|
9
9
|
from .paths import (
|
10
10
|
find_claude_code_user_all_candidates,
|
11
11
|
find_claude_code_user_settings_file,
|
@@ -22,17 +22,29 @@ def import_from_cursor() -> list[MCPServerConfig]:
|
|
22
22
|
"Cursor MCP config not found (~/.cursor/mcp.json).",
|
23
23
|
Path.home() / ".cursor" / "mcp.json",
|
24
24
|
)
|
25
|
-
data =
|
25
|
+
data = permissive_read_json(files[0])
|
26
26
|
return parse_mcp_like_json(data, default_enabled=True)
|
27
27
|
|
28
28
|
|
29
29
|
def import_from_vscode() -> list[MCPServerConfig]:
|
30
30
|
files = find_vscode_user_mcp_file()
|
31
31
|
if not files:
|
32
|
-
raise ImportErrorDetails(
|
33
|
-
|
34
|
-
|
35
|
-
|
32
|
+
raise ImportErrorDetails(
|
33
|
+
"VSCode configuration not found (checked User/mcp.json and User/settings.json)."
|
34
|
+
)
|
35
|
+
# Try each file; stop at the first that yields MCP servers
|
36
|
+
for f in files:
|
37
|
+
try:
|
38
|
+
log.info("VSCode config detected at: {}", f)
|
39
|
+
data = permissive_read_json(f)
|
40
|
+
parsed = parse_mcp_like_json(data, default_enabled=True)
|
41
|
+
if parsed:
|
42
|
+
return parsed
|
43
|
+
except Exception as e:
|
44
|
+
print(f"Failed reading VSCode config {f}: {e}")
|
45
|
+
# If we saw files but none yielded servers, return empty with info
|
46
|
+
log.info("No MCP servers found in VSCode config candidates; returning empty list")
|
47
|
+
return []
|
36
48
|
|
37
49
|
|
38
50
|
def import_from_claude_code() -> list[MCPServerConfig]:
|
@@ -44,7 +56,7 @@ def import_from_claude_code() -> list[MCPServerConfig]:
|
|
44
56
|
for f in files:
|
45
57
|
try:
|
46
58
|
log.info("Claude Code config detected at: {}", f)
|
47
|
-
data =
|
59
|
+
data = permissive_read_json(f)
|
48
60
|
parsed = parse_mcp_like_json(data, default_enabled=True)
|
49
61
|
if parsed:
|
50
62
|
return parsed
|
src/mcp_importer/parsers.py
CHANGED
@@ -1,10 +1,8 @@
|
|
1
|
-
# pyright: reportUnknownArgumentType=false, reportUnknownVariableType=false, reportMissingImports=false, reportUnknownMemberType=false
|
2
|
-
|
3
|
-
import json
|
4
1
|
import shlex
|
5
2
|
from pathlib import Path
|
6
3
|
from typing import Any, cast
|
7
4
|
|
5
|
+
import rapidjson
|
8
6
|
from loguru import logger as log
|
9
7
|
|
10
8
|
from src.config import MCPServerConfig
|
@@ -38,10 +36,13 @@ class ImportErrorDetails(Exception): # noqa: N818
|
|
38
36
|
self.path = path
|
39
37
|
|
40
38
|
|
41
|
-
|
39
|
+
# Many clients, like vscode, allow comments and trailing commas etc in their settings files.
|
40
|
+
def permissive_read_json(path: Path) -> dict[str, Any]:
|
42
41
|
try:
|
43
42
|
with open(path, encoding="utf-8") as f:
|
44
|
-
loaded =
|
43
|
+
loaded = rapidjson.load(
|
44
|
+
f, parse_mode=rapidjson.PM_COMMENTS | rapidjson.PM_TRAILING_COMMAS
|
45
|
+
)
|
45
46
|
except Exception as e:
|
46
47
|
raise ImportErrorDetails(f"Failed to read JSON from {path}: {e}", path) from e
|
47
48
|
|
src/mcp_importer/paths.py
CHANGED
@@ -17,14 +17,33 @@ def find_cursor_user_file() -> list[Path]:
|
|
17
17
|
return [p] if p.exists() else []
|
18
18
|
|
19
19
|
|
20
|
+
def _vscode_base_dirs() -> list[Path]:
|
21
|
+
"""Return likely VSCode user base directories for different distributions."""
|
22
|
+
home = Path.home()
|
23
|
+
vscode_config_base = (
|
24
|
+
home / "Library" / "Application Support" if is_macos() else home / ".config"
|
25
|
+
)
|
26
|
+
return [
|
27
|
+
x
|
28
|
+
for x in [
|
29
|
+
vscode_config_base / "Code",
|
30
|
+
vscode_config_base / "Code - Insiders",
|
31
|
+
vscode_config_base / "VSCodium",
|
32
|
+
vscode_config_base / "Code - OSS",
|
33
|
+
]
|
34
|
+
if x.exists()
|
35
|
+
]
|
36
|
+
|
37
|
+
|
20
38
|
def find_vscode_user_mcp_file() -> list[Path]:
|
21
|
-
"""Find VSCode user-level MCP config
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
39
|
+
"""Find VSCode user-level MCP config: prefer User/mcp.json; fall back to User/settings.json."""
|
40
|
+
results: list[Path] = []
|
41
|
+
for base in _vscode_base_dirs():
|
42
|
+
for filename in ("mcp.json", "settings.json"):
|
43
|
+
candidate = (base / "User" / filename).resolve()
|
44
|
+
if candidate.exists():
|
45
|
+
results.append(candidate)
|
46
|
+
return list(set(results))
|
28
47
|
|
29
48
|
|
30
49
|
def find_claude_code_user_settings_file() -> list[Path]:
|
@@ -71,11 +90,11 @@ def detect_vscode_config_path() -> Path | None:
|
|
71
90
|
|
72
91
|
|
73
92
|
def get_default_vscode_config_path() -> Path:
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
).resolve()
|
78
|
-
|
93
|
+
# Prefer the first base dir; target mcp.json under User/
|
94
|
+
existing_base_dir = next((base for base in _vscode_base_dirs() if base.exists()), None)
|
95
|
+
if existing_base_dir:
|
96
|
+
return (existing_base_dir / "User" / "mcp.json").resolve()
|
97
|
+
raise RuntimeError("No VSCode base directory found! Are you sure VSCode is installed?")
|
79
98
|
|
80
99
|
|
81
100
|
def get_default_cursor_config_path() -> Path:
|
@@ -16,7 +16,7 @@ from pathlib import Path
|
|
16
16
|
from typing import Any, cast
|
17
17
|
|
18
18
|
import mcp.types as mt
|
19
|
-
from fastmcp.exceptions import ToolError
|
19
|
+
from fastmcp.exceptions import NotFoundError, ToolError
|
20
20
|
from fastmcp.prompts.prompt import FunctionPrompt
|
21
21
|
from fastmcp.resources import FunctionResource
|
22
22
|
from fastmcp.server.middleware import Middleware
|
@@ -277,6 +277,10 @@ class SessionTrackingMiddleware(Middleware):
|
|
277
277
|
# Avoid noisy tracebacks for expected security blocks
|
278
278
|
log.warning(f"MCP request blocked by security policy: {e}")
|
279
279
|
raise
|
280
|
+
except NotFoundError as e:
|
281
|
+
# Tool/prompt/resource not found; avoid full traceback
|
282
|
+
log.warning(f"MCP tool/prompt/resource not found error: {e}")
|
283
|
+
raise
|
280
284
|
except ToolError as e:
|
281
285
|
# Upstream tool failed; avoid noisy traceback here. Specific handlers may format a response.
|
282
286
|
log.warning(f"MCP tool error: {e}")
|
@@ -409,6 +413,25 @@ class SessionTrackingMiddleware(Middleware):
|
|
409
413
|
_persist_session_to_db(session)
|
410
414
|
|
411
415
|
return result
|
416
|
+
except NotFoundError as e:
|
417
|
+
new_tool_call.status = "error"
|
418
|
+
new_tool_call.duration_ms = (time.perf_counter() - start_time) * 1000.0
|
419
|
+
|
420
|
+
_persist_session_to_db(session)
|
421
|
+
|
422
|
+
# Return concise not-found message similar to ToolError handling
|
423
|
+
log.warning(f"Tool not found: {context.message.name}: {e}")
|
424
|
+
return ToolResult(
|
425
|
+
content=[
|
426
|
+
{
|
427
|
+
"type": "text",
|
428
|
+
"text": (
|
429
|
+
f"Tool '{context.message.name}' not found. Please check the tool name or mounted servers."
|
430
|
+
),
|
431
|
+
}
|
432
|
+
],
|
433
|
+
structured_content=None,
|
434
|
+
)
|
412
435
|
except ToolError as e:
|
413
436
|
new_tool_call.status = "error"
|
414
437
|
new_tool_call.duration_ms = (time.perf_counter() - start_time) * 1000.0
|
src/permissions.py
CHANGED
@@ -11,9 +11,9 @@ from functools import cache
|
|
11
11
|
from pathlib import Path
|
12
12
|
from typing import Any
|
13
13
|
|
14
|
-
from loguru import logger as log
|
14
|
+
from loguru import logger as log # pyright: ignore[reportUnknownVariableType]
|
15
15
|
|
16
|
-
from src.config import Config, get_config_dir
|
16
|
+
from src.config import Config, ensure_permissions_file, get_config_dir
|
17
17
|
|
18
18
|
|
19
19
|
def _default_permissions_dir() -> Path:
|
@@ -180,24 +180,8 @@ class Permissions:
|
|
180
180
|
permissions: dict[str, Any] = {}
|
181
181
|
metadata: PermissionsMetadata | None = None
|
182
182
|
|
183
|
-
|
184
|
-
|
185
|
-
# Prefer copying repo/package defaults (next to src/), else create minimal stub.
|
186
|
-
file_path.parent.mkdir(parents=True, exist_ok=True)
|
187
|
-
|
188
|
-
repo_candidate = Path(__file__).parent.parent / file_path.name
|
189
|
-
if repo_candidate.exists():
|
190
|
-
file_path.write_text(repo_candidate.read_text(encoding="utf-8"), encoding="utf-8")
|
191
|
-
log.info(f"Bootstrapped permissions file from defaults: {file_path}")
|
192
|
-
if not file_path.exists():
|
193
|
-
# Create minimal empty structure
|
194
|
-
try:
|
195
|
-
file_path.write_text(json.dumps({"_metadata": {}}), encoding="utf-8")
|
196
|
-
log.info(f"Created empty permissions file: {file_path}")
|
197
|
-
except Exception as e:
|
198
|
-
raise PermissionsError(
|
199
|
-
f"Unable to create permissions file at {file_path}: {e}", file_path
|
200
|
-
) from e
|
183
|
+
# Centralized bootstrap: ensure file exists or create from repo defaults
|
184
|
+
file_path = ensure_permissions_file(file_path)
|
201
185
|
|
202
186
|
with open(file_path) as f:
|
203
187
|
data: dict[str, Any] = json.load(f)
|
src/server.py
CHANGED
@@ -31,7 +31,13 @@ from loguru import logger as log
|
|
31
31
|
from pydantic import BaseModel, Field
|
32
32
|
|
33
33
|
from src import events
|
34
|
-
from src.config import
|
34
|
+
from src.config import (
|
35
|
+
Config,
|
36
|
+
MCPServerConfig,
|
37
|
+
clear_json_file_cache,
|
38
|
+
get_config_json_path,
|
39
|
+
resolve_json_path_with_bootstrap,
|
40
|
+
)
|
35
41
|
from src.config import get_config_dir as _get_cfg_dir # type: ignore[attr-defined]
|
36
42
|
from src.mcp_stdio_capture import (
|
37
43
|
install_stdio_client_stderr_capture as _install_stdio_capture,
|
@@ -209,26 +215,7 @@ class OpenEdisonProxy:
|
|
209
215
|
2) Repository/package defaults next to src/ — and bootstrap a copy into the config dir if missing
|
210
216
|
3) Config dir target path (even if not yet created) as last resort
|
211
217
|
"""
|
212
|
-
|
213
|
-
try:
|
214
|
-
base = _get_cfg_dir()
|
215
|
-
except Exception:
|
216
|
-
base = Path.cwd()
|
217
|
-
target = base / filename
|
218
|
-
if target.exists():
|
219
|
-
return target
|
220
|
-
|
221
|
-
# 2) Repository/package defaults next to src/
|
222
|
-
repo_candidate = Path(__file__).parent.parent / filename
|
223
|
-
if repo_candidate.exists():
|
224
|
-
# Bootstrap a copy into config dir when possible (best effort)
|
225
|
-
with suppress(Exception):
|
226
|
-
target.parent.mkdir(parents=True, exist_ok=True)
|
227
|
-
target.write_text(repo_candidate.read_text(encoding="utf-8"), encoding="utf-8")
|
228
|
-
return target if target.exists() else repo_candidate
|
229
|
-
|
230
|
-
# 3) Fall back to config dir path (will be created on save)
|
231
|
-
return target
|
218
|
+
return resolve_json_path_with_bootstrap(filename)
|
232
219
|
|
233
220
|
async def _serve_json(filename: str) -> Response: # type: ignore[override]
|
234
221
|
if filename not in allowed_json_files:
|
src/setup_tui/main.py
CHANGED
@@ -39,7 +39,9 @@ def show_welcome_screen(*, dry_run: bool = False) -> None:
|
|
39
39
|
print(welcome_text)
|
40
40
|
|
41
41
|
# Prompt to continue
|
42
|
-
questionary.confirm("Ready to begin the setup process?", default=True).ask()
|
42
|
+
if not questionary.confirm("Ready to begin the setup process?", default=True).ask():
|
43
|
+
print("Setup aborted. ")
|
44
|
+
sys.exit(1)
|
43
45
|
|
44
46
|
|
45
47
|
def handle_mcp_source( # noqa: C901
|
@@ -130,6 +132,29 @@ def confirm_configs(configs: list[MCPServerConfig], *, dry_run: bool = False) ->
|
|
130
132
|
).ask()
|
131
133
|
|
132
134
|
|
135
|
+
def select_imported_configs(
|
136
|
+
configs: list[MCPServerConfig], *, dry_run: bool = False
|
137
|
+
) -> list[MCPServerConfig]:
|
138
|
+
"""Present a checkbox list of imported servers and return the selected subset.
|
139
|
+
|
140
|
+
- Arrow keys to navigate; Space to toggle; Enter to confirm.
|
141
|
+
- Defaults to all selected.
|
142
|
+
"""
|
143
|
+
if not configs:
|
144
|
+
return []
|
145
|
+
|
146
|
+
choices = [
|
147
|
+
questionary.Choice(title=config.name, value=config, checked=True) for config in configs
|
148
|
+
]
|
149
|
+
|
150
|
+
selected = questionary.checkbox(
|
151
|
+
"Select the MCP servers to import (Space to toggle, Enter to confirm):",
|
152
|
+
choices=choices,
|
153
|
+
).ask()
|
154
|
+
|
155
|
+
return list(selected or [])
|
156
|
+
|
157
|
+
|
133
158
|
def confirm_apply_configs(client: CLIENT, *, dry_run: bool = False) -> None:
|
134
159
|
if not questionary.confirm(
|
135
160
|
f"Would you like to set up Open Edison for {client.name}? (This will modify your MCP configuration. We will make a back up of your current one if you would like to revert.)",
|
@@ -246,15 +271,24 @@ def run(*, dry_run: bool = False, skip_oauth: bool = False) -> bool: # noqa: C9
|
|
246
271
|
# Deduplicate configs
|
247
272
|
configs = deduplicate_by_name(configs)
|
248
273
|
|
249
|
-
|
250
|
-
|
274
|
+
# Let the user select which servers to import
|
275
|
+
selected_configs = select_imported_configs(configs, dry_run=dry_run)
|
276
|
+
|
277
|
+
if len(selected_configs) == 0:
|
278
|
+
if not questionary.confirm(
|
279
|
+
"No MCP servers selected. Continue without importing any?", default=False
|
280
|
+
).ask():
|
281
|
+
return False
|
282
|
+
else:
|
283
|
+
if not confirm_configs(selected_configs, dry_run=dry_run):
|
284
|
+
return False
|
251
285
|
|
252
286
|
for client in mcp_clients:
|
253
287
|
confirm_apply_configs(client, dry_run=dry_run)
|
254
288
|
|
255
289
|
# Persist imported servers into config.json
|
256
|
-
if len(
|
257
|
-
save_imported_servers(
|
290
|
+
if len(selected_configs) > 0:
|
291
|
+
save_imported_servers(selected_configs, dry_run=dry_run)
|
258
292
|
|
259
293
|
show_manual_setup_screen()
|
260
294
|
|
File without changes
|
File without changes
|
File without changes
|