open-edison 0.1.44__py3-none-any.whl → 0.1.72rc1__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.72rc1.dist-info}/METADATA +2 -21
- open_edison-0.1.72rc1.dist-info/RECORD +41 -0
- src/cli.py +30 -113
- src/config.py +30 -9
- src/events.py +5 -2
- src/frontend_dist/assets/index-D05VN_1l.css +1 -0
- src/frontend_dist/assets/index-D6ziuTsl.js +51 -0
- src/frontend_dist/index.html +2 -2
- src/frontend_dist/sw.js +22 -2
- 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/exporters.py +1 -1
- 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/mcp_stdio_capture.py +144 -0
- src/middleware/data_access_tracker.py +49 -4
- src/middleware/session_tracking.py +123 -34
- src/oauth_manager.py +5 -3
- src/oauth_override.py +10 -0
- src/permissions.py +110 -10
- src/server.py +57 -16
- src/setup_tui/main.py +160 -21
- src/single_user_mcp.py +246 -105
- src/tools/io.py +35 -0
- src/vulture_whitelist.py +3 -0
- open_edison-0.1.44.dist-info/RECORD +0 -37
- src/frontend_dist/assets/index-BUUcUfTt.js +0 -51
- src/frontend_dist/assets/index-o6_8mdM8.css +0 -1
- {open_edison-0.1.44.dist-info → open_edison-0.1.72rc1.dist-info}/WHEEL +0 -0
- {open_edison-0.1.44.dist-info → open_edison-0.1.72rc1.dist-info}/entry_points.txt +0 -0
- {open_edison-0.1.44.dist-info → open_edison-0.1.72rc1.dist-info}/licenses/LICENSE +0 -0
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
|
|
@@ -52,6 +53,9 @@ class ResourcePermission:
|
|
52
53
|
write_operation: bool = False
|
53
54
|
read_private_data: bool = False
|
54
55
|
read_untrusted_public_data: bool = False
|
56
|
+
acl: str = "PUBLIC"
|
57
|
+
# Optional metadata fields (ignored by enforcement but accepted from JSON)
|
58
|
+
description: str | None = None
|
55
59
|
|
56
60
|
|
57
61
|
@dataclass
|
@@ -158,6 +162,12 @@ class Permissions:
|
|
158
162
|
)
|
159
163
|
|
160
164
|
@classmethod
|
165
|
+
def clear_permissions_file_cache(cls) -> None:
|
166
|
+
"""Clear the cache for the JSON permissions files"""
|
167
|
+
cls._load_permission_file.cache_clear()
|
168
|
+
|
169
|
+
@classmethod
|
170
|
+
@cache
|
161
171
|
def _load_permission_file(
|
162
172
|
cls,
|
163
173
|
file_path: Path,
|
@@ -171,7 +181,23 @@ class Permissions:
|
|
171
181
|
metadata: PermissionsMetadata | None = None
|
172
182
|
|
173
183
|
if not file_path.exists():
|
174
|
-
|
184
|
+
# Bootstrap missing permissions files on first run.
|
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
|
175
201
|
|
176
202
|
with open(file_path) as f:
|
177
203
|
data: dict[str, Any] = json.load(f)
|
@@ -203,43 +229,117 @@ class Permissions:
|
|
203
229
|
def get_tool_permission(self, tool_name: str) -> ToolPermission:
|
204
230
|
"""Get permission for a specific tool"""
|
205
231
|
if tool_name not in self.tool_permissions:
|
206
|
-
|
232
|
+
if tool_name.startswith("builtin_"):
|
233
|
+
log.info(
|
234
|
+
f"Tool '{tool_name}' not found; returning builtin safe default (enabled, 0 risk)"
|
235
|
+
)
|
236
|
+
return ToolPermission(
|
237
|
+
enabled=True,
|
238
|
+
write_operation=False,
|
239
|
+
read_private_data=False,
|
240
|
+
read_untrusted_public_data=False,
|
241
|
+
acl="PUBLIC",
|
242
|
+
)
|
243
|
+
log.warning(
|
244
|
+
f"Tool '{tool_name}' not found in permissions; returning enabled full-trifecta default"
|
245
|
+
)
|
246
|
+
return ToolPermission(
|
247
|
+
enabled=True,
|
248
|
+
write_operation=True,
|
249
|
+
read_private_data=True,
|
250
|
+
read_untrusted_public_data=True,
|
251
|
+
acl="SECRET",
|
252
|
+
)
|
207
253
|
return self.tool_permissions[tool_name]
|
208
254
|
|
209
255
|
def get_resource_permission(self, resource_name: str) -> ResourcePermission:
|
210
256
|
"""Get permission for a specific resource"""
|
211
257
|
if resource_name not in self.resource_permissions:
|
212
|
-
|
258
|
+
if resource_name.startswith("builtin_"):
|
259
|
+
log.info(
|
260
|
+
f"Resource '{resource_name}' not found; returning builtin safe default (enabled, 0 risk)"
|
261
|
+
)
|
262
|
+
return ResourcePermission(
|
263
|
+
enabled=True,
|
264
|
+
write_operation=False,
|
265
|
+
read_private_data=False,
|
266
|
+
read_untrusted_public_data=False,
|
267
|
+
)
|
268
|
+
log.warning(
|
269
|
+
f"Resource '{resource_name}' not found in permissions; returning enabled full-trifecta default"
|
270
|
+
)
|
271
|
+
return ResourcePermission(
|
272
|
+
enabled=True,
|
273
|
+
write_operation=True,
|
274
|
+
read_private_data=True,
|
275
|
+
read_untrusted_public_data=True,
|
276
|
+
)
|
213
277
|
return self.resource_permissions[resource_name]
|
214
278
|
|
215
279
|
def get_prompt_permission(self, prompt_name: str) -> PromptPermission:
|
216
280
|
"""Get permission for a specific prompt"""
|
217
281
|
if prompt_name not in self.prompt_permissions:
|
218
|
-
|
282
|
+
if prompt_name.startswith("builtin_"):
|
283
|
+
log.info(
|
284
|
+
f"Prompt '{prompt_name}' not found; returning builtin safe default (enabled, 0 risk)"
|
285
|
+
)
|
286
|
+
return PromptPermission(
|
287
|
+
enabled=True,
|
288
|
+
write_operation=False,
|
289
|
+
read_private_data=False,
|
290
|
+
read_untrusted_public_data=False,
|
291
|
+
acl="PUBLIC",
|
292
|
+
)
|
293
|
+
log.warning(
|
294
|
+
f"Prompt '{prompt_name}' not found in permissions; returning enabled full-trifecta default"
|
295
|
+
)
|
296
|
+
return PromptPermission(
|
297
|
+
enabled=True,
|
298
|
+
write_operation=True,
|
299
|
+
read_private_data=True,
|
300
|
+
read_untrusted_public_data=True,
|
301
|
+
acl="SECRET",
|
302
|
+
)
|
219
303
|
return self.prompt_permissions[prompt_name]
|
220
304
|
|
221
305
|
def is_tool_enabled(self, tool_name: str) -> bool:
|
222
306
|
"""Check if a tool is enabled
|
223
307
|
Also checks if the server is enabled"""
|
224
308
|
permission = self.get_tool_permission(tool_name)
|
225
|
-
|
226
|
-
|
309
|
+
try:
|
310
|
+
server_name = self.server_name_from_tool_name(tool_name)
|
311
|
+
server_enabled = self.is_server_enabled(server_name)
|
312
|
+
except PermissionsError:
|
313
|
+
log.warning(f"Server resolution failed for tool '{tool_name}'; treating as disabled")
|
314
|
+
server_enabled = False
|
227
315
|
return permission.enabled and server_enabled
|
228
316
|
|
229
317
|
def is_resource_enabled(self, resource_name: str) -> bool:
|
230
318
|
"""Check if a resource is enabled
|
231
319
|
Also checks if the server is enabled"""
|
232
320
|
permission = self.get_resource_permission(resource_name)
|
233
|
-
|
234
|
-
|
321
|
+
try:
|
322
|
+
server_name = self.server_name_from_tool_name(resource_name)
|
323
|
+
server_enabled = self.is_server_enabled(server_name)
|
324
|
+
except PermissionsError:
|
325
|
+
log.warning(
|
326
|
+
f"Server resolution failed for resource '{resource_name}'; treating as disabled"
|
327
|
+
)
|
328
|
+
server_enabled = False
|
235
329
|
return permission.enabled and server_enabled
|
236
330
|
|
237
331
|
def is_prompt_enabled(self, prompt_name: str) -> bool:
|
238
332
|
"""Check if a prompt is enabled
|
239
333
|
Also checks if the server is enabled"""
|
240
334
|
permission = self.get_prompt_permission(prompt_name)
|
241
|
-
|
242
|
-
|
335
|
+
try:
|
336
|
+
server_name = self.server_name_from_tool_name(prompt_name)
|
337
|
+
server_enabled = self.is_server_enabled(server_name)
|
338
|
+
except PermissionsError:
|
339
|
+
log.warning(
|
340
|
+
f"Server resolution failed for prompt '{prompt_name}'; treating as disabled"
|
341
|
+
)
|
342
|
+
server_enabled = False
|
243
343
|
return permission.enabled and server_enabled
|
244
344
|
|
245
345
|
@staticmethod
|
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,18 +25,24 @@ 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
|
31
32
|
|
32
33
|
from src import events
|
33
|
-
from src.config import Config, MCPServerConfig, clear_json_file_cache
|
34
|
+
from src.config import Config, MCPServerConfig, clear_json_file_cache, get_config_json_path
|
34
35
|
from src.config import get_config_dir as _get_cfg_dir # type: ignore[attr-defined]
|
36
|
+
from src.mcp_stdio_capture import (
|
37
|
+
install_stdio_client_stderr_capture as _install_stdio_capture,
|
38
|
+
)
|
35
39
|
from src.middleware.session_tracking import (
|
36
40
|
MCPSessionModel,
|
37
41
|
create_db_session,
|
38
42
|
)
|
39
43
|
from src.oauth_manager import OAuthStatus, get_oauth_manager
|
44
|
+
from src.oauth_override import OpenEdisonOAuth
|
45
|
+
from src.permissions import Permissions
|
40
46
|
from src.single_user_mcp import SingleUserMCP
|
41
47
|
from src.telemetry import initialize_telemetry, set_servers_installed
|
42
48
|
|
@@ -45,6 +51,9 @@ _security = HTTPBearer()
|
|
45
51
|
_auth_dependency = Depends(_security)
|
46
52
|
|
47
53
|
|
54
|
+
_install_stdio_capture()
|
55
|
+
|
56
|
+
|
48
57
|
class OpenEdisonProxy:
|
49
58
|
"""
|
50
59
|
Open Edison Single-User MCP Proxy Server
|
@@ -276,6 +285,12 @@ class OpenEdisonProxy:
|
|
276
285
|
# Clear cache for the config file, if it was config.json
|
277
286
|
if name == "config.json":
|
278
287
|
clear_json_file_cache()
|
288
|
+
elif name in (
|
289
|
+
"tool_permissions.json",
|
290
|
+
"resource_permissions.json",
|
291
|
+
"prompt_permissions.json",
|
292
|
+
):
|
293
|
+
Permissions.clear_permissions_file_cache()
|
279
294
|
|
280
295
|
return {"status": "ok"}
|
281
296
|
except Exception as e: # noqa: BLE001
|
@@ -363,6 +378,9 @@ class OpenEdisonProxy:
|
|
363
378
|
log.info(f"FastAPI management API on {self.host}:{self.port + 1}")
|
364
379
|
log.info(f"FastMCP protocol server on {self.host}:{self.port}")
|
365
380
|
|
381
|
+
# Print location of config
|
382
|
+
log.info(f"Config file location: {get_config_json_path()}")
|
383
|
+
|
366
384
|
initialize_telemetry()
|
367
385
|
|
368
386
|
# Ensure the sessions database exists and has the required schema
|
@@ -397,6 +415,7 @@ class OpenEdisonProxy:
|
|
397
415
|
host=self.host,
|
398
416
|
port=self.port + 1,
|
399
417
|
log_level=Config().logging.level.lower(),
|
418
|
+
timeout_graceful_shutdown=0,
|
400
419
|
)
|
401
420
|
fastapi_server = uvicorn.Server(fastapi_config)
|
402
421
|
servers_to_run.append(fastapi_server.serve())
|
@@ -408,13 +427,27 @@ class OpenEdisonProxy:
|
|
408
427
|
host=self.host,
|
409
428
|
port=self.port,
|
410
429
|
log_level=Config().logging.level.lower(),
|
430
|
+
timeout_graceful_shutdown=0,
|
411
431
|
)
|
412
432
|
fastmcp_server = uvicorn.Server(fastmcp_config)
|
413
433
|
servers_to_run.append(fastmcp_server.serve())
|
414
434
|
|
415
435
|
# Run both servers concurrently
|
416
436
|
log.info("🚀 Starting both FastAPI and FastMCP servers...")
|
417
|
-
|
437
|
+
loop = asyncio.get_running_loop()
|
438
|
+
|
439
|
+
def _trigger_shutdown(signame: str) -> None:
|
440
|
+
log.info(f"Received {signame}. Forcing shutdown of all servers...")
|
441
|
+
for srv in (fastapi_server, fastmcp_server):
|
442
|
+
with suppress(Exception):
|
443
|
+
srv.force_exit = True # type: ignore[attr-defined] # noqa
|
444
|
+
srv.should_exit = True # type: ignore[attr-defined] # noqa
|
445
|
+
|
446
|
+
for sig in (signal.SIGINT, signal.SIGTERM):
|
447
|
+
with suppress(Exception):
|
448
|
+
loop.add_signal_handler(sig, _trigger_shutdown, sig.name)
|
449
|
+
|
450
|
+
await asyncio.gather(*servers_to_run, return_exceptions=False)
|
418
451
|
|
419
452
|
def _register_routes(self, app: FastAPI) -> None:
|
420
453
|
"""Register all routes for the FastAPI app"""
|
@@ -519,11 +552,8 @@ class OpenEdisonProxy:
|
|
519
552
|
warms the lists to ensure subsequent list calls reflect current state.
|
520
553
|
"""
|
521
554
|
try:
|
522
|
-
|
523
|
-
|
524
|
-
await mcp._tool_manager.list_tools() # type: ignore[attr-defined]
|
525
|
-
await mcp._resource_manager.list_resources() # type: ignore[attr-defined]
|
526
|
-
await mcp._prompt_manager.list_prompts() # type: ignore[attr-defined]
|
555
|
+
clear_json_file_cache()
|
556
|
+
Permissions.clear_permissions_file_cache()
|
527
557
|
return {"status": "ok"}
|
528
558
|
except Exception as e: # noqa: BLE001
|
529
559
|
log.error(f"Failed to process permissions-changed: {e}")
|
@@ -638,14 +668,29 @@ class OpenEdisonProxy:
|
|
638
668
|
)
|
639
669
|
|
640
670
|
sessions: list[dict[str, Any]] = []
|
671
|
+
has_warned_about_missing_created_at = False
|
641
672
|
for row_model in results:
|
642
673
|
row = cast(Any, row_model)
|
643
674
|
tool_calls_val = row.tool_calls
|
644
675
|
data_access_summary_val = row.data_access_summary
|
676
|
+
created_at_val = None
|
677
|
+
if isinstance(data_access_summary_val, dict):
|
678
|
+
created_at_val = data_access_summary_val.get("created_at") # type: ignore[assignment]
|
679
|
+
if (
|
680
|
+
created_at_val is None
|
681
|
+
and isinstance(tool_calls_val, list)
|
682
|
+
and tool_calls_val
|
683
|
+
and not has_warned_about_missing_created_at
|
684
|
+
):
|
685
|
+
has_warned_about_missing_created_at = True
|
686
|
+
log.warning(
|
687
|
+
"created_at is missing, will have sessions with unknown timestamps"
|
688
|
+
)
|
645
689
|
sessions.append(
|
646
690
|
{
|
647
691
|
"session_id": row.session_id,
|
648
692
|
"correlation_id": row.correlation_id,
|
693
|
+
"created_at": created_at_val,
|
649
694
|
"tool_calls": tool_calls_val
|
650
695
|
if isinstance(tool_calls_val, list)
|
651
696
|
else [],
|
@@ -953,12 +998,8 @@ class OpenEdisonProxy:
|
|
953
998
|
|
954
999
|
log.info(f"🔗 Testing connection to {server_name} at {remote_url}")
|
955
1000
|
|
956
|
-
# Import FastMCP client for testing
|
957
|
-
from fastmcp import Client as FastMCPClient
|
958
|
-
from fastmcp.client.auth import OAuth
|
959
|
-
|
960
1001
|
# Create OAuth auth object
|
961
|
-
oauth =
|
1002
|
+
oauth = OpenEdisonOAuth(
|
962
1003
|
mcp_url=remote_url,
|
963
1004
|
scopes=scopes,
|
964
1005
|
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
|
@@ -72,16 +132,18 @@ def confirm_configs(configs: list[MCPServerConfig], *, dry_run: bool = False) ->
|
|
72
132
|
|
73
133
|
def confirm_apply_configs(client: CLIENT, *, dry_run: bool = False) -> None:
|
74
134
|
if not questionary.confirm(
|
75
|
-
f"
|
135
|
+
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.)",
|
76
136
|
default=True,
|
77
137
|
).ask():
|
78
138
|
return
|
79
139
|
|
80
|
-
export_edison_to(client, dry_run=dry_run)
|
140
|
+
result = export_edison_to(client, dry_run=dry_run)
|
81
141
|
if dry_run:
|
82
142
|
print(f"[dry-run] Export prepared for {client.name}; no changes written.")
|
83
143
|
else:
|
84
|
-
print(
|
144
|
+
print(
|
145
|
+
f"Successfully set up Open Edison for {client.name}! Your previous MCP configuration has been backed up at {result.backup_path}"
|
146
|
+
)
|
85
147
|
|
86
148
|
|
87
149
|
def show_manual_setup_screen() -> None:
|
@@ -95,8 +157,10 @@ def show_manual_setup_screen() -> None:
|
|
95
157
|
|
96
158
|
To set up open-edison manually in other clients, find your client's MCP config
|
97
159
|
JSON file and add the following configuration:
|
160
|
+
"""
|
98
161
|
|
99
|
-
"
|
162
|
+
json_snippet = """\t{
|
163
|
+
"mcpServers": {
|
100
164
|
"open-edison": {
|
101
165
|
"command": "npx",
|
102
166
|
"args": [
|
@@ -108,48 +172,123 @@ def show_manual_setup_screen() -> None:
|
|
108
172
|
"Authorization: Bearer dev-api-key-change-me"
|
109
173
|
]
|
110
174
|
}
|
111
|
-
}
|
175
|
+
}
|
176
|
+
}"""
|
112
177
|
|
178
|
+
after_text = """
|
113
179
|
Make sure to replace 'dev-api-key-change-me' with your actual API key.
|
114
180
|
"""
|
115
181
|
|
116
182
|
print(manual_setup_text)
|
183
|
+
# Use questionary's print with style for color
|
184
|
+
questionary.print(json_snippet, style="bold fg:ansigreen")
|
185
|
+
print(after_text)
|
186
|
+
|
187
|
+
|
188
|
+
class _TuiLogger:
|
189
|
+
def _fmt(self, msg: object, *args: object) -> str:
|
190
|
+
try:
|
191
|
+
if isinstance(msg, str) and args:
|
192
|
+
return msg.format(*args)
|
193
|
+
except Exception:
|
194
|
+
pass
|
195
|
+
return str(msg)
|
196
|
+
|
197
|
+
def info(self, msg: object, *args: object, **kwargs: object) -> None:
|
198
|
+
questionary.print(self._fmt(msg, *args), style="fg:ansiblue")
|
199
|
+
|
200
|
+
def debug(self, msg: object, *args: object, **kwargs: object) -> None:
|
201
|
+
questionary.print(self._fmt(msg, *args), style="fg:ansiblack")
|
202
|
+
|
203
|
+
def warning(self, msg: object, *args: object, **kwargs: object) -> None:
|
204
|
+
questionary.print(self._fmt(msg, *args), style="bold fg:ansiyellow")
|
205
|
+
|
206
|
+
def error(self, msg: object, *args: object, **kwargs: object) -> None:
|
207
|
+
questionary.print(self._fmt(msg, *args), style="bold fg:ansired")
|
208
|
+
|
209
|
+
|
210
|
+
@contextlib.contextmanager
|
211
|
+
def suppress_loguru_output() -> Generator[None, None, None]:
|
212
|
+
"""Suppress loguru output."""
|
213
|
+
with contextlib.suppress(Exception):
|
214
|
+
log.remove()
|
117
215
|
|
216
|
+
old_logger = oauth_mod.log
|
217
|
+
# Route oauth_manager's log calls to questionary for TUI output
|
218
|
+
oauth_mod.log = _TuiLogger() # type: ignore[attr-defined]
|
219
|
+
yield
|
220
|
+
oauth_mod.log = old_logger
|
221
|
+
log.add(sys.stdout, level="INFO")
|
118
222
|
|
119
|
-
|
120
|
-
|
223
|
+
|
224
|
+
@suppress_loguru_output()
|
225
|
+
def run(*, dry_run: bool = False, skip_oauth: bool = False) -> bool: # noqa: C901
|
226
|
+
"""Run the complete setup process.
|
227
|
+
Returns whether the setup was successful."""
|
121
228
|
show_welcome_screen(dry_run=dry_run)
|
122
229
|
# Additional setup steps will be added here
|
123
230
|
|
124
|
-
|
125
|
-
mcp_clients = detect_clients()
|
231
|
+
mcp_clients = sorted(detect_clients(), key=lambda x: x.value)
|
126
232
|
|
127
233
|
configs: list[MCPServerConfig] = []
|
128
234
|
|
129
|
-
for
|
130
|
-
configs.extend(handle_mcp_source(
|
235
|
+
for client in mcp_clients:
|
236
|
+
configs.extend(handle_mcp_source(client, dry_run=dry_run, skip_oauth=skip_oauth))
|
131
237
|
|
132
238
|
if len(configs) == 0:
|
133
|
-
|
134
|
-
"No MCP servers found.
|
135
|
-
)
|
136
|
-
|
239
|
+
if not questionary.confirm(
|
240
|
+
"No MCP servers found. Would you like to continue without them?", default=True
|
241
|
+
).ask():
|
242
|
+
print("Setup aborted. Please configure an MCP client and try again.")
|
243
|
+
return False
|
244
|
+
return True
|
245
|
+
|
246
|
+
# Deduplicate configs
|
247
|
+
configs = deduplicate_by_name(configs)
|
137
248
|
|
138
249
|
if not confirm_configs(configs, dry_run=dry_run):
|
139
|
-
return
|
250
|
+
return False
|
140
251
|
|
141
252
|
for client in mcp_clients:
|
142
253
|
confirm_apply_configs(client, dry_run=dry_run)
|
143
254
|
|
255
|
+
# Persist imported servers into config.json
|
256
|
+
if len(configs) > 0:
|
257
|
+
save_imported_servers(configs, dry_run=dry_run)
|
258
|
+
|
144
259
|
show_manual_setup_screen()
|
145
260
|
|
261
|
+
return True
|
262
|
+
|
263
|
+
|
264
|
+
# Triggered from cli.py
|
265
|
+
def run_import_tui(args: argparse.Namespace, force: bool = False) -> bool:
|
266
|
+
"""Run the import TUI, if necessary."""
|
267
|
+
# Find config dir, check if ".setup_tui_ran" exists
|
268
|
+
config_dir = get_config_dir()
|
269
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
270
|
+
|
271
|
+
setup_tui_ran_file = config_dir / ".setup_tui_ran"
|
272
|
+
success = True
|
273
|
+
if not setup_tui_ran_file.exists() or force:
|
274
|
+
success = run(dry_run=args.wizard_dry_run, skip_oauth=args.wizard_skip_oauth)
|
275
|
+
|
276
|
+
setup_tui_ran_file.touch()
|
277
|
+
|
278
|
+
return success
|
279
|
+
|
146
280
|
|
147
281
|
def main(argv: list[str] | None = None) -> int:
|
148
282
|
parser = argparse.ArgumentParser(description="Open Edison Setup TUI")
|
149
283
|
parser.add_argument("--dry-run", action="store_true", help="Preview actions without writing")
|
284
|
+
parser.add_argument(
|
285
|
+
"--skip-oauth",
|
286
|
+
action="store_true",
|
287
|
+
help="Skip OAuth for remote servers (they will be omitted from import)",
|
288
|
+
)
|
150
289
|
args = parser.parse_args(argv)
|
151
290
|
|
152
|
-
run(dry_run=args.dry_run)
|
291
|
+
run(dry_run=args.dry_run, skip_oauth=args.skip_oauth)
|
153
292
|
return 0
|
154
293
|
|
155
294
|
|