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.
@@ -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
 
@@ -1,3 +1 @@
1
- from __future__ import annotations
2
-
3
1
  # legacy helpers were removed to satisfy deadcode scanning
@@ -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 parse_mcp_like_json(data: dict[str, Any], default_enabled: bool = True) -> list[Any]:
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
- return top_level
143
-
144
- # Then, try nested structures heuristically
145
- nested = _collect_nested(data, default_enabled)
146
- if not nested:
147
- log.debug("No MCP-like entries detected in provided data")
148
- return nested
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)
@@ -1,5 +1,3 @@
1
- from __future__ import annotations
2
-
3
1
  import argparse
4
2
  from collections.abc import Iterable
5
3
 
src/mcp_importer/types.py CHANGED
@@ -1,5 +1,3 @@
1
1
  """Type helpers for MCP importer."""
2
2
 
3
- from __future__ import annotations
4
-
5
3
  # This module intentionally minimal to avoid unused code flags.
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 = 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
- raise PermissionsError(f"Permissions file not found at {file_path}")
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
- Simple FastAPI + FastMCP server for single-user MCP proxy.
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
- _ = await asyncio.gather(*servers_to_run)
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 = 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
- from src.config import MCPServerConfig
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(source: CLIENT, *, dry_run: bool = False) -> list[MCPServerConfig]:
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}... (TODO)")
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"The configuration for {config.name} is not valid. Please check the configuration and try again."
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
- "mcpServers": {
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
- def run(*, dry_run: bool = False) -> None:
120
- """Run the complete setup process."""
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
- mcp_sources = detect_clients()
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 source in mcp_sources:
130
- configs.extend(handle_mcp_source(source, dry_run=dry_run))
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
- print(
134
- "No MCP servers found. Please set up an MCP client with some servers and run this setup again."
135
- )
136
- return
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)
@@ -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,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,,