open-edison 0.1.84rc1__py3-none-any.whl → 0.1.85__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.84rc1.dist-info → open_edison-0.1.85.dist-info}/METADATA +1 -1
- {open_edison-0.1.84rc1.dist-info → open_edison-0.1.85.dist-info}/RECORD +8 -8
- src/cli.py +51 -0
- src/mcp_importer/api.py +21 -0
- src/mcp_importer/exporters.py +173 -11
- {open_edison-0.1.84rc1.dist-info → open_edison-0.1.85.dist-info}/WHEEL +0 -0
- {open_edison-0.1.84rc1.dist-info → open_edison-0.1.85.dist-info}/entry_points.txt +0 -0
- {open_edison-0.1.84rc1.dist-info → open_edison-0.1.85.dist-info}/licenses/LICENSE +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
src/__init__.py,sha256=bEYMwBiuW9jzF07iWhas4Vb30EcpnqfpNfz_Q6yO1jU,209
|
2
2
|
src/__main__.py,sha256=kQsaVyzRa_ESC57JpKDSQJAHExuXme0rM5beJsYxFeA,161
|
3
|
-
src/cli.py,sha256=
|
3
|
+
src/cli.py,sha256=_ATSA4QPiyFupf84B4OAY3Av0oM5wnZ8SwHGk7zBuNY,7871
|
4
4
|
src/config.py,sha256=wLjU18HzmA34rGD6FII3pTlOQvnY4KcnfoIOZ3QXHhQ,13491
|
5
5
|
src/config.pyi,sha256=HJ5l-Kk8W_-qW_4y4kw4aowHSPLf5SOZxB_yJvtmqXc,2260
|
6
6
|
src/events.py,sha256=KkhrQ9CE5-WBBCeDkUgdCZURKsXakNP3Kj3gP91NYQM,5046
|
@@ -19,10 +19,10 @@ src/frontend_dist/assets/index-D05VN_1l.css,sha256=TVt1HETNMBbfMcrUfYuEE0a-tfpxC
|
|
19
19
|
src/frontend_dist/assets/index-D6ziuTsl.js,sha256=lwKVKobmch7-DzowsVXL3VazVV0nr7-D8gmAsoJ-Hj8,264213
|
20
20
|
src/mcp_importer/__init__.py,sha256=Mk59pVr7OMGfYGWeSYk8-URfhIcrs3SPLYS7fmJbMII,275
|
21
21
|
src/mcp_importer/__main__.py,sha256=mFcxXFqJMC0SFEqIP-9WVEqLJSYqShC0x1Ht7PQZPm8,479
|
22
|
-
src/mcp_importer/api.py,sha256=
|
22
|
+
src/mcp_importer/api.py,sha256=yjXVUlNgjeJ1RtkL46xuna7OiIh4qgg8rZG1mIB1yl4,18115
|
23
23
|
src/mcp_importer/cli.py,sha256=Pe0GLWm1nMd1VuNXOSkxIrFZuGNFc9dNvfBsvf-bdBI,3487
|
24
24
|
src/mcp_importer/export_cli.py,sha256=Fw0jDQCI8gGW4BDrJLzWjLUtV4q6v0h2QZ7HF1V2Jcg,6279
|
25
|
-
src/mcp_importer/exporters.py,sha256=
|
25
|
+
src/mcp_importer/exporters.py,sha256=rNGWtcEJ5cl5zgzxOjZdS2IrcNZ3ZkTIrCS2Rny0BIA,16887
|
26
26
|
src/mcp_importer/import_api.py,sha256=wD5yqxWwFfn1MQNKE79rEeyZODdmPgUDhsRYdCJYh4Q,59
|
27
27
|
src/mcp_importer/importers.py,sha256=NSZURilb-pmCZQ6ydyHIoMrZ-18CvREG2UN3MvuX3yY,2677
|
28
28
|
src/mcp_importer/merge.py,sha256=KIGT7UgbAm07-LdyoUXEJ7ABSIiPTFlj_qjz669yFxg,1569
|
@@ -35,8 +35,8 @@ src/middleware/session_tracking.py,sha256=cmmeDzI19uQ3WBUbKkIOGfVEDNaFkpI9M2Q3-J
|
|
35
35
|
src/setup_tui/__init__.py,sha256=mDFrQoiOtQOHc0sFfGKrNXVLEDeB1S0O5aISBVzfxYo,184
|
36
36
|
src/setup_tui/main.py,sha256=Cc1T3qn4V4WST9Q_ToOJLBjP16mZRNFUBmQ9HEkYQHU,12341
|
37
37
|
src/tools/io.py,sha256=hhc4pv3eUzYWSZ7BbThclxSMwWBQaGMoGsItIPf_pco,1047
|
38
|
-
open_edison-0.1.
|
39
|
-
open_edison-0.1.
|
40
|
-
open_edison-0.1.
|
41
|
-
open_edison-0.1.
|
42
|
-
open_edison-0.1.
|
38
|
+
open_edison-0.1.85.dist-info/METADATA,sha256=D8_-XHWJcoxBhq8tF-L1vTKCbAYlPbGoQkAbWRZkQlU,12031
|
39
|
+
open_edison-0.1.85.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
40
|
+
open_edison-0.1.85.dist-info/entry_points.txt,sha256=YiGNm9x2I00hgT10HDyB4gxC1LcaV_mu8bXFjolu0Yw,171
|
41
|
+
open_edison-0.1.85.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
42
|
+
open_edison-0.1.85.dist-info/RECORD,,
|
src/cli.py
CHANGED
@@ -14,6 +14,7 @@ from loguru import logger as log
|
|
14
14
|
|
15
15
|
from src.config import Config, get_config_dir, get_config_json_path
|
16
16
|
from src.demos.trifecta import demo_config_dir, run_trifecta_demo
|
17
|
+
from src.mcp_importer.api import detect_clients, restore_client
|
17
18
|
from src.mcp_importer.cli import run_cli
|
18
19
|
from src.server import OpenEdisonProxy
|
19
20
|
from src.setup_tui.main import run_import_tui
|
@@ -106,6 +107,21 @@ def _parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
106
107
|
),
|
107
108
|
)
|
108
109
|
|
110
|
+
# restore-clients: restore editor configs from backups or remove OE-only config
|
111
|
+
sp_restore = subparsers.add_parser(
|
112
|
+
"restore-clients",
|
113
|
+
help="Restore backed up MCP client configs (Cursor, VS Code, Claude Code)",
|
114
|
+
description=(
|
115
|
+
"Detect installed clients and restore their MCP config from the most recent backup, "
|
116
|
+
"or remove the Open Edison-only MCP entry if no backup is present."
|
117
|
+
),
|
118
|
+
)
|
119
|
+
sp_restore.add_argument(
|
120
|
+
"--dry-run",
|
121
|
+
action="store_true",
|
122
|
+
help="Preview restore operations without writing",
|
123
|
+
)
|
124
|
+
|
109
125
|
return parser.parse_args(argv)
|
110
126
|
|
111
127
|
|
@@ -158,6 +174,41 @@ def main(argv: list[str] | None = None) -> NoReturn: # noqa: C901
|
|
158
174
|
asyncio.run(_run_server(args))
|
159
175
|
raise SystemExit(0)
|
160
176
|
|
177
|
+
if args.command == "restore-clients":
|
178
|
+
# Detect clients and prompt user per client
|
179
|
+
available = sorted(detect_clients(), key=lambda c: c.value)
|
180
|
+
if not available:
|
181
|
+
log.info("No supported MCP clients detected")
|
182
|
+
raise SystemExit(0)
|
183
|
+
from questionary import confirm
|
184
|
+
|
185
|
+
for client in available:
|
186
|
+
if not confirm(
|
187
|
+
f"Restore original MCP config for {client.value}? (removes Open Edison)", default=True
|
188
|
+
).ask():
|
189
|
+
continue
|
190
|
+
try:
|
191
|
+
res = restore_client(client, dry_run=getattr(args, "dry_run", False))
|
192
|
+
if getattr(args, "dry_run", False):
|
193
|
+
log.info("[dry-run] {}: would restore at {}", client.value, res.target_path)
|
194
|
+
else:
|
195
|
+
if res.restored_from_backup is not None:
|
196
|
+
log.info(
|
197
|
+
"Restored {} from backup {}", client.value, res.restored_from_backup
|
198
|
+
)
|
199
|
+
elif res.removed_open_edison_only:
|
200
|
+
log.info(
|
201
|
+
"Removed Open Edison-only entry from {} at {}",
|
202
|
+
client.value,
|
203
|
+
res.target_path,
|
204
|
+
)
|
205
|
+
else:
|
206
|
+
log.info("No restore action taken for {}", client.value)
|
207
|
+
except Exception as e:
|
208
|
+
log.error(f"Restore failed for {client.value}: {e}")
|
209
|
+
raise SystemExit(1) from e
|
210
|
+
raise SystemExit(0)
|
211
|
+
|
161
212
|
# Run import tui if necessary
|
162
213
|
tui_success = run_import_tui(args, force=args.wizard_force)
|
163
214
|
if not tui_success:
|
src/mcp_importer/api.py
CHANGED
@@ -15,9 +15,13 @@ from src.config import Config, MCPServerConfig, get_config_json_path
|
|
15
15
|
from src.mcp_importer import paths as _paths
|
16
16
|
from src.mcp_importer.exporters import (
|
17
17
|
ExportResult,
|
18
|
+
RestoreResult,
|
18
19
|
export_to_claude_code,
|
19
20
|
export_to_cursor,
|
20
21
|
export_to_vscode,
|
22
|
+
restore_claude_code,
|
23
|
+
restore_cursor,
|
24
|
+
restore_vscode,
|
21
25
|
)
|
22
26
|
from src.mcp_importer.importers import (
|
23
27
|
import_from_claude_code,
|
@@ -135,6 +139,23 @@ def export_edison_to(
|
|
135
139
|
)
|
136
140
|
|
137
141
|
|
142
|
+
def restore_client(
|
143
|
+
client: CLIENT,
|
144
|
+
*,
|
145
|
+
server_name: str = "open-edison",
|
146
|
+
dry_run: bool = False,
|
147
|
+
) -> RestoreResult:
|
148
|
+
if dry_run:
|
149
|
+
print(f"[dry-run] Would restore original MCP config for '{client}' (using latest backup if present)")
|
150
|
+
match client:
|
151
|
+
case CLIENT.CURSOR:
|
152
|
+
return restore_cursor(server_name=server_name, dry_run=dry_run)
|
153
|
+
case CLIENT.VSCODE:
|
154
|
+
return restore_vscode(server_name=server_name, dry_run=dry_run)
|
155
|
+
case CLIENT.CLAUDE_CODE:
|
156
|
+
return restore_claude_code(server_name=server_name, dry_run=dry_run)
|
157
|
+
|
158
|
+
|
138
159
|
def verify_mcp_server(server: MCPServerConfig) -> bool: # noqa
|
139
160
|
"""Minimal validation: try listing tools/resources/prompts via FastMCP within a timeout."""
|
140
161
|
|
src/mcp_importer/exporters.py
CHANGED
@@ -5,7 +5,7 @@ import tempfile
|
|
5
5
|
from dataclasses import dataclass
|
6
6
|
from datetime import datetime
|
7
7
|
from pathlib import Path
|
8
|
-
from typing import Any
|
8
|
+
from typing import Any, cast
|
9
9
|
|
10
10
|
from loguru import logger as log
|
11
11
|
|
@@ -63,7 +63,8 @@ def _read_json_or_error(path: Path) -> dict[str, Any]:
|
|
63
63
|
raise ExportError(f"Malformed JSON at {path}: {e}") from e
|
64
64
|
if not isinstance(data, dict):
|
65
65
|
raise ExportError(f"Expected top-level JSON object at {path}")
|
66
|
-
|
66
|
+
typed_data: dict[str, Any] = cast(dict[str, Any], data)
|
67
|
+
return typed_data
|
67
68
|
|
68
69
|
|
69
70
|
def _require_supported_os() -> None:
|
@@ -188,21 +189,25 @@ def _build_open_edison_server(
|
|
188
189
|
def _is_already_open_edison(
|
189
190
|
config_obj: dict[str, Any], *, url: str, api_key: str, name: str
|
190
191
|
) -> bool:
|
191
|
-
|
192
|
-
if not isinstance(
|
192
|
+
servers_raw: Any = config_obj.get("mcpServers") or config_obj.get("servers")
|
193
|
+
if not isinstance(servers_raw, dict):
|
193
194
|
return False
|
195
|
+
servers_node: dict[str, Any] = cast(dict[str, Any], servers_raw)
|
194
196
|
# Must be exactly one server
|
195
197
|
if len(servers_node) != 1:
|
196
198
|
return False
|
197
|
-
only_name,
|
198
|
-
if only_name != name or not isinstance(
|
199
|
+
only_name, only_spec_any = next(iter(servers_node.items()))
|
200
|
+
if only_name != name or not isinstance(only_spec_any, dict):
|
199
201
|
return False
|
200
|
-
|
202
|
+
only_spec: dict[str, Any] = cast(dict[str, Any], only_spec_any)
|
203
|
+
cmd_val_any: Any = only_spec.get("command")
|
204
|
+
if cmd_val_any != "npx":
|
201
205
|
return False
|
202
|
-
|
203
|
-
if not isinstance(
|
206
|
+
args_obj_any: Any = only_spec.get("args")
|
207
|
+
if not isinstance(args_obj_any, list):
|
204
208
|
return False
|
205
|
-
|
209
|
+
args_list: list[Any] = cast(list[Any], args_obj_any)
|
210
|
+
args_str = [str(a) for a in args_list]
|
206
211
|
expected_header = f"Authorization: Bearer {api_key}"
|
207
212
|
return (
|
208
213
|
url in args_str
|
@@ -213,6 +218,163 @@ def _is_already_open_edison(
|
|
213
218
|
)
|
214
219
|
|
215
220
|
|
221
|
+
# --- Restore helpers and functions ---
|
222
|
+
|
223
|
+
|
224
|
+
@dataclass
|
225
|
+
class RestoreResult:
|
226
|
+
target_path: Path
|
227
|
+
restored_from_backup: Path | None
|
228
|
+
wrote_changes: bool
|
229
|
+
dry_run: bool
|
230
|
+
removed_open_edison_only: bool
|
231
|
+
|
232
|
+
|
233
|
+
def _find_latest_backup(target_path: Path) -> Path | None:
|
234
|
+
parent = target_path.parent
|
235
|
+
prefix = target_path.name + ".bak-"
|
236
|
+
candidates: list[Path] = [p for p in parent.glob(target_path.name + ".bak-*") if p.is_file()]
|
237
|
+
if not candidates:
|
238
|
+
return None
|
239
|
+
# Sort by timestamp portion descending (string sort works with our format)
|
240
|
+
def _key(p: Path) -> str:
|
241
|
+
return p.name.replace(prefix, "")
|
242
|
+
|
243
|
+
candidates.sort(key=_key, reverse=True)
|
244
|
+
return candidates[0]
|
245
|
+
|
246
|
+
|
247
|
+
def _is_open_edison_singleton(config_obj: dict[str, Any], *, name: str) -> bool:
|
248
|
+
servers_raw: Any = config_obj.get("mcpServers") or config_obj.get("servers")
|
249
|
+
if not isinstance(servers_raw, dict):
|
250
|
+
return False
|
251
|
+
servers_node: dict[str, Any] = cast(dict[str, Any], servers_raw)
|
252
|
+
if len(servers_node) != 1:
|
253
|
+
return False
|
254
|
+
only_name, only_spec_any = next(iter(servers_node.items()))
|
255
|
+
if only_name != name or not isinstance(only_spec_any, dict):
|
256
|
+
return False
|
257
|
+
only_spec: dict[str, Any] = cast(dict[str, Any], only_spec_any)
|
258
|
+
cmd_val_any: Any = only_spec.get("command")
|
259
|
+
if cmd_val_any != "npx":
|
260
|
+
return False
|
261
|
+
args_obj_any: Any = only_spec.get("args")
|
262
|
+
if not isinstance(args_obj_any, list):
|
263
|
+
return False
|
264
|
+
args_list: list[Any] = cast(list[Any], args_obj_any)
|
265
|
+
args_str = [str(a) for a in args_list]
|
266
|
+
return "mcp-remote" in args_str
|
267
|
+
|
268
|
+
|
269
|
+
def _restore_from_backup_or_remove(
|
270
|
+
*,
|
271
|
+
target_path: Path,
|
272
|
+
label: str,
|
273
|
+
key_name: str,
|
274
|
+
server_name: str,
|
275
|
+
dry_run: bool,
|
276
|
+
) -> RestoreResult:
|
277
|
+
backup = _find_latest_backup(target_path)
|
278
|
+
if backup is not None:
|
279
|
+
if dry_run:
|
280
|
+
log.info("[dry-run] Would restore {} from backup {}", label, backup)
|
281
|
+
return RestoreResult(
|
282
|
+
target_path=target_path,
|
283
|
+
restored_from_backup=backup,
|
284
|
+
wrote_changes=False,
|
285
|
+
dry_run=True,
|
286
|
+
removed_open_edison_only=False,
|
287
|
+
)
|
288
|
+
_ensure_parent_dir(target_path)
|
289
|
+
shutil.copy2(backup, target_path)
|
290
|
+
log.info("Restored {} from backup {}", label, backup)
|
291
|
+
return RestoreResult(
|
292
|
+
target_path=target_path,
|
293
|
+
restored_from_backup=backup,
|
294
|
+
wrote_changes=True,
|
295
|
+
dry_run=False,
|
296
|
+
removed_open_edison_only=False,
|
297
|
+
)
|
298
|
+
|
299
|
+
# No backup found; as a safety, remove the Open Edison-only MCP section if it exactly matches
|
300
|
+
if not target_path.exists():
|
301
|
+
return RestoreResult(
|
302
|
+
target_path=target_path,
|
303
|
+
restored_from_backup=None,
|
304
|
+
wrote_changes=False,
|
305
|
+
dry_run=dry_run,
|
306
|
+
removed_open_edison_only=False,
|
307
|
+
)
|
308
|
+
current = _read_json_or_error(target_path)
|
309
|
+
if _is_open_edison_singleton(current, name=server_name):
|
310
|
+
if dry_run:
|
311
|
+
log.info("[dry-run] Would remove Open Edison-only MCP section from {}", label)
|
312
|
+
return RestoreResult(
|
313
|
+
target_path=target_path,
|
314
|
+
restored_from_backup=None,
|
315
|
+
wrote_changes=False,
|
316
|
+
dry_run=True,
|
317
|
+
removed_open_edison_only=True,
|
318
|
+
)
|
319
|
+
if key_name in current:
|
320
|
+
# Remove entire MCP section
|
321
|
+
current.pop(key_name, None)
|
322
|
+
_atomic_write_json(target_path, current)
|
323
|
+
log.info("Removed Open Edison-only MCP section from {}", label)
|
324
|
+
return RestoreResult(
|
325
|
+
target_path=target_path,
|
326
|
+
restored_from_backup=None,
|
327
|
+
wrote_changes=True,
|
328
|
+
dry_run=False,
|
329
|
+
removed_open_edison_only=True,
|
330
|
+
)
|
331
|
+
# Nothing to do
|
332
|
+
return RestoreResult(
|
333
|
+
target_path=target_path,
|
334
|
+
restored_from_backup=None,
|
335
|
+
wrote_changes=False,
|
336
|
+
dry_run=dry_run,
|
337
|
+
removed_open_edison_only=False,
|
338
|
+
)
|
339
|
+
|
340
|
+
|
341
|
+
def restore_cursor(*, server_name: str = "open-edison", dry_run: bool = False) -> RestoreResult:
|
342
|
+
_require_supported_os()
|
343
|
+
target_path = _resolve_cursor_target()
|
344
|
+
return _restore_from_backup_or_remove(
|
345
|
+
target_path=target_path,
|
346
|
+
label="Cursor",
|
347
|
+
key_name="mcpServers",
|
348
|
+
server_name=server_name,
|
349
|
+
dry_run=dry_run,
|
350
|
+
)
|
351
|
+
|
352
|
+
|
353
|
+
def restore_vscode(*, server_name: str = "open-edison", dry_run: bool = False) -> RestoreResult:
|
354
|
+
_require_supported_os()
|
355
|
+
target_path = _resolve_vscode_target()
|
356
|
+
return _restore_from_backup_or_remove(
|
357
|
+
target_path=target_path,
|
358
|
+
label="VS Code",
|
359
|
+
key_name="servers",
|
360
|
+
server_name=server_name,
|
361
|
+
dry_run=dry_run,
|
362
|
+
)
|
363
|
+
|
364
|
+
|
365
|
+
def restore_claude_code(*, server_name: str = "open-edison", dry_run: bool = False) -> RestoreResult:
|
366
|
+
_require_supported_os()
|
367
|
+
target_path = _resolve_claude_code_target()
|
368
|
+
# Claude Code uses general settings format; MCP key is "mcpServers"
|
369
|
+
return _restore_from_backup_or_remove(
|
370
|
+
target_path=target_path,
|
371
|
+
label="Claude Code",
|
372
|
+
key_name="mcpServers",
|
373
|
+
server_name=server_name,
|
374
|
+
dry_run=dry_run,
|
375
|
+
)
|
376
|
+
|
377
|
+
|
216
378
|
def export_to_cursor(
|
217
379
|
*,
|
218
380
|
url: str = "http://localhost:3000/mcp/",
|
@@ -378,7 +540,7 @@ def export_to_claude_code(
|
|
378
540
|
current = {}
|
379
541
|
|
380
542
|
new_mcp = _build_open_edison_server(name=server_name, url=url, api_key=api_key)
|
381
|
-
if is_existing and
|
543
|
+
if is_existing and current:
|
382
544
|
new_config = _merge_preserving_non_mcp(current, new_mcp)
|
383
545
|
else:
|
384
546
|
new_config = {"mcpServers": new_mcp}
|
File without changes
|
File without changes
|
File without changes
|