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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: open-edison
3
- Version: 0.1.43
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 getattr(args, "command", None) == "website":
219
- exit_code = _run_website(port=args.port, website_dir=getattr(args, "dir", None))
220
- raise SystemExit(exit_code)
131
+ if args.command is None:
132
+ args.command = "run"
221
133
 
222
- if getattr(args, "command", None) == "import-mcp":
134
+ if args.command == "import-mcp":
223
135
  result_code = run_cli(argv)
224
136
  raise SystemExit(result_code)
225
137
 
226
- # default: run server (top-level flags)
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
- return (
112
- self.command == "npx"
113
- and len(self.args) >= 3
114
- and self.args[1] == "mcp-remote"
115
- and self.args[2].startswith("https://")
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
- if self.is_remote_server():
126
- return self.args[2]
127
- return None
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
@@ -1,5 +1,3 @@
1
- from __future__ import annotations
2
-
3
1
  import sys
4
2
 
5
3
  from src.mcp_importer.cli import run_cli as import_run_cli