open-edison 0.1.44__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.44.dist-info → open_edison-0.1.64.dist-info}/METADATA +2 -21
- 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/main.py +155 -18
- src/single_user_mcp.py +7 -3
- src/tools/io.py +35 -0
- src/vulture_whitelist.py +3 -0
- open_edison-0.1.44.dist-info/RECORD +0 -37
- {open_edison-0.1.44.dist-info → open_edison-0.1.64.dist-info}/WHEEL +0 -0
- {open_edison-0.1.44.dist-info → open_edison-0.1.64.dist-info}/entry_points.txt +0 -0
- {open_edison-0.1.44.dist-info → open_edison-0.1.64.dist-info}/licenses/LICENSE +0 -0
src/mcp_importer/export_cli.py
CHANGED
@@ -5,8 +5,10 @@ from loguru import logger as log
|
|
5
5
|
|
6
6
|
from .exporters import ExportError, export_to_claude_code, export_to_cursor, export_to_vscode
|
7
7
|
from .paths import (
|
8
|
+
detect_claude_code_config_path,
|
8
9
|
detect_cursor_config_path,
|
9
10
|
detect_vscode_config_path,
|
11
|
+
get_default_claude_code_config_path,
|
10
12
|
get_default_cursor_config_path,
|
11
13
|
get_default_vscode_config_path,
|
12
14
|
)
|
@@ -135,8 +137,6 @@ def _handle_vscode(args: argparse.Namespace) -> int:
|
|
135
137
|
|
136
138
|
|
137
139
|
def _handle_claude_code(args: argparse.Namespace) -> int:
|
138
|
-
from .paths import detect_claude_code_config_path, get_default_claude_code_config_path
|
139
|
-
|
140
140
|
detected = detect_claude_code_config_path()
|
141
141
|
target_path: Path = detected if detected else get_default_claude_code_config_path()
|
142
142
|
|
src/mcp_importer/import_api.py
CHANGED
src/mcp_importer/parsers.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# pyright: reportUnknownArgumentType=false, reportUnknownVariableType=false, reportMissingImports=false, reportUnknownMemberType=false
|
2
2
|
|
3
3
|
import json
|
4
|
+
import shlex
|
4
5
|
from pathlib import Path
|
5
6
|
from typing import Any, cast
|
6
7
|
|
@@ -50,7 +51,7 @@ def safe_read_json(path: Path) -> dict[str, Any]:
|
|
50
51
|
return data
|
51
52
|
|
52
53
|
|
53
|
-
def _coerce_server_entry(name: str, node: dict[str, Any], default_enabled: bool) -> Any:
|
54
|
+
def _coerce_server_entry(name: str, node: dict[str, Any], default_enabled: bool) -> Any: # noqa: C901
|
54
55
|
command_val = node.get("command", "")
|
55
56
|
command = str(command_val) if isinstance(command_val, str) else ""
|
56
57
|
|
@@ -67,12 +68,35 @@ def _coerce_server_entry(name: str, node: dict[str, Any], default_enabled: bool)
|
|
67
68
|
|
68
69
|
args: list[str] = [str(a) for a in args_raw]
|
69
70
|
|
71
|
+
# If command is provided as a full string with flags, split into program + args
|
72
|
+
if command and (" " in command or command.endswith(("\t", "\n"))):
|
73
|
+
try:
|
74
|
+
parts = shlex.split(command)
|
75
|
+
if parts:
|
76
|
+
command = parts[0]
|
77
|
+
# Prepend split args before any provided args to preserve order
|
78
|
+
args = parts[1:] + args
|
79
|
+
except Exception:
|
80
|
+
# If shlex fails, keep original command/args
|
81
|
+
pass
|
82
|
+
|
70
83
|
env_raw = node.get("env") or node.get("environment") or {}
|
71
84
|
env: dict[str, str] = {}
|
72
85
|
if isinstance(env_raw, dict):
|
73
86
|
for k, v in env_raw.items():
|
74
87
|
env[str(k)] = str(v)
|
75
88
|
|
89
|
+
# Support Cursor-style remote config: { "url": "...", "headers": {...} }
|
90
|
+
# Translate to `npx mcp-remote <url> [--header Key: Value]*` so downstream verification works.
|
91
|
+
url_val = node.get("url")
|
92
|
+
if isinstance(url_val, str) and url_val:
|
93
|
+
command = "npx"
|
94
|
+
args = ["-y", "mcp-remote", url_val]
|
95
|
+
headers_raw = node.get("headers")
|
96
|
+
if isinstance(headers_raw, dict):
|
97
|
+
for hk, hv in headers_raw.items():
|
98
|
+
args.extend(["--header", f"{str(hk)}: {str(hv)}"])
|
99
|
+
|
76
100
|
enabled = bool(node.get("enabled", default_enabled))
|
77
101
|
|
78
102
|
roots_raw = node.get("roots") or node.get("rootPaths") or []
|
@@ -135,14 +159,28 @@ def _collect_nested(data: dict[str, Any], default_enabled: bool) -> list[Any]:
|
|
135
159
|
return results
|
136
160
|
|
137
161
|
|
138
|
-
def
|
162
|
+
def deduplicate_by_name(servers: list[MCPServerConfig]) -> list[MCPServerConfig]:
|
163
|
+
result: list[MCPServerConfig] = []
|
164
|
+
names = set()
|
165
|
+
for server in servers:
|
166
|
+
if server.name not in names:
|
167
|
+
names.add(server.name)
|
168
|
+
result.append(server)
|
169
|
+
return result
|
170
|
+
|
171
|
+
|
172
|
+
def parse_mcp_like_json(
|
173
|
+
data: dict[str, Any], default_enabled: bool = True
|
174
|
+
) -> list[MCPServerConfig]:
|
139
175
|
# First, try top-level keys
|
140
176
|
top_level = _collect_top_level(data, default_enabled)
|
177
|
+
res: list[MCPServerConfig] = []
|
141
178
|
if top_level:
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
179
|
+
res = top_level
|
180
|
+
else:
|
181
|
+
# Then, try nested structures heuristically
|
182
|
+
nested = _collect_nested(data, default_enabled)
|
183
|
+
if not nested:
|
184
|
+
log.debug("No MCP-like entries detected in provided data")
|
185
|
+
res = nested
|
186
|
+
return deduplicate_by_name(res)
|
src/mcp_importer/quick_cli.py
CHANGED
src/mcp_importer/types.py
CHANGED
src/oauth_manager.py
CHANGED
@@ -19,6 +19,8 @@ from fastmcp.client.auth.oauth import (
|
|
19
19
|
)
|
20
20
|
from loguru import logger as log
|
21
21
|
|
22
|
+
from src.oauth_override import OpenEdisonOAuth
|
23
|
+
|
22
24
|
|
23
25
|
class OAuthStatus(Enum):
|
24
26
|
"""OAuth authentication status for MCP servers."""
|
@@ -209,7 +211,7 @@ class OAuthManager:
|
|
209
211
|
return None
|
210
212
|
|
211
213
|
try:
|
212
|
-
oauth =
|
214
|
+
oauth = OpenEdisonOAuth(
|
213
215
|
mcp_url=mcp_url,
|
214
216
|
scopes=scopes or info.scopes,
|
215
217
|
client_name=client_name or info.client_name,
|
src/oauth_override.py
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
import webbrowser
|
2
|
+
|
3
|
+
from fastmcp.client.auth.oauth import OAuth as _FastMCPOAuth
|
4
|
+
|
5
|
+
|
6
|
+
class OpenEdisonOAuth(_FastMCPOAuth):
|
7
|
+
async def redirect_handler(self, authorization_url: str) -> None: # noqa: ARG002
|
8
|
+
# Print a clean, single-line URL and still open the browser.
|
9
|
+
print(f"OAuth authorization URL: {authorization_url}", flush=True)
|
10
|
+
webbrowser.open(authorization_url)
|
src/permissions.py
CHANGED
@@ -7,6 +7,7 @@ Reads tool, resource, and prompt permission files and provides a singleton inter
|
|
7
7
|
|
8
8
|
import json
|
9
9
|
from dataclasses import dataclass
|
10
|
+
from functools import cache
|
10
11
|
from pathlib import Path
|
11
12
|
from typing import Any
|
12
13
|
|
@@ -158,6 +159,12 @@ class Permissions:
|
|
158
159
|
)
|
159
160
|
|
160
161
|
@classmethod
|
162
|
+
def clear_permissions_file_cache(cls) -> None:
|
163
|
+
"""Clear the cache for the JSON permissions files"""
|
164
|
+
cls._load_permission_file.cache_clear()
|
165
|
+
|
166
|
+
@classmethod
|
167
|
+
@cache
|
161
168
|
def _load_permission_file(
|
162
169
|
cls,
|
163
170
|
file_path: Path,
|
@@ -171,7 +178,23 @@ class Permissions:
|
|
171
178
|
metadata: PermissionsMetadata | None = None
|
172
179
|
|
173
180
|
if not file_path.exists():
|
174
|
-
|
181
|
+
# Bootstrap missing permissions files on first run.
|
182
|
+
# Prefer copying repo/package defaults (next to src/), else create minimal stub.
|
183
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
184
|
+
|
185
|
+
repo_candidate = Path(__file__).parent.parent / file_path.name
|
186
|
+
if repo_candidate.exists():
|
187
|
+
file_path.write_text(repo_candidate.read_text(encoding="utf-8"), encoding="utf-8")
|
188
|
+
log.info(f"Bootstrapped permissions file from defaults: {file_path}")
|
189
|
+
if not file_path.exists():
|
190
|
+
# Create minimal empty structure
|
191
|
+
try:
|
192
|
+
file_path.write_text(json.dumps({"_metadata": {}}), encoding="utf-8")
|
193
|
+
log.info(f"Created empty permissions file: {file_path}")
|
194
|
+
except Exception as e:
|
195
|
+
raise PermissionsError(
|
196
|
+
f"Unable to create permissions file at {file_path}: {e}", file_path
|
197
|
+
) from e
|
175
198
|
|
176
199
|
with open(file_path) as f:
|
177
200
|
data: dict[str, Any] = json.load(f)
|
src/server.py
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
"""
|
2
|
-
Open Edison Server
|
3
|
-
|
4
|
-
|
5
|
-
No multi-user support, no complex routing - just a straightforward proxy.
|
2
|
+
Open Edison MCP Proxy Server
|
3
|
+
Main server entrypoint for FastAPI and FastMCP integration.
|
4
|
+
See README for usage and configuration details.
|
6
5
|
"""
|
7
6
|
|
8
7
|
import asyncio
|
9
8
|
import json
|
9
|
+
import signal
|
10
10
|
import traceback
|
11
11
|
from collections.abc import Awaitable, Callable, Coroutine
|
12
12
|
from contextlib import suppress
|
@@ -25,6 +25,7 @@ from fastapi.responses import (
|
|
25
25
|
)
|
26
26
|
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
27
27
|
from fastapi.staticfiles import StaticFiles
|
28
|
+
from fastmcp import Client as FastMCPClient
|
28
29
|
from fastmcp import FastMCP
|
29
30
|
from loguru import logger as log
|
30
31
|
from pydantic import BaseModel, Field
|
@@ -37,6 +38,8 @@ from src.middleware.session_tracking import (
|
|
37
38
|
create_db_session,
|
38
39
|
)
|
39
40
|
from src.oauth_manager import OAuthStatus, get_oauth_manager
|
41
|
+
from src.oauth_override import OpenEdisonOAuth
|
42
|
+
from src.permissions import Permissions
|
40
43
|
from src.single_user_mcp import SingleUserMCP
|
41
44
|
from src.telemetry import initialize_telemetry, set_servers_installed
|
42
45
|
|
@@ -276,6 +279,12 @@ class OpenEdisonProxy:
|
|
276
279
|
# Clear cache for the config file, if it was config.json
|
277
280
|
if name == "config.json":
|
278
281
|
clear_json_file_cache()
|
282
|
+
elif name in (
|
283
|
+
"tool_permissions.json",
|
284
|
+
"resource_permissions.json",
|
285
|
+
"prompt_permissions.json",
|
286
|
+
):
|
287
|
+
Permissions.clear_permissions_file_cache()
|
279
288
|
|
280
289
|
return {"status": "ok"}
|
281
290
|
except Exception as e: # noqa: BLE001
|
@@ -397,6 +406,7 @@ class OpenEdisonProxy:
|
|
397
406
|
host=self.host,
|
398
407
|
port=self.port + 1,
|
399
408
|
log_level=Config().logging.level.lower(),
|
409
|
+
timeout_graceful_shutdown=0,
|
400
410
|
)
|
401
411
|
fastapi_server = uvicorn.Server(fastapi_config)
|
402
412
|
servers_to_run.append(fastapi_server.serve())
|
@@ -408,13 +418,27 @@ class OpenEdisonProxy:
|
|
408
418
|
host=self.host,
|
409
419
|
port=self.port,
|
410
420
|
log_level=Config().logging.level.lower(),
|
421
|
+
timeout_graceful_shutdown=0,
|
411
422
|
)
|
412
423
|
fastmcp_server = uvicorn.Server(fastmcp_config)
|
413
424
|
servers_to_run.append(fastmcp_server.serve())
|
414
425
|
|
415
426
|
# Run both servers concurrently
|
416
427
|
log.info("🚀 Starting both FastAPI and FastMCP servers...")
|
417
|
-
|
428
|
+
loop = asyncio.get_running_loop()
|
429
|
+
|
430
|
+
def _trigger_shutdown(signame: str) -> None:
|
431
|
+
log.info(f"Received {signame}. Forcing shutdown of all servers...")
|
432
|
+
for srv in (fastapi_server, fastmcp_server):
|
433
|
+
with suppress(Exception):
|
434
|
+
srv.force_exit = True # type: ignore[attr-defined] # noqa
|
435
|
+
srv.should_exit = True # type: ignore[attr-defined] # noqa
|
436
|
+
|
437
|
+
for sig in (signal.SIGINT, signal.SIGTERM):
|
438
|
+
with suppress(Exception):
|
439
|
+
loop.add_signal_handler(sig, _trigger_shutdown, sig.name)
|
440
|
+
|
441
|
+
await asyncio.gather(*servers_to_run, return_exceptions=False)
|
418
442
|
|
419
443
|
def _register_routes(self, app: FastAPI) -> None:
|
420
444
|
"""Register all routes for the FastAPI app"""
|
@@ -953,12 +977,8 @@ class OpenEdisonProxy:
|
|
953
977
|
|
954
978
|
log.info(f"🔗 Testing connection to {server_name} at {remote_url}")
|
955
979
|
|
956
|
-
# Import FastMCP client for testing
|
957
|
-
from fastmcp import Client as FastMCPClient
|
958
|
-
from fastmcp.client.auth import OAuth
|
959
|
-
|
960
980
|
# Create OAuth auth object
|
961
|
-
oauth =
|
981
|
+
oauth = OpenEdisonOAuth(
|
962
982
|
mcp_url=remote_url,
|
963
983
|
scopes=scopes,
|
964
984
|
client_name=client_name or "OpenEdison MCP Gateway",
|
src/setup_tui/main.py
CHANGED
@@ -1,15 +1,26 @@
|
|
1
1
|
import argparse
|
2
|
+
import asyncio
|
3
|
+
import contextlib
|
4
|
+
import sys
|
5
|
+
from collections.abc import Generator
|
2
6
|
|
3
7
|
import questionary
|
8
|
+
from loguru import logger as log
|
4
9
|
|
5
|
-
|
10
|
+
import src.oauth_manager as oauth_mod
|
11
|
+
from src.config import MCPServerConfig, get_config_dir
|
6
12
|
from src.mcp_importer.api import (
|
7
13
|
CLIENT,
|
14
|
+
authorize_server_oauth,
|
8
15
|
detect_clients,
|
9
16
|
export_edison_to,
|
17
|
+
has_oauth_tokens,
|
10
18
|
import_from,
|
19
|
+
save_imported_servers,
|
11
20
|
verify_mcp_server,
|
12
21
|
)
|
22
|
+
from src.mcp_importer.parsers import deduplicate_by_name
|
23
|
+
from src.oauth_manager import OAuthStatus, get_oauth_manager
|
13
24
|
|
14
25
|
|
15
26
|
def show_welcome_screen(*, dry_run: bool = False) -> None:
|
@@ -31,7 +42,9 @@ def show_welcome_screen(*, dry_run: bool = False) -> None:
|
|
31
42
|
questionary.confirm("Ready to begin the setup process?", default=True).ask()
|
32
43
|
|
33
44
|
|
34
|
-
def handle_mcp_source(
|
45
|
+
def handle_mcp_source( # noqa: C901
|
46
|
+
source: CLIENT, *, dry_run: bool = False, skip_oauth: bool = False
|
47
|
+
) -> list[MCPServerConfig]:
|
35
48
|
"""Handle the MCP source."""
|
36
49
|
if not questionary.confirm(
|
37
50
|
f"We have found {source.name} installed. Would you like to import its MCP servers to open-edison?",
|
@@ -41,18 +54,65 @@ def handle_mcp_source(source: CLIENT, *, dry_run: bool = False) -> list[MCPServe
|
|
41
54
|
|
42
55
|
configs = import_from(source)
|
43
56
|
|
57
|
+
# Filter out any "open-edison" configs
|
58
|
+
if "open-edison" in [config.name for config in configs]:
|
59
|
+
print(
|
60
|
+
"Found an 'open-edison' config. This is not allowed, so it will be excluded from the import."
|
61
|
+
)
|
62
|
+
|
63
|
+
configs = [config for config in configs if config.name != "open-edison"]
|
64
|
+
|
44
65
|
print(f"Loaded {len(configs)} MCP server configuration from {source.name}!")
|
45
66
|
|
46
67
|
verified_configs: list[MCPServerConfig] = []
|
47
68
|
|
48
69
|
for config in configs:
|
49
|
-
print(f"Verifying the configuration for {config.name}...
|
70
|
+
print(f"Verifying the configuration for {config.name}... ")
|
50
71
|
result = verify_mcp_server(config)
|
51
72
|
if result:
|
73
|
+
# For remote servers, only prompt if OAuth is actually required
|
74
|
+
if config.is_remote_server():
|
75
|
+
# Heuristic: if inline headers are present (e.g., API key), treat as not requiring OAuth
|
76
|
+
has_inline_headers: bool = any(
|
77
|
+
(a == "--header" or a.startswith("--header")) for a in config.args
|
78
|
+
)
|
79
|
+
if not has_inline_headers:
|
80
|
+
# Prefer cached result from verification; only check if missing
|
81
|
+
oauth_mgr = get_oauth_manager()
|
82
|
+
info = oauth_mgr.get_server_info(config.name)
|
83
|
+
if info is None:
|
84
|
+
info = asyncio.run(
|
85
|
+
oauth_mgr.check_oauth_requirement(config.name, config.get_remote_url())
|
86
|
+
)
|
87
|
+
|
88
|
+
if info.status == OAuthStatus.NEEDS_AUTH:
|
89
|
+
tokens_present: bool = has_oauth_tokens(config)
|
90
|
+
if not tokens_present:
|
91
|
+
if skip_oauth:
|
92
|
+
print(
|
93
|
+
f"Skipping OAuth for {config.name} due to --skip-oauth (OAuth required, no tokens). This server will not be imported."
|
94
|
+
)
|
95
|
+
continue
|
96
|
+
|
97
|
+
if questionary.confirm(
|
98
|
+
f"{config.name} requires OAuth and no credentials were found. Obtain credentials now?",
|
99
|
+
default=True,
|
100
|
+
).ask():
|
101
|
+
success = authorize_server_oauth(config)
|
102
|
+
if not success:
|
103
|
+
print(
|
104
|
+
f"Failed to obtain OAuth credentials for {config.name}. Skipping this server."
|
105
|
+
)
|
106
|
+
continue
|
107
|
+
else:
|
108
|
+
print(f"Skipping {config.name} per user choice.")
|
109
|
+
continue
|
110
|
+
|
111
|
+
print(f"Verification successful for {config.name}.")
|
52
112
|
verified_configs.append(config)
|
53
113
|
else:
|
54
114
|
print(
|
55
|
-
f"
|
115
|
+
f"Verification failed for the configuration of {config.name}. Please check the configuration and try again."
|
56
116
|
)
|
57
117
|
|
58
118
|
return verified_configs
|
@@ -95,8 +155,10 @@ def show_manual_setup_screen() -> None:
|
|
95
155
|
|
96
156
|
To set up open-edison manually in other clients, find your client's MCP config
|
97
157
|
JSON file and add the following configuration:
|
158
|
+
"""
|
98
159
|
|
99
|
-
"
|
160
|
+
json_snippet = """\t{
|
161
|
+
"mcpServers": {
|
100
162
|
"open-edison": {
|
101
163
|
"command": "npx",
|
102
164
|
"args": [
|
@@ -108,48 +170,123 @@ def show_manual_setup_screen() -> None:
|
|
108
170
|
"Authorization: Bearer dev-api-key-change-me"
|
109
171
|
]
|
110
172
|
}
|
111
|
-
}
|
173
|
+
}
|
174
|
+
}"""
|
112
175
|
|
176
|
+
after_text = """
|
113
177
|
Make sure to replace 'dev-api-key-change-me' with your actual API key.
|
114
178
|
"""
|
115
179
|
|
116
180
|
print(manual_setup_text)
|
181
|
+
# Use questionary's print with style for color
|
182
|
+
questionary.print(json_snippet, style="bold fg:ansigreen")
|
183
|
+
print(after_text)
|
184
|
+
|
185
|
+
|
186
|
+
class _TuiLogger:
|
187
|
+
def _fmt(self, msg: object, *args: object) -> str:
|
188
|
+
try:
|
189
|
+
if isinstance(msg, str) and args:
|
190
|
+
return msg.format(*args)
|
191
|
+
except Exception:
|
192
|
+
pass
|
193
|
+
return str(msg)
|
194
|
+
|
195
|
+
def info(self, msg: object, *args: object, **kwargs: object) -> None:
|
196
|
+
questionary.print(self._fmt(msg, *args), style="fg:ansiblue")
|
197
|
+
|
198
|
+
def debug(self, msg: object, *args: object, **kwargs: object) -> None:
|
199
|
+
questionary.print(self._fmt(msg, *args), style="fg:ansiblack")
|
200
|
+
|
201
|
+
def warning(self, msg: object, *args: object, **kwargs: object) -> None:
|
202
|
+
questionary.print(self._fmt(msg, *args), style="bold fg:ansiyellow")
|
203
|
+
|
204
|
+
def error(self, msg: object, *args: object, **kwargs: object) -> None:
|
205
|
+
questionary.print(self._fmt(msg, *args), style="bold fg:ansired")
|
206
|
+
|
207
|
+
|
208
|
+
@contextlib.contextmanager
|
209
|
+
def suppress_loguru_output() -> Generator[None, None, None]:
|
210
|
+
"""Suppress loguru output."""
|
211
|
+
with contextlib.suppress(Exception):
|
212
|
+
log.remove()
|
213
|
+
|
214
|
+
old_logger = oauth_mod.log
|
215
|
+
# Route oauth_manager's log calls to questionary for TUI output
|
216
|
+
oauth_mod.log = _TuiLogger() # type: ignore[attr-defined]
|
217
|
+
yield
|
218
|
+
oauth_mod.log = old_logger
|
219
|
+
log.add(sys.stdout, level="INFO")
|
117
220
|
|
118
221
|
|
119
|
-
|
120
|
-
|
222
|
+
@suppress_loguru_output()
|
223
|
+
def run(*, dry_run: bool = False, skip_oauth: bool = False) -> bool: # noqa: C901
|
224
|
+
"""Run the complete setup process.
|
225
|
+
Returns whether the setup was successful."""
|
121
226
|
show_welcome_screen(dry_run=dry_run)
|
122
227
|
# Additional setup steps will be added here
|
123
228
|
|
124
|
-
|
125
|
-
mcp_clients = detect_clients()
|
229
|
+
mcp_clients = sorted(detect_clients(), key=lambda x: x.value)
|
126
230
|
|
127
231
|
configs: list[MCPServerConfig] = []
|
128
232
|
|
129
|
-
for
|
130
|
-
configs.extend(handle_mcp_source(
|
233
|
+
for client in mcp_clients:
|
234
|
+
configs.extend(handle_mcp_source(client, dry_run=dry_run, skip_oauth=skip_oauth))
|
131
235
|
|
132
236
|
if len(configs) == 0:
|
133
|
-
|
134
|
-
"No MCP servers found.
|
135
|
-
)
|
136
|
-
|
237
|
+
if not questionary.confirm(
|
238
|
+
"No MCP servers found. Would you like to continue without them?", default=False
|
239
|
+
).ask():
|
240
|
+
print("Setup aborted. Please configure an MCP client and try again.")
|
241
|
+
return False
|
242
|
+
return True
|
243
|
+
|
244
|
+
# Deduplicate configs
|
245
|
+
configs = deduplicate_by_name(configs)
|
137
246
|
|
138
247
|
if not confirm_configs(configs, dry_run=dry_run):
|
139
|
-
return
|
248
|
+
return False
|
140
249
|
|
141
250
|
for client in mcp_clients:
|
142
251
|
confirm_apply_configs(client, dry_run=dry_run)
|
143
252
|
|
253
|
+
# Persist imported servers into config.json
|
254
|
+
if len(configs) > 0:
|
255
|
+
save_imported_servers(configs, dry_run=dry_run)
|
256
|
+
|
144
257
|
show_manual_setup_screen()
|
145
258
|
|
259
|
+
return True
|
260
|
+
|
261
|
+
|
262
|
+
# Triggered from cli.py
|
263
|
+
def run_import_tui(args: argparse.Namespace, force: bool = False) -> bool:
|
264
|
+
"""Run the import TUI, if necessary."""
|
265
|
+
# Find config dir, check if ".setup_tui_ran" exists
|
266
|
+
config_dir = get_config_dir()
|
267
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
268
|
+
|
269
|
+
setup_tui_ran_file = config_dir / ".setup_tui_ran"
|
270
|
+
success = True
|
271
|
+
if not setup_tui_ran_file.exists() or force:
|
272
|
+
success = run(dry_run=args.wizard_dry_run, skip_oauth=args.wizard_skip_oauth)
|
273
|
+
|
274
|
+
setup_tui_ran_file.touch()
|
275
|
+
|
276
|
+
return success
|
277
|
+
|
146
278
|
|
147
279
|
def main(argv: list[str] | None = None) -> int:
|
148
280
|
parser = argparse.ArgumentParser(description="Open Edison Setup TUI")
|
149
281
|
parser.add_argument("--dry-run", action="store_true", help="Preview actions without writing")
|
282
|
+
parser.add_argument(
|
283
|
+
"--skip-oauth",
|
284
|
+
action="store_true",
|
285
|
+
help="Skip OAuth for remote servers (they will be omitted from import)",
|
286
|
+
)
|
150
287
|
args = parser.parse_args(argv)
|
151
288
|
|
152
|
-
run(dry_run=args.dry_run)
|
289
|
+
run(dry_run=args.dry_run, skip_oauth=args.skip_oauth)
|
153
290
|
return 0
|
154
291
|
|
155
292
|
|
src/single_user_mcp.py
CHANGED
@@ -9,6 +9,7 @@ from typing import Any, TypedDict
|
|
9
9
|
|
10
10
|
from fastmcp import Client as FastMCPClient
|
11
11
|
from fastmcp import Context, FastMCP
|
12
|
+
from fastmcp.server.dependencies import get_context
|
12
13
|
from loguru import logger as log
|
13
14
|
|
14
15
|
from src.config import Config, MCPServerConfig
|
@@ -281,9 +282,6 @@ class SingleUserMCP(FastMCP[Any]):
|
|
281
282
|
async def _send_list_changed_notifications(self) -> None:
|
282
283
|
"""Send notifications to clients about changed component lists."""
|
283
284
|
try:
|
284
|
-
# Import here to avoid circular imports
|
285
|
-
from fastmcp.server.dependencies import get_context
|
286
|
-
|
287
285
|
try:
|
288
286
|
context = get_context()
|
289
287
|
# Queue notifications for all component types since we don't know
|
@@ -323,12 +321,18 @@ class SingleUserMCP(FastMCP[Any]):
|
|
323
321
|
log.info("✅ Single User MCP server initialized with composite proxy")
|
324
322
|
|
325
323
|
# Invalidate and warm lists to ensure reload
|
324
|
+
log.debug("Reloading tool list...")
|
326
325
|
_ = await self._tool_manager.list_tools()
|
326
|
+
log.debug("Reloading resource list...")
|
327
327
|
_ = await self._resource_manager.list_resources()
|
328
|
+
log.debug("Reloading prompt list...")
|
328
329
|
_ = await self._prompt_manager.list_prompts()
|
330
|
+
log.debug("Reloading complete")
|
329
331
|
|
330
332
|
# Send notifications to clients about changed component lists
|
333
|
+
log.debug("Sending list changed notifications...")
|
331
334
|
await self._send_list_changed_notifications()
|
335
|
+
log.debug("List changed notifications sent")
|
332
336
|
|
333
337
|
def _calculate_risk_level(self, trifecta: dict[str, bool]) -> str:
|
334
338
|
"""
|
src/tools/io.py
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
import os
|
2
|
+
from collections.abc import Iterator
|
3
|
+
from contextlib import contextmanager
|
4
|
+
|
5
|
+
|
6
|
+
@contextmanager
|
7
|
+
def suppress_fds(*, suppress_stdout: bool = False, suppress_stderr: bool = True) -> Iterator[None]:
|
8
|
+
"""Temporarily redirect process-level stdout/stderr to os.devnull.
|
9
|
+
|
10
|
+
Args:
|
11
|
+
suppress_stdout: If True, redirect fd 1 to devnull
|
12
|
+
suppress_stderr: If True, redirect fd 2 to devnull
|
13
|
+
|
14
|
+
Yields:
|
15
|
+
None
|
16
|
+
"""
|
17
|
+
saved: list[tuple[int, int]] = []
|
18
|
+
try:
|
19
|
+
if suppress_stdout:
|
20
|
+
saved.append((1, os.dup(1)))
|
21
|
+
devnull_out = os.open(os.devnull, os.O_WRONLY)
|
22
|
+
os.dup2(devnull_out, 1)
|
23
|
+
os.close(devnull_out)
|
24
|
+
if suppress_stderr:
|
25
|
+
saved.append((2, os.dup(2)))
|
26
|
+
devnull_err = os.open(os.devnull, os.O_WRONLY)
|
27
|
+
os.dup2(devnull_err, 2)
|
28
|
+
os.close(devnull_err)
|
29
|
+
yield
|
30
|
+
finally:
|
31
|
+
for fd, backup in saved:
|
32
|
+
try:
|
33
|
+
os.dup2(backup, fd)
|
34
|
+
finally:
|
35
|
+
os.close(backup)
|
src/vulture_whitelist.py
ADDED
@@ -1,37 +0,0 @@
|
|
1
|
-
src/__init__.py,sha256=bEYMwBiuW9jzF07iWhas4Vb30EcpnqfpNfz_Q6yO1jU,209
|
2
|
-
src/__main__.py,sha256=kQsaVyzRa_ESC57JpKDSQJAHExuXme0rM5beJsYxFeA,161
|
3
|
-
src/cli.py,sha256=fqX-HuRDePRasexpnURQ_pVYeycJuWxllMcwfqDxMQw,8490
|
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=W9QSo0vfGDQ_i-QWCngkv7YLSL3Rk5jfPmqjU1J2rnU,9911
|
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=Mk59pVr7OMGfYGWeSYk8-URfhIcrs3SPLYS7fmJbMII,275
|
17
|
-
src/mcp_importer/__main__.py,sha256=0jVfxKzyr6koVu1ghhWseah5ilKIoGovE6zkEZ-u-Og,515
|
18
|
-
src/mcp_importer/api.py,sha256=47tur0xgl1NBI1Vnh3cpScEmDS64bKMYcWjZDuqx7HQ,6644
|
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=4mJe10q_lZCYLm75QBt1rYy2j8mGEsRZoAqA0agjfSM,1834
|
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
|
-
src/setup_tui/__init__.py,sha256=mDFrQoiOtQOHc0sFfGKrNXVLEDeB1S0O5aISBVzfxYo,184
|
32
|
-
src/setup_tui/main.py,sha256=892X2KVKOYmKzUu1Ok8SnApNYxpcFHFHmFLvpPZP4qY,5501
|
33
|
-
open_edison-0.1.44.dist-info/METADATA,sha256=hY2fd8IeT-YeBAUjg_FBtpGf50VGOpgRp4xdBhx7ED4,12375
|
34
|
-
open_edison-0.1.44.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
35
|
-
open_edison-0.1.44.dist-info/entry_points.txt,sha256=YiGNm9x2I00hgT10HDyB4gxC1LcaV_mu8bXFjolu0Yw,171
|
36
|
-
open_edison-0.1.44.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
37
|
-
open_edison-0.1.44.dist-info/RECORD,,
|
File without changes
|