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.
src/setup_tui/main.py ADDED
@@ -0,0 +1,294 @@
1
+ import argparse
2
+ import asyncio
3
+ import contextlib
4
+ import sys
5
+ from collections.abc import Generator
6
+
7
+ import questionary
8
+ from loguru import logger as log
9
+
10
+ import src.oauth_manager as oauth_mod
11
+ from src.config import MCPServerConfig, get_config_dir
12
+ from src.mcp_importer.api import (
13
+ CLIENT,
14
+ authorize_server_oauth,
15
+ detect_clients,
16
+ export_edison_to,
17
+ has_oauth_tokens,
18
+ import_from,
19
+ save_imported_servers,
20
+ verify_mcp_server,
21
+ )
22
+ from src.mcp_importer.parsers import deduplicate_by_name
23
+ from src.oauth_manager import OAuthStatus, get_oauth_manager
24
+
25
+
26
+ def show_welcome_screen(*, dry_run: bool = False) -> None:
27
+ """Display the welcome screen for open-edison setup."""
28
+ welcome_text = """
29
+ ╔══════════════════════════════════════════════════════════════╗
30
+ ║ ║
31
+ ║ Welcome to open-edison ║
32
+ ║ ║
33
+ ║ This setup wizard will help you configure open-edison ║
34
+ ║ for your development environment. ║
35
+ ║ ║
36
+ ╚══════════════════════════════════════════════════════════════╝
37
+ """
38
+
39
+ print(welcome_text)
40
+
41
+ # Prompt to continue
42
+ questionary.confirm("Ready to begin the setup process?", default=True).ask()
43
+
44
+
45
+ def handle_mcp_source( # noqa: C901
46
+ source: CLIENT, *, dry_run: bool = False, skip_oauth: bool = False
47
+ ) -> list[MCPServerConfig]:
48
+ """Handle the MCP source."""
49
+ if not questionary.confirm(
50
+ f"We have found {source.name} installed. Would you like to import its MCP servers to open-edison?",
51
+ default=True,
52
+ ).ask():
53
+ return []
54
+
55
+ configs = import_from(source)
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
+
65
+ print(f"Loaded {len(configs)} MCP server configuration from {source.name}!")
66
+
67
+ verified_configs: list[MCPServerConfig] = []
68
+
69
+ for config in configs:
70
+ print(f"Verifying the configuration for {config.name}... ")
71
+ result = verify_mcp_server(config)
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}.")
112
+ verified_configs.append(config)
113
+ else:
114
+ print(
115
+ f"Verification failed for the configuration of {config.name}. Please check the configuration and try again."
116
+ )
117
+
118
+ return verified_configs
119
+
120
+
121
+ def confirm_configs(configs: list[MCPServerConfig], *, dry_run: bool = False) -> bool:
122
+ """Confirm the MCP configs."""
123
+ print("These are the servers you have selected:")
124
+
125
+ for config in configs:
126
+ print(f"○ {config.name}")
127
+
128
+ return questionary.confirm(
129
+ "Are you sure you want to use these servers with open-edison?", default=True
130
+ ).ask()
131
+
132
+
133
+ def confirm_apply_configs(client: CLIENT, *, dry_run: bool = False) -> None:
134
+ if not questionary.confirm(
135
+ f"We have detected that you have {client.name} installed. Would you like to connect it to open-edison?",
136
+ default=True,
137
+ ).ask():
138
+ return
139
+
140
+ export_edison_to(client, dry_run=dry_run)
141
+ if dry_run:
142
+ print(f"[dry-run] Export prepared for {client.name}; no changes written.")
143
+ else:
144
+ print(f"Successfully set up Open Edison for {client.name}!")
145
+
146
+
147
+ def show_manual_setup_screen() -> None:
148
+ """Display manual setup instructions for open-edison."""
149
+ manual_setup_text = """
150
+ ╔══════════════════════════════════════════════════════════════╗
151
+ ║ ║
152
+ ║ Manual Setup Instructions ║
153
+ ║ ║
154
+ ╚══════════════════════════════════════════════════════════════╝
155
+
156
+ To set up open-edison manually in other clients, find your client's MCP config
157
+ JSON file and add the following configuration:
158
+ """
159
+
160
+ json_snippet = """\t{
161
+ "mcpServers": {
162
+ "open-edison": {
163
+ "command": "npx",
164
+ "args": [
165
+ "-y",
166
+ "mcp-remote",
167
+ "http://localhost:3000/mcp/",
168
+ "--http-only",
169
+ "--header",
170
+ "Authorization: Bearer dev-api-key-change-me"
171
+ ]
172
+ }
173
+ }
174
+ }"""
175
+
176
+ after_text = """
177
+ Make sure to replace 'dev-api-key-change-me' with your actual API key.
178
+ """
179
+
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")
220
+
221
+
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."""
226
+ show_welcome_screen(dry_run=dry_run)
227
+ # Additional setup steps will be added here
228
+
229
+ mcp_clients = sorted(detect_clients(), key=lambda x: x.value)
230
+
231
+ configs: list[MCPServerConfig] = []
232
+
233
+ for client in mcp_clients:
234
+ configs.extend(handle_mcp_source(client, dry_run=dry_run, skip_oauth=skip_oauth))
235
+
236
+ if len(configs) == 0:
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)
246
+
247
+ if not confirm_configs(configs, dry_run=dry_run):
248
+ return False
249
+
250
+ for client in mcp_clients:
251
+ confirm_apply_configs(client, dry_run=dry_run)
252
+
253
+ # Persist imported servers into config.json
254
+ if len(configs) > 0:
255
+ save_imported_servers(configs, dry_run=dry_run)
256
+
257
+ show_manual_setup_screen()
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
+
278
+
279
+ def main(argv: list[str] | None = None) -> int:
280
+ parser = argparse.ArgumentParser(description="Open Edison Setup TUI")
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
+ )
287
+ args = parser.parse_args(argv)
288
+
289
+ run(dry_run=args.dry_run, skip_oauth=args.skip_oauth)
290
+ return 0
291
+
292
+
293
+ if __name__ == "__main__":
294
+ raise SystemExit(main())
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)
@@ -0,0 +1,3 @@
1
+ from src.oauth_override import OpenEdisonOAuth
2
+
3
+ OpenEdisonOAuth.redirect_handler # noqa: B018 unused method (src/oauth_override.py:7)
@@ -1,35 +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
- open_edison-0.1.43.dist-info/METADATA,sha256=OO5PDNk7pByRgIShryHaCtp2_7ua9XOwC8eBVdU-b5o,13188
32
- open_edison-0.1.43.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
33
- open_edison-0.1.43.dist-info/entry_points.txt,sha256=YiGNm9x2I00hgT10HDyB4gxC1LcaV_mu8bXFjolu0Yw,171
34
- open_edison-0.1.43.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
35
- open_edison-0.1.43.dist-info/RECORD,,