open-edison 0.1.43__py3-none-any.whl → 0.1.64__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.43.dist-info → open_edison-0.1.64.dist-info}/METADATA +3 -46
- open_edison-0.1.64.dist-info/RECORD +40 -0
- src/cli.py +25 -109
- src/config.py +26 -9
- src/mcp_importer/__main__.py +0 -2
- src/mcp_importer/api.py +254 -44
- src/mcp_importer/export_cli.py +2 -2
- src/mcp_importer/import_api.py +0 -2
- src/mcp_importer/parsers.py +47 -9
- src/mcp_importer/quick_cli.py +0 -2
- src/mcp_importer/types.py +0 -2
- src/oauth_manager.py +3 -1
- src/oauth_override.py +10 -0
- src/permissions.py +24 -1
- src/server.py +30 -10
- src/setup_tui/__init__.py +5 -0
- src/setup_tui/main.py +294 -0
- src/single_user_mcp.py +7 -3
- src/tools/io.py +35 -0
- src/vulture_whitelist.py +3 -0
- open_edison-0.1.43.dist-info/RECORD +0 -35
- {open_edison-0.1.43.dist-info → open_edison-0.1.64.dist-info}/WHEEL +0 -0
- {open_edison-0.1.43.dist-info → open_edison-0.1.64.dist-info}/entry_points.txt +0 -0
- {open_edison-0.1.43.dist-info → open_edison-0.1.64.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.64
|
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
|
@@ -17,6 +17,7 @@ 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
19
|
Requires-Dist: pyyaml>=6.0.2
|
20
|
+
Requires-Dist: questionary>=2.1.1
|
20
21
|
Requires-Dist: sqlalchemy>=2.0.41
|
21
22
|
Requires-Dist: starlette>=0.47.1
|
22
23
|
Requires-Dist: uvicorn>=0.35.0
|
@@ -71,13 +72,7 @@ curl -fsSL https://raw.githubusercontent.com/Edison-Watch/open-edison/main/curl_
|
|
71
72
|
```
|
72
73
|
|
73
74
|
Run locally with uvx: `uvx open-edison`
|
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 src.mcp_importer.quick_cli --yes
|
80
|
-
```
|
75
|
+
That will run the setup wizard if necessary.
|
81
76
|
|
82
77
|
<details>
|
83
78
|
<summary>⬇️ Install Node.js/npm (optional for MCP tools)</summary>
|
@@ -128,44 +123,6 @@ OPEN_EDISON_CONFIG_DIR=~/edison-config open-edison run
|
|
128
123
|
|
129
124
|
</details>
|
130
125
|
|
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 src.mcp_importer.quick_cli --yes
|
143
|
-
```
|
144
|
-
|
145
|
-
- Preview what will be imported (no writes):
|
146
|
-
|
147
|
-
```bash
|
148
|
-
uv run python -m src.mcp_importer --source cursor --dry-run
|
149
|
-
```
|
150
|
-
|
151
|
-
- Import servers into Open Edison `config.json` (merge policy defaults to `skip`):
|
152
|
-
|
153
|
-
```bash
|
154
|
-
uv run python -m src.mcp_importer --source cursor
|
155
|
-
uv run python -m src.mcp_importer --source vscode
|
156
|
-
uv run python -m src.mcp_importer --source claude-code
|
157
|
-
```
|
158
|
-
|
159
|
-
- Point your editor to Open Edison (backup original config and replace with a single Open Edison server):
|
160
|
-
|
161
|
-
```bash
|
162
|
-
uv run python -m src.mcp_importer export --target cursor --yes
|
163
|
-
uv run python -m src.mcp_importer export --target vscode --yes
|
164
|
-
uv run python -m src.mcp_importer export --target claude-code --yes
|
165
|
-
```
|
166
|
-
|
167
|
-
</details>
|
168
|
-
|
169
126
|
<details>
|
170
127
|
<summary><img src="https://img.shields.io/badge/Docker-2CA5E0?style=for-the-badge&logo=docker&logoColor=white" alt="Docker"> Run with Docker</summary>
|
171
128
|
|
@@ -0,0 +1,40 @@
|
|
1
|
+
src/__init__.py,sha256=bEYMwBiuW9jzF07iWhas4Vb30EcpnqfpNfz_Q6yO1jU,209
|
2
|
+
src/__main__.py,sha256=kQsaVyzRa_ESC57JpKDSQJAHExuXme0rM5beJsYxFeA,161
|
3
|
+
src/cli.py,sha256=PH2qPLma0PO1L75OSK06IdPy8RB5gTBp2R1HkacCQ0Q,4736
|
4
|
+
src/config.py,sha256=oO89omLCLoPfEGH6j4WSHQgZbhJZnYou-qdxY54VfDo,11037
|
5
|
+
src/config.pyi,sha256=FgehEGli8ZXSjGlANBgMGv5497q4XskQciOc1fUcxqM,2033
|
6
|
+
src/events.py,sha256=aFQrVXDIZwt55Dz6OtyoXu2yi9evqo-8jZzo3CR2Tto,4965
|
7
|
+
src/oauth_manager.py,sha256=MJ1gHVKiu-pMbskSCxRlZ6xP4wJOr-ELydOdgdUBKKw,9969
|
8
|
+
src/oauth_override.py,sha256=C7QS8sPA6JqJDiNZA0FGeXcB7jU-yYu-k8V56QVpsqU,393
|
9
|
+
src/permissions.py,sha256=dERB8s40gDInsbXtu0pJYDDuZ3_kD8rxXyYYTfrG3qs,11621
|
10
|
+
src/server.py,sha256=WseZks-r07tq7UGX0XFc0OUEzUrB9MGfDiHeYHa3NEE,46593
|
11
|
+
src/single_user_mcp.py,sha256=2xQgrxqulTD_1HySoMNZD5AoADJS5pz41Gn-tEFgBHI,18760
|
12
|
+
src/telemetry.py,sha256=-RZPIjpI53zbsKmp-63REeZ1JirWHV5WvpSRa2nqZEk,11321
|
13
|
+
src/vulture_whitelist.py,sha256=CjBOSsarbzbQt_9ATWc8MbruBsYX3hJVa_ysbRD9ZYM,135
|
14
|
+
src/frontend_dist/index.html,sha256=s95FMkH8VLisvawLH7bZxbLzRUFvMhHkH6ZMzpVBngs,673
|
15
|
+
src/frontend_dist/sw.js,sha256=rihX1es-vWwjmtnXyaksJjs2dio6MVAOTAWwQPeJUYw,2164
|
16
|
+
src/frontend_dist/assets/index-BUUcUfTt.js,sha256=awoyPI6u0v6ao2iarZdSkrSDUvyU8aNkMLqHMvgVgyY,257666
|
17
|
+
src/frontend_dist/assets/index-o6_8mdM8.css,sha256=nwmX_6q55mB9463XN2JM8BdeihjkALpQK83Fc3_iGvE,15936
|
18
|
+
src/mcp_importer/__init__.py,sha256=Mk59pVr7OMGfYGWeSYk8-URfhIcrs3SPLYS7fmJbMII,275
|
19
|
+
src/mcp_importer/__main__.py,sha256=mFcxXFqJMC0SFEqIP-9WVEqLJSYqShC0x1Ht7PQZPm8,479
|
20
|
+
src/mcp_importer/api.py,sha256=N5oVaTj3OMIROLx__UOSr60VMqXXX20JsOHmeHIGP48,17431
|
21
|
+
src/mcp_importer/cli.py,sha256=Pe0GLWm1nMd1VuNXOSkxIrFZuGNFc9dNvfBsvf-bdBI,3487
|
22
|
+
src/mcp_importer/export_cli.py,sha256=Fw0jDQCI8gGW4BDrJLzWjLUtV4q6v0h2QZ7HF1V2Jcg,6279
|
23
|
+
src/mcp_importer/exporters.py,sha256=fSgl6seduoXFp7YnKH26UEaC1sFBnd4whSut7CJLBQs,11348
|
24
|
+
src/mcp_importer/import_api.py,sha256=wD5yqxWwFfn1MQNKE79rEeyZODdmPgUDhsRYdCJYh4Q,59
|
25
|
+
src/mcp_importer/importers.py,sha256=zGN8lT7qQJ95jDTd-ck09j_w5PSvH-uj33TILoHfHbs,2191
|
26
|
+
src/mcp_importer/merge.py,sha256=KIGT7UgbAm07-LdyoUXEJ7ABSIiPTFlj_qjz669yFxg,1569
|
27
|
+
src/mcp_importer/parsers.py,sha256=MDhzODsvX5t1U_CI8byBxCpx6rA4WkpqX4bJMiNE74s,6298
|
28
|
+
src/mcp_importer/paths.py,sha256=4L-cPr7KCM9X9gAUP7Da6ictLNrPWuQ_IM419zqY-2I,2700
|
29
|
+
src/mcp_importer/quick_cli.py,sha256=Vv2vjNzpSOaic0YHFbPAuX0nZByawS2kDw6KiCtEX3A,1798
|
30
|
+
src/mcp_importer/types.py,sha256=nSaOLGqpCmA3R14QCO6wrpgX75VaLz9HfslUWzw_GPQ,102
|
31
|
+
src/middleware/data_access_tracker.py,sha256=bArBffWgYmvxOx9z_pgXQhogvnWQcc1m6WvEblDD4gw,15039
|
32
|
+
src/middleware/session_tracking.py,sha256=5W1VH9HNqIZeX0HNxDEm41U4GY6SqKSXtApDEeZK2qo,23084
|
33
|
+
src/setup_tui/__init__.py,sha256=mDFrQoiOtQOHc0sFfGKrNXVLEDeB1S0O5aISBVzfxYo,184
|
34
|
+
src/setup_tui/main.py,sha256=BM6t8YzWOSn2hDzVhp_nOol93NoHf6fiP4uFLOpx-BQ,11082
|
35
|
+
src/tools/io.py,sha256=hhc4pv3eUzYWSZ7BbThclxSMwWBQaGMoGsItIPf_pco,1047
|
36
|
+
open_edison-0.1.64.dist-info/METADATA,sha256=1X57SDfaooRExNvcCN-vHr1HH1QzbrVLWhJaNIaIEDI,11993
|
37
|
+
open_edison-0.1.64.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
38
|
+
open_edison-0.1.64.dist-info/entry_points.txt,sha256=YiGNm9x2I00hgT10HDyB4gxC1LcaV_mu8bXFjolu0Yw,171
|
39
|
+
open_edison-0.1.64.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
40
|
+
open_edison-0.1.64.dist-info/RECORD,,
|
src/cli.py
CHANGED
@@ -7,8 +7,6 @@ Provides `open-edison` executable when installed via pip/uvx/pipx.
|
|
7
7
|
import argparse
|
8
8
|
import asyncio
|
9
9
|
import os
|
10
|
-
import subprocess as _subprocess
|
11
|
-
from contextlib import suppress
|
12
10
|
from pathlib import Path
|
13
11
|
from typing import Any, NoReturn
|
14
12
|
|
@@ -17,6 +15,7 @@ from loguru import logger as _log # type: ignore[reportMissingImports]
|
|
17
15
|
from src.config import Config, get_config_dir, get_config_json_path
|
18
16
|
from src.mcp_importer.cli import run_cli
|
19
17
|
from src.server import OpenEdisonProxy
|
18
|
+
from src.setup_tui.main import run_import_tui
|
20
19
|
|
21
20
|
log: Any = _log
|
22
21
|
|
@@ -37,6 +36,22 @@ def _parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
37
36
|
parser.add_argument(
|
38
37
|
"--port", type=int, help="Server port override (FastMCP on port, FastAPI on port+1)"
|
39
38
|
)
|
39
|
+
# For the setup wizard
|
40
|
+
parser.add_argument(
|
41
|
+
"--wizard-dry-run",
|
42
|
+
action="store_true",
|
43
|
+
help="(For the setup wizard) Show changes without writing to config.json",
|
44
|
+
)
|
45
|
+
parser.add_argument(
|
46
|
+
"--wizard-skip-oauth",
|
47
|
+
action="store_true",
|
48
|
+
help="(For the setup wizard) Skip OAuth for remote servers (they will be omitted from import)",
|
49
|
+
)
|
50
|
+
parser.add_argument(
|
51
|
+
"--wizard-force",
|
52
|
+
action="store_true",
|
53
|
+
help="(For the setup wizard) Force running the setup wizard even if it has already been run",
|
54
|
+
)
|
40
55
|
# Website runs from packaged assets by default; no extra website flags
|
41
56
|
|
42
57
|
# Subcommands (extensible)
|
@@ -88,89 +103,7 @@ def _parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
88
103
|
return parser.parse_args(argv)
|
89
104
|
|
90
105
|
|
91
|
-
def _spawn_frontend_dev( # noqa: C901 - pragmatic complexity for env probing
|
92
|
-
port: int,
|
93
|
-
override_dir: Path | None = None,
|
94
|
-
config_dir: Path | None = None,
|
95
|
-
) -> tuple[int, _subprocess.Popen[bytes] | None]:
|
96
|
-
"""Try to start the frontend dev server by running `npm run dev`.
|
97
|
-
|
98
|
-
Search order for working directory:
|
99
|
-
1) Packaged project path: <pkg_root>/frontend
|
100
|
-
2) Current working directory (if it contains a package.json)
|
101
|
-
"""
|
102
|
-
candidates: list[Path] = []
|
103
|
-
# Prefer packaged static assets; if present, the backend serves /dashboard
|
104
|
-
static_candidates = [
|
105
|
-
Path(__file__).parent / "frontend_dist", # inside package dir
|
106
|
-
Path(__file__).parent.parent / "frontend_dist", # site-packages root
|
107
|
-
]
|
108
|
-
static_dir = next((p for p in static_candidates if p.exists() and p.is_dir()), None)
|
109
|
-
if static_dir is not None:
|
110
|
-
log.info(
|
111
|
-
f"Packaged dashboard detected at {static_dir}. It will be served at /dashboard by the API server."
|
112
|
-
)
|
113
|
-
# No separate website process needed. Return sentinel port (-1) so caller knows not to warn.
|
114
|
-
return (-1, None)
|
115
|
-
|
116
|
-
if static_dir is None:
|
117
|
-
raise RuntimeError(
|
118
|
-
"No packaged dashboard detected. The website will be served from the frontend directory."
|
119
|
-
)
|
120
|
-
|
121
|
-
pkg_frontend_candidates = [
|
122
|
-
Path(__file__).parent / "frontend", # inside package dir
|
123
|
-
Path(__file__).parent.parent / "frontend", # site-packages root
|
124
|
-
]
|
125
|
-
if override_dir is not None:
|
126
|
-
candidates.append(override_dir)
|
127
|
-
for pf in pkg_frontend_candidates:
|
128
|
-
if pf.exists():
|
129
|
-
candidates.append(pf)
|
130
|
-
if config_dir is not None and (config_dir / "package.json").exists():
|
131
|
-
candidates.append(config_dir)
|
132
|
-
cwd_pkg = Path.cwd()
|
133
|
-
if (cwd_pkg / "package.json").exists():
|
134
|
-
candidates.append(cwd_pkg)
|
135
|
-
|
136
|
-
if not candidates:
|
137
|
-
log.warning(
|
138
|
-
"No frontend directory found (no packaged frontend and no package.json in CWD). Skipping website."
|
139
|
-
)
|
140
|
-
return (port, None)
|
141
|
-
|
142
|
-
for candidate in candidates:
|
143
|
-
try:
|
144
|
-
# If no package.json but directory exists, try a basic npm i per user request
|
145
|
-
if not (candidate / "package.json").exists():
|
146
|
-
log.info(f"No package.json in {candidate}. Running 'npm i' as best effort...")
|
147
|
-
_ = _subprocess.call(["npm", "i"], cwd=str(candidate))
|
148
|
-
|
149
|
-
# Install deps if needed
|
150
|
-
if (
|
151
|
-
not (candidate / "node_modules").exists()
|
152
|
-
and (candidate / "package-lock.json").exists()
|
153
|
-
):
|
154
|
-
log.info(f"Installing frontend dependencies with npm ci in {candidate}...")
|
155
|
-
r_install = _subprocess.call(["npm", "ci"], cwd=str(candidate))
|
156
|
-
if r_install != 0:
|
157
|
-
log.error("Failed to install frontend dependencies")
|
158
|
-
continue
|
159
|
-
|
160
|
-
log.info(f"Starting frontend dev server in {candidate} on port {port}...")
|
161
|
-
cmd_default = ["npm", "run", "dev", "--", "--port", str(port)]
|
162
|
-
proc = _subprocess.Popen(cmd_default, cwd=str(candidate))
|
163
|
-
return (port, proc)
|
164
|
-
except FileNotFoundError:
|
165
|
-
log.error("npm not found. Please install Node.js to run the website dev server.")
|
166
|
-
return (port, None)
|
167
|
-
|
168
|
-
# If all candidates failed
|
169
|
-
return (port, None)
|
170
|
-
|
171
|
-
|
172
106
|
async def _run_server(args: Any) -> None:
|
173
|
-
# TODO check this works as we want it to
|
174
107
|
# Resolve config dir and expose via env for the rest of the app
|
175
108
|
config_dir_arg = getattr(args, "config_dir", None)
|
176
109
|
if config_dir_arg is not None:
|
@@ -186,44 +119,27 @@ async def _run_server(args: Any) -> None:
|
|
186
119
|
log.info(f"Using config directory: {config_dir}")
|
187
120
|
proxy = OpenEdisonProxy(host=host, port=port)
|
188
121
|
|
189
|
-
# Website served from packaged assets by default; still detect and log
|
190
|
-
frontend_proc = None
|
191
|
-
used_port, frontend_proc = _spawn_frontend_dev(5173, None, config_dir)
|
192
|
-
if frontend_proc is None and used_port == -1:
|
193
|
-
log.info("Frontend is being served from packaged assets at /dashboard")
|
194
|
-
|
195
122
|
try:
|
196
123
|
await proxy.start()
|
197
|
-
_ = await asyncio.Event().wait()
|
198
124
|
except KeyboardInterrupt:
|
199
125
|
log.info("Received shutdown signal")
|
200
|
-
finally:
|
201
|
-
if frontend_proc is not None:
|
202
|
-
with suppress(Exception):
|
203
|
-
frontend_proc.terminate()
|
204
|
-
_ = frontend_proc.wait(timeout=5)
|
205
|
-
with suppress(Exception):
|
206
|
-
frontend_proc.kill()
|
207
|
-
|
208
|
-
|
209
|
-
def _run_website(port: int, website_dir: Path | None = None) -> int:
|
210
|
-
# Use the same spawning logic, then return 0 if started or 1 if failed
|
211
|
-
_, proc = _spawn_frontend_dev(port, website_dir)
|
212
|
-
return 0 if proc is not None else 1
|
213
126
|
|
214
127
|
|
215
128
|
def main(argv: list[str] | None = None) -> NoReturn: # noqa: C901
|
216
129
|
args = _parse_args(argv)
|
217
130
|
|
218
|
-
if
|
219
|
-
|
220
|
-
raise SystemExit(exit_code)
|
131
|
+
if args.command is None:
|
132
|
+
args.command = "run"
|
221
133
|
|
222
|
-
if
|
134
|
+
if args.command == "import-mcp":
|
223
135
|
result_code = run_cli(argv)
|
224
136
|
raise SystemExit(result_code)
|
225
137
|
|
226
|
-
#
|
138
|
+
# Run import tui if necessary
|
139
|
+
tui_success = run_import_tui(args, force=args.wizard_force)
|
140
|
+
if not tui_success:
|
141
|
+
raise SystemExit(1)
|
142
|
+
|
227
143
|
try:
|
228
144
|
asyncio.run(_run_server(args))
|
229
145
|
raise SystemExit(0)
|
src/config.py
CHANGED
@@ -108,12 +108,21 @@ class MCPServerConfig:
|
|
108
108
|
Remote servers use mcp-remote with HTTPS URLs and may require OAuth.
|
109
109
|
Local servers run as child processes and don't need OAuth.
|
110
110
|
"""
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
111
|
+
if self.command != "npx":
|
112
|
+
return False
|
113
|
+
|
114
|
+
# Be tolerant of npx flags by scanning for 'mcp-remote' and the subsequent HTTPS URL
|
115
|
+
try:
|
116
|
+
if "mcp-remote" not in self.args:
|
117
|
+
return False
|
118
|
+
idx: int = self.args.index("mcp-remote")
|
119
|
+
# Look for first https?:// argument after 'mcp-remote'
|
120
|
+
for candidate in self.args[idx + 1 :]:
|
121
|
+
if candidate.startswith(("https://", "http://")):
|
122
|
+
return candidate.startswith("https://")
|
123
|
+
return False
|
124
|
+
except Exception:
|
125
|
+
return False
|
117
126
|
|
118
127
|
def get_remote_url(self) -> str | None:
|
119
128
|
"""
|
@@ -122,9 +131,17 @@ class MCPServerConfig:
|
|
122
131
|
Returns:
|
123
132
|
The HTTPS URL if this is a remote server, None otherwise
|
124
133
|
"""
|
125
|
-
|
126
|
-
|
127
|
-
|
134
|
+
# Reuse the same tolerant parsing as is_remote_server
|
135
|
+
if self.command != "npx" or "mcp-remote" not in self.args:
|
136
|
+
return None
|
137
|
+
try:
|
138
|
+
idx: int = self.args.index("mcp-remote")
|
139
|
+
for candidate in self.args[idx + 1 :]:
|
140
|
+
if candidate.startswith(("https://", "http://")):
|
141
|
+
return candidate
|
142
|
+
return None
|
143
|
+
except Exception:
|
144
|
+
return None
|
128
145
|
|
129
146
|
|
130
147
|
@dataclass
|