open-edison 0.1.10__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.10.dist-info/METADATA +332 -0
- open_edison-0.1.10.dist-info/RECORD +17 -0
- open_edison-0.1.10.dist-info/WHEEL +4 -0
- open_edison-0.1.10.dist-info/entry_points.txt +3 -0
- open_edison-0.1.10.dist-info/licenses/LICENSE +674 -0
- src/__init__.py +11 -0
- src/__main__.py +10 -0
- src/cli.py +274 -0
- src/config.py +224 -0
- src/frontend_dist/assets/index-CKkid2y-.js +51 -0
- src/frontend_dist/assets/index-CRxojymD.css +1 -0
- src/frontend_dist/index.html +21 -0
- src/mcp_manager.py +137 -0
- src/middleware/data_access_tracker.py +510 -0
- src/middleware/session_tracking.py +477 -0
- src/server.py +560 -0
- src/single_user_mcp.py +403 -0
src/__init__.py
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
"""
|
2
|
+
Open Edison Source Package
|
3
|
+
|
4
|
+
Main source code package for the Open Edison single-user MCP proxy server.
|
5
|
+
|
6
|
+
This package exposes a CLI via `open-edison` / `open_edison` entrypoints.
|
7
|
+
"""
|
8
|
+
|
9
|
+
from .server import OpenEdisonProxy
|
10
|
+
|
11
|
+
__all__ = ["OpenEdisonProxy"]
|
src/__main__.py
ADDED
src/cli.py
ADDED
@@ -0,0 +1,274 @@
|
|
1
|
+
"""
|
2
|
+
CLI entrypoint for Open Edison.
|
3
|
+
|
4
|
+
Provides `open-edison` executable when installed via pip/uvx/pipx.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from __future__ import annotations
|
8
|
+
|
9
|
+
import argparse
|
10
|
+
import asyncio
|
11
|
+
import os
|
12
|
+
import subprocess as _subprocess
|
13
|
+
from contextlib import suppress
|
14
|
+
from pathlib import Path
|
15
|
+
from typing import Any, NoReturn, cast
|
16
|
+
import sys
|
17
|
+
|
18
|
+
from loguru import logger as _log # type: ignore[reportMissingImports]
|
19
|
+
|
20
|
+
from .config import Config, get_config_dir
|
21
|
+
from .server import OpenEdisonProxy
|
22
|
+
|
23
|
+
log: Any = _log
|
24
|
+
|
25
|
+
|
26
|
+
def _parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
27
|
+
parser: Any = argparse.ArgumentParser(
|
28
|
+
prog="open-edison",
|
29
|
+
description="Open Edison - Single-user MCP proxy server",
|
30
|
+
)
|
31
|
+
|
32
|
+
# Top-level options for default run mode
|
33
|
+
parser.add_argument(
|
34
|
+
"--config-dir",
|
35
|
+
type=Path,
|
36
|
+
help="Directory containing config.json and related files. If omitted, uses OPEN_EDISON_CONFIG_DIR or package root.",
|
37
|
+
)
|
38
|
+
parser.add_argument("--host", type=str, help="Server host override")
|
39
|
+
parser.add_argument(
|
40
|
+
"--port", type=int, help="Server port override (FastMCP on port, FastAPI on port+1)"
|
41
|
+
)
|
42
|
+
# Website runs from packaged assets by default; no extra website flags
|
43
|
+
|
44
|
+
# Subcommands (extensible)
|
45
|
+
subparsers = parser.add_subparsers(dest="command", required=False)
|
46
|
+
|
47
|
+
# import-mcp: import MCP servers from other tools into config.json
|
48
|
+
sp_import = subparsers.add_parser(
|
49
|
+
"import-mcp",
|
50
|
+
help="Import MCP servers from other tools (Cursor, Windsurf, Cline, Claude Desktop, etc.)",
|
51
|
+
description=(
|
52
|
+
"Import MCP server configurations from other tools into Open Edison config.json.\n"
|
53
|
+
"Use --source to choose the tool and optional flags to control merging."
|
54
|
+
),
|
55
|
+
)
|
56
|
+
sp_import.add_argument(
|
57
|
+
"--source",
|
58
|
+
choices=[
|
59
|
+
"cursor",
|
60
|
+
"windsurf",
|
61
|
+
"cline",
|
62
|
+
"claude-desktop",
|
63
|
+
"vscode",
|
64
|
+
"claude-code",
|
65
|
+
"gemini-cli",
|
66
|
+
"codex",
|
67
|
+
"interactive",
|
68
|
+
],
|
69
|
+
default="interactive",
|
70
|
+
help="Source application to import from",
|
71
|
+
)
|
72
|
+
sp_import.add_argument(
|
73
|
+
"--project-dir",
|
74
|
+
type=Path,
|
75
|
+
help="When --source=cursor, path to the project containing .cursor/mcp.json",
|
76
|
+
)
|
77
|
+
sp_import.add_argument(
|
78
|
+
"--config-dir",
|
79
|
+
type=Path,
|
80
|
+
help=(
|
81
|
+
"Directory containing target config.json (default: OPEN_EDISON_CONFIG_DIR or repo root)."
|
82
|
+
),
|
83
|
+
)
|
84
|
+
sp_import.add_argument(
|
85
|
+
"--merge",
|
86
|
+
choices=["skip", "overwrite", "rename"],
|
87
|
+
default="skip",
|
88
|
+
help="Merge policy for duplicate server names",
|
89
|
+
)
|
90
|
+
sp_import.add_argument(
|
91
|
+
"--enable-imported",
|
92
|
+
action="store_true",
|
93
|
+
help="Enable imported servers (default: disabled)",
|
94
|
+
)
|
95
|
+
sp_import.add_argument(
|
96
|
+
"--dry-run",
|
97
|
+
action="store_true",
|
98
|
+
help="Show changes without writing to config.json",
|
99
|
+
)
|
100
|
+
|
101
|
+
return parser.parse_args(argv)
|
102
|
+
|
103
|
+
|
104
|
+
def _spawn_frontend_dev( # noqa: C901 - pragmatic complexity for env probing
|
105
|
+
port: int,
|
106
|
+
override_dir: Path | None = None,
|
107
|
+
config_dir: Path | None = None,
|
108
|
+
) -> tuple[int, _subprocess.Popen[bytes] | None]:
|
109
|
+
"""Try to start the frontend dev server by running `npm run dev`.
|
110
|
+
|
111
|
+
Search order for working directory:
|
112
|
+
1) Packaged project path: <pkg_root>/frontend
|
113
|
+
2) Current working directory (if it contains a package.json)
|
114
|
+
"""
|
115
|
+
candidates: list[Path] = []
|
116
|
+
# Prefer packaged static assets; if present, the backend serves /dashboard
|
117
|
+
static_candidates = [
|
118
|
+
Path(__file__).parent / "frontend_dist", # inside package dir
|
119
|
+
Path(__file__).parent.parent / "frontend_dist", # site-packages root
|
120
|
+
]
|
121
|
+
static_dir = next((p for p in static_candidates if p.exists() and p.is_dir()), None)
|
122
|
+
if static_dir is not None:
|
123
|
+
log.info(
|
124
|
+
f"Packaged dashboard detected at {static_dir}. It will be served at /dashboard by the API server."
|
125
|
+
)
|
126
|
+
# No separate website process needed. Return sentinel port (-1) so caller knows not to warn.
|
127
|
+
return (-1, None)
|
128
|
+
pkg_frontend_candidates = [
|
129
|
+
Path(__file__).parent / "frontend", # inside package dir
|
130
|
+
Path(__file__).parent.parent / "frontend", # site-packages root
|
131
|
+
]
|
132
|
+
if override_dir is not None:
|
133
|
+
candidates.append(override_dir)
|
134
|
+
for pf in pkg_frontend_candidates:
|
135
|
+
if pf.exists():
|
136
|
+
candidates.append(pf)
|
137
|
+
if config_dir is not None and (config_dir / "package.json").exists():
|
138
|
+
candidates.append(config_dir)
|
139
|
+
cwd_pkg = Path.cwd()
|
140
|
+
if (cwd_pkg / "package.json").exists():
|
141
|
+
candidates.append(cwd_pkg)
|
142
|
+
|
143
|
+
if not candidates:
|
144
|
+
log.warning(
|
145
|
+
"No frontend directory found (no packaged frontend and no package.json in CWD). Skipping website."
|
146
|
+
)
|
147
|
+
return (port, None)
|
148
|
+
|
149
|
+
for candidate in candidates:
|
150
|
+
try:
|
151
|
+
# If no package.json but directory exists, try a basic npm i per user request
|
152
|
+
if not (candidate / "package.json").exists():
|
153
|
+
log.info(f"No package.json in {candidate}. Running 'npm i' as best effort...")
|
154
|
+
_ = _subprocess.call(["npm", "i"], cwd=str(candidate))
|
155
|
+
|
156
|
+
# Install deps if needed
|
157
|
+
if (
|
158
|
+
not (candidate / "node_modules").exists()
|
159
|
+
and (candidate / "package-lock.json").exists()
|
160
|
+
):
|
161
|
+
log.info(f"Installing frontend dependencies with npm ci in {candidate}...")
|
162
|
+
r_install = _subprocess.call(["npm", "ci"], cwd=str(candidate))
|
163
|
+
if r_install != 0:
|
164
|
+
log.error("Failed to install frontend dependencies")
|
165
|
+
continue
|
166
|
+
|
167
|
+
log.info(f"Starting frontend dev server in {candidate} on port {port}...")
|
168
|
+
cmd_default = ["npm", "run", "dev", "--", "--port", str(port)]
|
169
|
+
proc = _subprocess.Popen(cmd_default, cwd=str(candidate))
|
170
|
+
return (port, proc)
|
171
|
+
except FileNotFoundError:
|
172
|
+
log.error("npm not found. Please install Node.js to run the website dev server.")
|
173
|
+
return (port, None)
|
174
|
+
|
175
|
+
# If all candidates failed
|
176
|
+
return (port, None)
|
177
|
+
|
178
|
+
|
179
|
+
async def _run_server(args: Any) -> None:
|
180
|
+
# Resolve config dir and expose via env for the rest of the app
|
181
|
+
config_dir_arg = getattr(args, "config_dir", None)
|
182
|
+
if config_dir_arg is not None:
|
183
|
+
os.environ["OPEN_EDISON_CONFIG_DIR"] = str(Path(config_dir_arg).expanduser().resolve())
|
184
|
+
config_dir = get_config_dir()
|
185
|
+
|
186
|
+
# Load config after setting env override
|
187
|
+
cfg = Config.load()
|
188
|
+
|
189
|
+
host = getattr(args, "host", None) or cfg.server.host
|
190
|
+
port = getattr(args, "port", None) or cfg.server.port
|
191
|
+
|
192
|
+
log.info(f"Using config directory: {config_dir}")
|
193
|
+
proxy = OpenEdisonProxy(host=host, port=port)
|
194
|
+
|
195
|
+
# Website served from packaged assets by default; still detect and log
|
196
|
+
frontend_proc = None
|
197
|
+
used_port, frontend_proc = _spawn_frontend_dev(5173, None, config_dir)
|
198
|
+
if frontend_proc is None and used_port == -1:
|
199
|
+
log.info("Frontend is being served from packaged assets at /dashboard")
|
200
|
+
|
201
|
+
try:
|
202
|
+
await proxy.start()
|
203
|
+
_ = await asyncio.Event().wait()
|
204
|
+
except KeyboardInterrupt:
|
205
|
+
log.info("Received shutdown signal")
|
206
|
+
finally:
|
207
|
+
if frontend_proc is not None:
|
208
|
+
with suppress(Exception):
|
209
|
+
frontend_proc.terminate()
|
210
|
+
_ = frontend_proc.wait(timeout=5)
|
211
|
+
with suppress(Exception):
|
212
|
+
frontend_proc.kill()
|
213
|
+
|
214
|
+
|
215
|
+
def _run_website(port: int, website_dir: Path | None = None) -> int:
|
216
|
+
# Use the same spawning logic, then return 0 if started or 1 if failed
|
217
|
+
_, proc = _spawn_frontend_dev(port, website_dir)
|
218
|
+
return 0 if proc is not None else 1
|
219
|
+
|
220
|
+
|
221
|
+
def main(argv: list[str] | None = None) -> NoReturn:
|
222
|
+
args = _parse_args(argv)
|
223
|
+
|
224
|
+
if getattr(args, "command", None) == "website":
|
225
|
+
exit_code = _run_website(port=args.port, website_dir=getattr(args, "dir", None))
|
226
|
+
raise SystemExit(exit_code)
|
227
|
+
|
228
|
+
if getattr(args, "command", None) == "import-mcp":
|
229
|
+
# Defer-import importer package (lives under repository scripts/)
|
230
|
+
importer_pkg = Path(__file__).parent.parent / "scripts" / "mcp_importer"
|
231
|
+
try:
|
232
|
+
if str(importer_pkg) not in sys.path:
|
233
|
+
sys.path.insert(0, str(importer_pkg))
|
234
|
+
from mcp_importer.cli import run_cli # type: ignore
|
235
|
+
except Exception as imp_exc: # noqa: BLE001
|
236
|
+
log.error(
|
237
|
+
"Failed to load MCP importer package from {}: {}",
|
238
|
+
importer_pkg,
|
239
|
+
imp_exc,
|
240
|
+
)
|
241
|
+
raise SystemExit(1) from imp_exc
|
242
|
+
|
243
|
+
importer_argv: list[str] = []
|
244
|
+
if args.source:
|
245
|
+
importer_argv += ["--source", str(args.source)]
|
246
|
+
if getattr(args, "project_dir", None):
|
247
|
+
importer_argv += [
|
248
|
+
"--project-dir",
|
249
|
+
str(Path(args.project_dir).expanduser().resolve()),
|
250
|
+
]
|
251
|
+
if getattr(args, "config_dir", None):
|
252
|
+
importer_argv += [
|
253
|
+
"--config-dir",
|
254
|
+
str(Path(args.config_dir).expanduser().resolve()),
|
255
|
+
]
|
256
|
+
if args.merge:
|
257
|
+
importer_argv += ["--merge", str(args.merge)]
|
258
|
+
if bool(getattr(args, "enable_imported", False)):
|
259
|
+
importer_argv += ["--enable-imported"]
|
260
|
+
if bool(getattr(args, "dry_run", False)):
|
261
|
+
importer_argv += ["--dry-run"]
|
262
|
+
|
263
|
+
rc_val: int = int(cast(Any, run_cli)(importer_argv))
|
264
|
+
raise SystemExit(rc_val)
|
265
|
+
|
266
|
+
# default: run server (top-level flags)
|
267
|
+
try:
|
268
|
+
asyncio.run(_run_server(args))
|
269
|
+
raise SystemExit(0)
|
270
|
+
except KeyboardInterrupt:
|
271
|
+
raise SystemExit(0) from None
|
272
|
+
except Exception as exc: # noqa: BLE001
|
273
|
+
log.error(f"Fatal error: {exc}")
|
274
|
+
raise SystemExit(1) from exc
|
src/config.py
ADDED
@@ -0,0 +1,224 @@
|
|
1
|
+
"""
|
2
|
+
Configuration management for Open Edison
|
3
|
+
|
4
|
+
Simple JSON-based configuration for single-user MCP proxy.
|
5
|
+
No database, no multi-user support - just local file-based config.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import json
|
9
|
+
import os
|
10
|
+
import sys
|
11
|
+
import tomllib
|
12
|
+
from dataclasses import asdict, dataclass
|
13
|
+
from pathlib import Path
|
14
|
+
from typing import Any
|
15
|
+
|
16
|
+
from loguru import logger as log
|
17
|
+
|
18
|
+
# Get the path to the repository/package root directory (module src/ parent)
|
19
|
+
root_dir = Path(__file__).parent.parent
|
20
|
+
|
21
|
+
|
22
|
+
def get_config_dir() -> Path:
|
23
|
+
"""Resolve configuration directory for runtime.
|
24
|
+
|
25
|
+
Order of precedence:
|
26
|
+
1) Environment variable OPEN_EDISON_CONFIG_DIR (if set)
|
27
|
+
2) OS-appropriate user config directory under app name
|
28
|
+
- macOS: ~/Library/Application Support/Open Edison
|
29
|
+
- Windows: %APPDATA%/Open Edison
|
30
|
+
- Linux/Unix: $XDG_CONFIG_HOME/open-edison or ~/.config/open-edison
|
31
|
+
"""
|
32
|
+
env_dir = os.environ.get("OPEN_EDISON_CONFIG_DIR")
|
33
|
+
if env_dir:
|
34
|
+
try:
|
35
|
+
return Path(env_dir).expanduser().resolve()
|
36
|
+
except Exception:
|
37
|
+
# Fall through to defaults
|
38
|
+
pass
|
39
|
+
|
40
|
+
# Platform-specific defaults
|
41
|
+
try:
|
42
|
+
if sys.platform == "darwin":
|
43
|
+
base = Path.home() / "Library" / "Application Support"
|
44
|
+
return (base / "Open Edison").resolve()
|
45
|
+
if os.name == "nt": # Windows
|
46
|
+
appdata = os.environ.get("APPDATA")
|
47
|
+
base = Path(appdata) if appdata else Path.home() / "AppData" / "Roaming"
|
48
|
+
return (base / "Open Edison").resolve()
|
49
|
+
# POSIX / Linux
|
50
|
+
xdg = os.environ.get("XDG_CONFIG_HOME")
|
51
|
+
base = Path(xdg).expanduser() if xdg else Path.home() / ".config"
|
52
|
+
return (base / "open-edison").resolve()
|
53
|
+
except Exception:
|
54
|
+
# Ultimate fallback: user home
|
55
|
+
return (Path.home() / ".open-edison").resolve()
|
56
|
+
|
57
|
+
|
58
|
+
# Back-compat private alias (internal modules may import this)
|
59
|
+
def _get_config_dir() -> Path: # noqa: D401
|
60
|
+
"""Alias to public get_config_dir (maintained for internal imports)."""
|
61
|
+
return get_config_dir()
|
62
|
+
|
63
|
+
|
64
|
+
def _default_config_path() -> Path:
|
65
|
+
"""Determine default config.json path.
|
66
|
+
|
67
|
+
In development (editable or source checkout), prefer repository root
|
68
|
+
`config.json` when present. In an installed package (site-packages),
|
69
|
+
use the resolved user config dir.
|
70
|
+
"""
|
71
|
+
repo_pyproject = root_dir / "pyproject.toml"
|
72
|
+
repo_config = root_dir / "config.json"
|
73
|
+
|
74
|
+
# If pyproject.toml exists next to src/, we are likely in a repo checkout
|
75
|
+
if repo_pyproject.exists():
|
76
|
+
return repo_config
|
77
|
+
|
78
|
+
# Otherwise, prefer user config directory
|
79
|
+
return get_config_dir() / "config.json"
|
80
|
+
|
81
|
+
|
82
|
+
class ConfigError(Exception):
|
83
|
+
"""Exception raised for configuration-related errors"""
|
84
|
+
|
85
|
+
def __init__(self, message: str, config_path: Path | None = None):
|
86
|
+
self.message = message
|
87
|
+
self.config_path = config_path
|
88
|
+
super().__init__(self.message)
|
89
|
+
|
90
|
+
|
91
|
+
@dataclass
|
92
|
+
class ServerConfig:
|
93
|
+
"""Server configuration"""
|
94
|
+
|
95
|
+
host: str = "localhost"
|
96
|
+
port: int = 3000
|
97
|
+
api_key: str = "dev-api-key-change-me"
|
98
|
+
|
99
|
+
|
100
|
+
@dataclass
|
101
|
+
class LoggingConfig:
|
102
|
+
"""Logging configuration"""
|
103
|
+
|
104
|
+
level: str = "INFO"
|
105
|
+
database_path: str = "sessions.db"
|
106
|
+
|
107
|
+
|
108
|
+
@dataclass
|
109
|
+
class MCPServerConfig:
|
110
|
+
"""Individual MCP server configuration"""
|
111
|
+
|
112
|
+
name: str
|
113
|
+
command: str
|
114
|
+
args: list[str]
|
115
|
+
env: dict[str, str] | None = None
|
116
|
+
enabled: bool = True
|
117
|
+
roots: list[str] | None = None
|
118
|
+
|
119
|
+
def __post_init__(self):
|
120
|
+
if self.env is None:
|
121
|
+
self.env = {}
|
122
|
+
|
123
|
+
|
124
|
+
@dataclass
|
125
|
+
class Config:
|
126
|
+
"""Main configuration class"""
|
127
|
+
|
128
|
+
server: ServerConfig
|
129
|
+
logging: LoggingConfig
|
130
|
+
mcp_servers: list[MCPServerConfig]
|
131
|
+
|
132
|
+
@property
|
133
|
+
def version(self) -> str:
|
134
|
+
"""Get version from pyproject.toml"""
|
135
|
+
try:
|
136
|
+
pyproject_path = root_dir / "pyproject.toml"
|
137
|
+
if pyproject_path.exists():
|
138
|
+
with open(pyproject_path, "rb") as f:
|
139
|
+
pyproject_data = tomllib.load(f)
|
140
|
+
project_data = pyproject_data.get("project", {}) # type: ignore
|
141
|
+
version = project_data.get("version", "unknown") # type: ignore
|
142
|
+
return str(version) # type: ignore
|
143
|
+
return "unknown"
|
144
|
+
except Exception as e:
|
145
|
+
log.warning(f"Failed to read version from pyproject.toml: {e}")
|
146
|
+
return "unknown"
|
147
|
+
|
148
|
+
@classmethod
|
149
|
+
def load(cls, config_path: Path | None = None) -> "Config":
|
150
|
+
"""Load configuration from JSON file.
|
151
|
+
|
152
|
+
If a directory path is provided, will look for `config.json` inside it.
|
153
|
+
If no path is provided, uses OPEN_EDISON_CONFIG_DIR or project root.
|
154
|
+
"""
|
155
|
+
if config_path is None:
|
156
|
+
config_path = _default_config_path()
|
157
|
+
else:
|
158
|
+
# If a directory was passed, use config.json inside it
|
159
|
+
if config_path.is_dir():
|
160
|
+
config_path = config_path / "config.json"
|
161
|
+
|
162
|
+
if not config_path.exists():
|
163
|
+
log.warning(f"Config file not found at {config_path}, creating default config")
|
164
|
+
default_config = cls.create_default()
|
165
|
+
default_config.save(config_path)
|
166
|
+
return default_config
|
167
|
+
|
168
|
+
with open(config_path) as f:
|
169
|
+
data: dict[str, Any] = json.load(f)
|
170
|
+
|
171
|
+
mcp_servers_data = data.get("mcp_servers", []) # type: ignore
|
172
|
+
server_data = data.get("server", {}) # type: ignore
|
173
|
+
logging_data = data.get("logging", {}) # type: ignore
|
174
|
+
|
175
|
+
return cls(
|
176
|
+
server=ServerConfig(**server_data), # type: ignore
|
177
|
+
logging=LoggingConfig(**logging_data), # type: ignore
|
178
|
+
mcp_servers=[
|
179
|
+
MCPServerConfig(**server_item) # type: ignore
|
180
|
+
for server_item in mcp_servers_data # type: ignore
|
181
|
+
],
|
182
|
+
)
|
183
|
+
|
184
|
+
def save(self, config_path: Path | None = None) -> None:
|
185
|
+
"""Save configuration to JSON file"""
|
186
|
+
if config_path is None:
|
187
|
+
config_path = _default_config_path()
|
188
|
+
else:
|
189
|
+
# If a directory was passed, save to config.json inside it
|
190
|
+
if config_path.is_dir():
|
191
|
+
config_path = config_path / "config.json"
|
192
|
+
|
193
|
+
data = {
|
194
|
+
"server": asdict(self.server),
|
195
|
+
"logging": asdict(self.logging),
|
196
|
+
"mcp_servers": [asdict(server) for server in self.mcp_servers],
|
197
|
+
}
|
198
|
+
|
199
|
+
# Ensure directory exists
|
200
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
201
|
+
with open(config_path, "w") as f:
|
202
|
+
json.dump(data, f, indent=2)
|
203
|
+
|
204
|
+
log.info(f"Configuration saved to {config_path}")
|
205
|
+
|
206
|
+
@classmethod
|
207
|
+
def create_default(cls) -> "Config":
|
208
|
+
"""Create default configuration"""
|
209
|
+
return cls(
|
210
|
+
server=ServerConfig(),
|
211
|
+
logging=LoggingConfig(),
|
212
|
+
mcp_servers=[
|
213
|
+
MCPServerConfig(
|
214
|
+
name="filesystem",
|
215
|
+
command="uvx",
|
216
|
+
args=["mcp-server-filesystem", "/tmp"],
|
217
|
+
enabled=False,
|
218
|
+
)
|
219
|
+
],
|
220
|
+
)
|
221
|
+
|
222
|
+
|
223
|
+
# Load global configuration
|
224
|
+
config = Config.load()
|