open-edison 0.1.17__py3-none-any.whl → 0.1.19__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: open-edison
3
- Version: 0.1.17
3
+ Version: 0.1.19
4
4
  Summary: Open-source MCP security, aggregation, and monitoring. Single-user, self-hosted MCP proxy.
5
5
  Author-email: Hugo Berg <hugo@edison.watch>
6
6
  License-File: LICENSE
@@ -27,9 +27,24 @@ Description-Content-Type: text/markdown
27
27
 
28
28
  # OpenEdison
29
29
 
30
- Open-source MCP security gateway that prevents data exfiltration—via direct access or tool chaining—with full monitoring for local single‑user deployments. Provides core functionality of <https://edison.watch> for local, single-user use.
30
+ Open-source single-user MCP security gateway that prevents data exfiltration—via direct access or tool chaining—with full monitoring for local single‑user deployments. Provides core functionality of <https://edison.watch> for local use.
31
31
 
32
- Just want to run it?
32
+ <div align="center">
33
+ <h2>📧 Interested in connecting AI to your business software with proper access controls? <a href="mailto:hello@edison.watch">Contact us</a> to discuss.</h2>
34
+ </div>
35
+
36
+ ## Features
37
+
38
+ - **Single-user MCP proxy** - No multi-user complexity, just a simple proxy for your MCP servers
39
+ - **JSON configuration** - Easy to configure and manage your MCP servers
40
+ - **Simple local frontend** - Track and monitor your MCP interactions, servers, and sessions.
41
+ - **Session tracking** - Track and monitor your MCP interactions
42
+ - **Simple API** - REST API for managing MCP servers and proxying requests
43
+ - **Docker support** - Run in a container for easy deployment
44
+
45
+ ## Quick Start
46
+
47
+ The fastest way to get started:
33
48
 
34
49
  ```bash
35
50
  # Installs uv (via Astral installer) and launches open-edison with uvx.
@@ -39,36 +54,30 @@ curl -fsSL https://raw.githubusercontent.com/Edison-Watch/open-edison/main/curl_
39
54
 
40
55
  Run locally with uvx: `uvx open-edison --config-dir ~/edison-config`
41
56
 
57
+ <details>
58
+ <summary>Install Node.js/npm (optional for MCP tools)</summary>
59
+
42
60
  If you need `npx` (for Node-based MCP tools like `mcp-remote`), install Node.js as well:
43
61
 
44
- - macOS:
45
- - uv: `curl -fsSL https://astral.sh/uv/install.sh | sh`
46
- - Node/npx: `brew install node`
47
- - Linux (Debian/Ubuntu):
48
- - uv: `curl -fsSL https://astral.sh/uv/install.sh | sh`
49
- - Node/npx: `sudo apt-get update && sudo apt-get install -y nodejs npm`
50
- - Windows (PowerShell):
51
- - uv: `powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"`
52
- - Node/npx: `winget install -e --id OpenJS.NodeJS`
62
+ ![macOS](https://img.shields.io/badge/mac%20os-000000?style=for-the-badge&logo=apple&logoColor=white)
53
63
 
54
- After installation, ensure that `npx` is available on PATH.
64
+ - uv: `curl -fsSL https://astral.sh/uv/install.sh | sh`
65
+ - Node/npx: `brew install node`
55
66
 
56
- <div align="center">
57
- <h2>📧 Interested in connecting AI to your business software with proper access controls? <a href="mailto:hello@edison.watch">Contact us</a> to discuss.</h2>
58
- </div>
67
+ ![Linux](https://img.shields.io/badge/Linux-FCC624?style=for-the-badge&logo=linux&logoColor=black)
59
68
 
60
- ## Features
69
+ - uv: `curl -fsSL https://astral.sh/uv/install.sh | sh`
70
+ - Node/npx: `sudo apt-get update && sudo apt-get install -y nodejs npm`
61
71
 
62
- - **Single-user MCP proxy** - No multi-user complexity, just a simple proxy for your MCP servers
63
- - **JSON configuration** - Easy to configure and manage your MCP servers
64
- - **Simple local frontend** - Track and monitor your MCP interactions, servers, and sessions.
65
- - **Session tracking** - Track and monitor your MCP interactions
66
- - **Simple API** - REST API for managing MCP servers and proxying requests
67
- - **Docker support** - Run in a container for easy deployment
72
+ ![Windows](https://img.shields.io/badge/Windows-0078D6?style=for-the-badge&logo=windows&logoColor=white)
68
73
 
69
- ## Quick Start
74
+ - uv: `powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"`
75
+ - Node/npx: `winget install -e --id OpenJS.NodeJS`
76
+
77
+ After installation, ensure that `npx` is available on PATH.
70
78
 
71
- ### Install from PyPI
79
+ <details>
80
+ <summary><img src="https://img.shields.io/badge/pypi-3775A9?style=for-the-badge&logo=pypi&logoColor=white" alt="PyPI"> Install from PyPI</summary>
72
81
 
73
82
  #### Prerequisites
74
83
 
@@ -91,7 +100,10 @@ open-edison run --config-dir ~/edison-config
91
100
  OPEN_EDISON_CONFIG_DIR=~/edison-config open-edison run
92
101
  ```
93
102
 
94
- ### Run with Docker
103
+ </details>
104
+
105
+ <details>
106
+ <summary><img src="https://img.shields.io/badge/Docker-2CA5E0?style=for-the-badge&logo=docker&logoColor=white" alt="Docker"> Run with Docker</summary>
95
107
 
96
108
  There is a dockerfile for simple local setup.
97
109
 
@@ -110,7 +122,10 @@ make docker_run
110
122
 
111
123
  The MCP server will be available at `http://localhost:3000` and the api + frontend at `http://localhost:3001`.
112
124
 
113
- ### Run from source
125
+ </details>
126
+
127
+ <details>
128
+ <summary>⚙️ Run from source</summary>
114
129
 
115
130
  1. Clone the repository:
116
131
 
@@ -148,7 +163,10 @@ open-edison run
148
163
 
149
164
  The server will be available at `http://localhost:3000`.
150
165
 
151
- ## MCP Connection
166
+ </details>
167
+
168
+ <details>
169
+ <summary>MCP Connection</summary>
152
170
 
153
171
  Connect any MCP client to Open Edison (requires Node.js/npm for `npx`):
154
172
 
@@ -169,13 +187,17 @@ Or add to your MCP client config:
169
187
  }
170
188
  ```
171
189
 
172
- ## Usage
190
+ </details>
191
+
192
+ <details>
193
+ <summary>Usage</summary>
173
194
 
174
195
  ### API Endpoints
175
196
 
176
197
  See [API Reference](docs/quick-reference/api_reference.md) for full API documentation.
177
198
 
178
- ## Development
199
+ <details>
200
+ <summary>Development</summary>
179
201
 
180
202
  ### Setup
181
203
 
@@ -197,6 +219,11 @@ We expect `make ci` to return cleanly.
197
219
  make ci
198
220
  ```
199
221
 
222
+ </details>
223
+
224
+ <details>
225
+ <summary>⚙️ Configuration (config.json)</summary>
226
+
200
227
  ## Configuration
201
228
 
202
229
  The `config.json` file contains all configuration:
@@ -215,14 +242,23 @@ Each MCP server configuration includes:
215
242
  - `env` - Environment variables (optional)
216
243
  - `enabled` - Whether to auto-start this server
217
244
 
218
- ## Security & Permissions System
245
+ </details>
219
246
 
220
- Open Edison includes a comprehensive security monitoring system that tracks the "lethal trifecta" of AI agent risks:
247
+ </details>
248
+
249
+ <details>
250
+ <summary>Security & Permissions System</summary>
251
+
252
+ Open Edison includes a comprehensive security monitoring system that tracks the "lethal trifecta" of AI agent risks, as described in [Simon Willison's blog post](https://simonwillison.net/2025/Jun/16/the-lethal-trifecta/):
253
+
254
+ <img src="media/lethal-trifecta.png" alt="The lethal trifecta diagram showing the three key AI agent security risks" width="30%">
221
255
 
222
256
  1. **Private data access** - Access to sensitive local files/data
223
257
  2. **Untrusted content exposure** - Exposure to external/web content
224
258
  3. **External communication** - Ability to write/send data externally
225
259
 
260
+ <img src="media/pam-diagram.png" alt="Privileged Access Management (PAM) example showing the lethal trifecta in action" width="60%">
261
+
226
262
  The configuration allows you to classify these risks across **tools**, **resources**, and **prompts** using separate configuration files.
227
263
 
228
264
  In addition to trifecta, we track Access Control Level (ACL) for each tool call,
@@ -246,6 +282,9 @@ Defines security classifications for MCP tools. See full file: [tool_permissions
246
282
  }
247
283
  ```
248
284
 
285
+ <details>
286
+ <summary>Resource Permissions (`resource_permissions.json`)</summary>
287
+
249
288
  ### Resource Permissions (`resource_permissions.json`)
250
289
 
251
290
  Defines security classifications for resource access patterns. See full file: [resource_permissions.json](resource_permissions.json), it looks like:
@@ -257,6 +296,11 @@ Defines security classifications for resource access patterns. See full file: [r
257
296
  }
258
297
  ```
259
298
 
299
+ </details>
300
+
301
+ <details>
302
+ <summary>Prompt Permissions (`prompt_permissions.json`)</summary>
303
+
260
304
  ### Prompt Permissions (`prompt_permissions.json`)
261
305
 
262
306
  Defines security classifications for prompt types. See full file: [prompt_permissions.json](prompt_permissions.json), it looks like:
@@ -268,6 +312,8 @@ Defines security classifications for prompt types. See full file: [prompt_permis
268
312
  }
269
313
  ```
270
314
 
315
+ </details>
316
+
271
317
  ### Wildcard Patterns
272
318
 
273
319
  All permission types support wildcard patterns:
@@ -282,7 +328,10 @@ All permission types support wildcard patterns:
282
328
 
283
329
  Use the `get_security_status` tool to monitor your session's current risk level and see which capabilities have been accessed. When the lethal trifecta is achieved (all three risk flags set), further potentially dangerous operations are blocked.
284
330
 
285
- ## Documentation
331
+ </details>
332
+
333
+ <details>
334
+ <summary>Documentation</summary>
286
335
 
287
336
  📚 **Complete documentation available in [`docs/`](docs/)**
288
337
 
@@ -291,6 +340,11 @@ Use the `get_security_status` tool to monitor your session's current risk level
291
340
  - **[API Reference](docs/quick-reference/api_reference.md)** - REST API documentation
292
341
  - **[Development Guide](docs/development/development_guide.md)** - Contributing and development
293
342
 
294
- ## License
343
+ </details>
344
+
345
+ <details>
346
+ <summary>License</summary>
295
347
 
296
348
  GPL-3.0 License - see [LICENSE](LICENSE) for details.
349
+
350
+ </details>
@@ -0,0 +1,14 @@
1
+ src/__init__.py,sha256=QWeZdjAm2D2B0eWhd8m2-DPpWvIP26KcNJxwEoU1oEQ,254
2
+ src/__main__.py,sha256=kQsaVyzRa_ESC57JpKDSQJAHExuXme0rM5beJsYxFeA,161
3
+ src/cli.py,sha256=9cJN6mRvjbCcpTyTdUVl47J7OB7bxzSy0h8tfVbHuQU,9982
4
+ src/config.py,sha256=2a5rdImQmNGggL690PQprqZVsRUAJcdo8KS2Foj9N-U,9345
5
+ src/server.py,sha256=cXW16m6UMUofQFbtM6E2EasxClhWAS-955BuasNupmM,29557
6
+ src/single_user_mcp.py,sha256=3pDBMant1DNlNPeW_NWD-uFyLrA-qNrx6sDHgDKsDfM,14457
7
+ src/telemetry.py,sha256=M8iZ7nTPA6BhbPna_xsEoTOOa7A81YyvZ0CkVYa_pPg,12619
8
+ src/middleware/data_access_tracker.py,sha256=N4g_T-JF9W7yzRIFRasY-JA7ha-Zt_Ov4nSn-TCq-Ps,27026
9
+ src/middleware/session_tracking.py,sha256=O-n8RvEVCUGAFGYny_gA7-MMQYSlvND-lj3oBZLCT3U,20046
10
+ open_edison-0.1.19.dist-info/METADATA,sha256=88QHl-ngXl0uFZwYUO2dVMtxmQ0T3eyVUE7NyP6jXAY,10905
11
+ open_edison-0.1.19.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
12
+ open_edison-0.1.19.dist-info/entry_points.txt,sha256=qNAkJcnoTXRhj8J--3PDmXz_TQKdB8H_0C9wiCtDIyA,72
13
+ open_edison-0.1.19.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
14
+ open_edison-0.1.19.dist-info/RECORD,,
@@ -171,6 +171,12 @@ def _load_tool_permissions_cached() -> dict[str, dict[str, Any]]:
171
171
  return {}
172
172
 
173
173
 
174
+ def clear_tool_permissions_cache() -> None:
175
+ """Clear the tool permissions cache to force reload from file."""
176
+ _load_tool_permissions_cached.cache_clear()
177
+ log.info("Tool permissions cache cleared")
178
+
179
+
174
180
  @cache
175
181
  def _load_resource_permissions_cached() -> dict[str, dict[str, Any]]:
176
182
  """Load resource permissions from JSON configuration file with LRU caching."""
@@ -186,6 +192,12 @@ def _load_resource_permissions_cached() -> dict[str, dict[str, Any]]:
186
192
  return {}
187
193
 
188
194
 
195
+ def clear_resource_permissions_cache() -> None:
196
+ """Clear the resource permissions cache to force reload from file."""
197
+ _load_resource_permissions_cached.cache_clear()
198
+ log.info("Resource permissions cache cleared")
199
+
200
+
189
201
  @cache
190
202
  def _load_prompt_permissions_cached() -> dict[str, dict[str, Any]]:
191
203
  """Load prompt permissions from JSON configuration file with LRU caching."""
@@ -201,6 +213,20 @@ def _load_prompt_permissions_cached() -> dict[str, dict[str, Any]]:
201
213
  return {}
202
214
 
203
215
 
216
+ def clear_prompt_permissions_cache() -> None:
217
+ """Clear the prompt permissions cache to force reload from file."""
218
+ _load_prompt_permissions_cached.cache_clear()
219
+ log.info("Prompt permissions cache cleared")
220
+
221
+
222
+ def clear_all_permissions_caches() -> None:
223
+ """Clear all permission caches to force reload from files."""
224
+ clear_tool_permissions_cache()
225
+ clear_resource_permissions_cache()
226
+ clear_prompt_permissions_cache()
227
+ log.info("All permission caches cleared")
228
+
229
+
204
230
  @cache
205
231
  def _classify_tool_permissions_cached(tool_name: str) -> dict[str, Any]:
206
232
  """Classify tool permissions with LRU caching."""
@@ -351,6 +377,10 @@ class DataAccessTracker:
351
377
  """Load prompt permissions from JSON configuration file with caching."""
352
378
  return _load_prompt_permissions_cached()
353
379
 
380
+ def clear_caches(self) -> None:
381
+ """Clear all permission caches to force reload from configuration files."""
382
+ clear_all_permissions_caches()
383
+
354
384
  def _classify_by_tool_name(self, tool_name: str) -> dict[str, Any]:
355
385
  """Classify permissions based on external JSON configuration only."""
356
386
  return _classify_tool_permissions_cached(tool_name)
@@ -606,6 +636,5 @@ class SecurityError(Exception):
606
636
  ████ ████ ████ ████ ████ ████
607
637
  ██ ████ ████ ████ ████ ████ █
608
638
  ████ ████ ████ ████ ████ ████
609
- {message}
610
- """
639
+ """
611
640
  super().__init__(message)
src/server.py CHANGED
@@ -363,7 +363,6 @@ class OpenEdisonProxy:
363
363
  "/mcp/status",
364
364
  self.mcp_status,
365
365
  methods=["GET"],
366
- dependencies=[Depends(self.verify_api_key)],
367
366
  )
368
367
  app.add_api_route(
369
368
  "/mcp/validate",
@@ -377,12 +376,24 @@ class OpenEdisonProxy:
377
376
  methods=["GET"],
378
377
  dependencies=[Depends(self.verify_api_key)],
379
378
  )
379
+ app.add_api_route(
380
+ "/mcp/reinitialize",
381
+ self.reinitialize_mcp_servers,
382
+ methods=["POST"],
383
+ dependencies=[Depends(self.verify_api_key)],
384
+ )
380
385
  # Public sessions endpoint (no auth) for simple local dashboard
381
386
  app.add_api_route(
382
387
  "/sessions",
383
388
  self.get_sessions,
384
389
  methods=["GET"],
385
390
  )
391
+ # Cache invalidation endpoint (no auth required - allowed to fail)
392
+ app.add_api_route(
393
+ "/api/clear-caches",
394
+ self.clear_caches,
395
+ methods=["POST"],
396
+ )
386
397
 
387
398
  async def verify_api_key(
388
399
  self, credentials: HTTPAuthorizationCredentials = _auth_dependency
@@ -446,6 +457,49 @@ class OpenEdisonProxy:
446
457
  detail=f"Failed to get mounted servers: {str(e)}",
447
458
  ) from e
448
459
 
460
+ async def reinitialize_mcp_servers(self) -> dict[str, Any]:
461
+ """Reinitialize all MCP servers by creating a fresh instance and reloading config."""
462
+ old_mcp = None
463
+ try:
464
+ log.info("🔄 Reinitializing MCP servers via API endpoint")
465
+
466
+ # Reload configuration from disk
467
+ log.info("Reloading configuration from disk")
468
+ from src.config import Config
469
+
470
+ fresh_config = Config.load()
471
+ log.info("✅ Configuration reloaded from disk")
472
+
473
+ # Create a completely new SingleUserMCP instance to ensure clean state
474
+ old_mcp = self.single_user_mcp
475
+ self.single_user_mcp = SingleUserMCP()
476
+
477
+ # Initialize the new instance with fresh config
478
+ await self.single_user_mcp.initialize(fresh_config)
479
+
480
+ # Get final status
481
+ final_mounted = await self.single_user_mcp.get_mounted_servers()
482
+
483
+ result = {
484
+ "status": "success",
485
+ "message": "MCP servers reinitialized successfully",
486
+ "final_mounted_servers": [server["name"] for server in final_mounted],
487
+ "total_final_mounted": len(final_mounted),
488
+ }
489
+
490
+ log.info("✅ MCP servers reinitialized successfully via API")
491
+ return result
492
+
493
+ except Exception as e:
494
+ log.error(f"❌ Failed to reinitialize MCP servers: {e}")
495
+ # Restore the old instance on failure
496
+ if old_mcp is not None:
497
+ self.single_user_mcp = old_mcp
498
+ raise HTTPException(
499
+ status_code=500,
500
+ detail=f"Failed to reinitialize MCP servers: {str(e)}",
501
+ ) from e
502
+
449
503
  async def get_sessions(self) -> dict[str, Any]:
450
504
  """Return recent MCP session summaries from local SQLite.
451
505
 
@@ -495,6 +549,21 @@ class OpenEdisonProxy:
495
549
  log.error(f"Failed to fetch sessions: {e}")
496
550
  raise HTTPException(status_code=500, detail="Failed to fetch sessions") from e
497
551
 
552
+ async def clear_caches(self) -> dict[str, str]:
553
+ """Clear all permission caches to force reload from configuration files."""
554
+ try:
555
+ from src.middleware.data_access_tracker import clear_all_permissions_caches
556
+
557
+ log.info("🔄 Clearing all permission caches via API endpoint")
558
+ clear_all_permissions_caches()
559
+ log.info("✅ All permission caches cleared successfully")
560
+
561
+ return {"status": "success", "message": "All permission caches cleared"}
562
+ except Exception as e:
563
+ log.error(f"❌ Failed to clear permission caches: {e}")
564
+ # Don't raise HTTPException - allow to fail gracefully as requested
565
+ return {"status": "error", "message": f"Failed to clear caches: {str(e)}"}
566
+
498
567
  # ---- MCP validation ----
499
568
  class _ValidateRequest(BaseModel):
500
569
  name: str | None = Field(None, description="Optional server name label")
@@ -551,9 +620,9 @@ class OpenEdisonProxy:
551
620
  "args": body.args,
552
621
  "has_roots": bool(body.roots),
553
622
  },
554
- "tools": [self._safe_tool(t) for t in tools],
623
+ "tools": [self._safe_tool(t, prefix=server_name) for t in tools],
555
624
  "resources": [self._safe_resource(r) for r in resources],
556
- "prompts": [self._safe_prompt(p) for p in prompts],
625
+ "prompts": [self._safe_prompt(p, prefix=server_name) for p in prompts],
557
626
  }
558
627
  except TimeoutError as te: # noqa: PERF203
559
628
  log.error(f"MCP validation timed out: {te}\n{traceback.format_exc()}")
@@ -624,10 +693,13 @@ class OpenEdisonProxy:
624
693
  timeout = body.timeout_s if isinstance(body.timeout_s, (int | float)) else 20.0
625
694
  return await asyncio.wait_for(list_all(), timeout=timeout)
626
695
 
627
- def _safe_tool(self, t: Any) -> dict[str, Any]:
696
+ def _safe_tool(self, t: Any, prefix: str) -> dict[str, Any]:
628
697
  name = getattr(t, "name", None)
629
698
  description = getattr(t, "description", None)
630
- return {"name": str(name) if name is not None else "", "description": description}
699
+ return {
700
+ "name": prefix + "_" + str(name) if name is not None else "",
701
+ "description": description,
702
+ }
631
703
 
632
704
  def _safe_resource(self, r: Any) -> dict[str, Any]:
633
705
  uri = getattr(r, "uri", None)
@@ -638,7 +710,10 @@ class OpenEdisonProxy:
638
710
  description = getattr(r, "description", None)
639
711
  return {"uri": uri_str, "description": description}
640
712
 
641
- def _safe_prompt(self, p: Any) -> dict[str, Any]:
713
+ def _safe_prompt(self, p: Any, prefix: str) -> dict[str, Any]:
642
714
  name = getattr(p, "name", None)
643
715
  description = getattr(p, "description", None)
644
- return {"name": str(name) if name is not None else "", "description": description}
716
+ return {
717
+ "name": prefix + "_" + str(name) if name is not None else "",
718
+ "description": description,
719
+ }
src/single_user_mcp.py CHANGED
@@ -7,6 +7,7 @@ Handles MCP protocol communication with running servers using a unified composit
7
7
 
8
8
  from typing import Any, TypedDict
9
9
 
10
+ from fastmcp import Client as FastMCPClient
10
11
  from fastmcp import FastMCP
11
12
  from loguru import logger as log
12
13
 
@@ -107,103 +108,27 @@ class SingleUserMCP(FastMCP[Any]):
107
108
  Returns:
108
109
  True if composite proxy was created successfully, False otherwise
109
110
  """
110
- try:
111
- if not enabled_servers:
112
- log.info("No real servers to mount in composite proxy")
113
- return True
114
-
115
- # Convert to FastMCP config format
116
- fastmcp_config = self._convert_to_fastmcp_config(enabled_servers)
117
-
118
- log.info(
119
- f"Creating composite proxy for servers: {list(fastmcp_config['mcpServers'].keys())}"
120
- )
121
-
122
- # Create the composite proxy using FastMCP's multi-server support
123
- self.composite_proxy = FastMCP.as_proxy(
124
- backend=fastmcp_config, name="open-edison-composite-proxy"
125
- )
126
-
127
- # Import the composite proxy into this main server
128
- # Tools and resources will be automatically namespaced by server name
129
- await self.import_server(self.composite_proxy)
130
-
131
- # Track mounted servers for status reporting
132
- for server_config in enabled_servers:
133
- self.mounted_servers[server_config.name] = MountedServerInfo(
134
- config=server_config, proxy=self.composite_proxy
135
- )
136
-
137
- log.info(f"✅ Created composite proxy with {len(enabled_servers)} servers")
111
+ if not enabled_servers:
112
+ log.info("No real servers to mount in composite proxy")
138
113
  return True
139
114
 
140
- except Exception as e:
141
- log.error(f"❌ Failed to create composite proxy: {e}")
142
- return False
143
-
144
- async def mount_server(self, server_config: MCPServerConfig) -> bool:
145
- """
146
- Mount a single MCP server by rebuilding the composite proxy.
147
-
148
- Args:
149
- server_config: Configuration for the server to mount
150
-
151
- Returns:
152
- True if mounting was successful, False otherwise
153
- """
154
- try:
155
- # Check if server is already mounted
156
- if server_config.name in self.mounted_servers:
157
- log.info(f"Server {server_config.name} is already mounted")
158
- return True
159
-
160
- # Handle test servers separately
161
- if server_config.command == "echo":
162
- return await self._mount_test_server(server_config)
163
-
164
- # For real servers, we need to rebuild the composite proxy
165
- log.info(f"Mounting server {server_config.name} via composite proxy rebuild")
166
-
167
- # Get currently mounted servers and add the new one
168
- current_configs = [mounted["config"] for mounted in self.mounted_servers.values()]
169
-
170
- # Add the new server if not already there
171
- if server_config not in current_configs:
172
- current_configs.append(server_config)
173
-
174
- # Rebuild composite proxy with new server list
175
- return await self.create_composite_proxy(current_configs)
176
-
177
- except Exception as e:
178
- log.error(f"❌ Failed to mount server {server_config.name}: {e}")
179
- return False
180
-
181
- async def unmount_server(self, server_name: str) -> bool:
182
- """
183
- Unmount an MCP server and stop its subprocess.
184
-
185
- NOTE: For servers in the composite proxy, this will require rebuilding
186
- the entire composite proxy without the specified server.
187
- """
188
- try:
189
- # Check if this is a test server (individually mounted)
190
- if server_name in self.mounted_servers:
191
- mounted = self.mounted_servers[server_name]
192
- if mounted["config"].command == "echo":
193
- # Test server - handle individually
194
- await self._cleanup_mounted_server(server_name)
195
- return True
196
-
197
- # Real server in composite proxy - needs full rebuild
198
- log.warning(f"Unmounting {server_name} requires rebuilding composite proxy")
199
- return await self._rebuild_composite_proxy_without(server_name)
200
-
201
- log.warning(f"Server {server_name} not found in mounted servers")
202
- return False
115
+ # Import the composite proxy into this main server
116
+ # Tools and resources will be automatically namespaced by server name
117
+ for server_config in enabled_servers:
118
+ server_name = server_config.name
119
+ # Skip if this server would produce an empty config (e.g., misconfigured)
120
+ fastmcp_config = self._convert_to_fastmcp_config([server_config])
121
+ if not fastmcp_config.get("mcpServers"):
122
+ log.warning(f"Skipping server '{server_name}' due to empty MCP config")
123
+ continue
124
+ proxy = FastMCP.as_proxy(FastMCPClient(fastmcp_config))
125
+ self.mount(proxy, prefix=server_name)
126
+ self.mounted_servers[server_name] = MountedServerInfo(config=server_config, proxy=proxy)
203
127
 
204
- except Exception as e:
205
- log.error(f" Failed to unmount MCP server {server_name}: {e}")
206
- return False
128
+ log.info(
129
+ f" Created composite proxy with {len(enabled_servers)} servers ({self.mounted_servers.keys()})"
130
+ )
131
+ return True
207
132
 
208
133
  async def _rebuild_composite_proxy_without(self, excluded_server: str) -> bool:
209
134
  """Rebuild the composite proxy without the specified server."""
@@ -233,6 +158,7 @@ class SingleUserMCP(FastMCP[Any]):
233
158
 
234
159
  async def _cleanup_mounted_server(self, server_name: str) -> None:
235
160
  """Clean up mounted server resources."""
161
+ # TODO not sure this is possible for the self object? i.e. there is no self.unmount
236
162
  if server_name in self.mounted_servers:
237
163
  del self.mounted_servers[server_name]
238
164
  log.info(f"✅ Unmounted MCP server: {server_name}")
@@ -270,6 +196,80 @@ class SingleUserMCP(FastMCP[Any]):
270
196
 
271
197
  log.info("✅ Single User MCP server initialized with composite proxy")
272
198
 
199
+ async def reinitialize(self, test_config: Any | None = None) -> dict[str, Any]:
200
+ """
201
+ Reinitialize all MCP servers by cleaning up existing ones and reloading config.
202
+
203
+ This method:
204
+ 1. Cleans up all mounted servers and MCP proxies
205
+ 2. Reloads the configuration
206
+ 3. Reinitializes all enabled servers
207
+
208
+ Args:
209
+ test_config: Optional test configuration to use instead of reloading from disk
210
+
211
+ Returns:
212
+ Dictionary with reinitialization status and details
213
+ """
214
+ log.info("🔄 Reinitializing all MCP servers")
215
+
216
+ try:
217
+ # Step 1: Clean up existing mounted servers and proxies
218
+ log.info("Cleaning up existing mounted servers and proxies")
219
+
220
+ # Clean up composite proxy if it exists
221
+ if self.composite_proxy is not None:
222
+ log.info("Cleaning up composite proxy")
223
+ self.composite_proxy = None
224
+
225
+ # Clean up all mounted servers
226
+ mounted_server_names = list(self.mounted_servers.keys())
227
+ for server_name in mounted_server_names:
228
+ await self._cleanup_mounted_server(server_name)
229
+
230
+ # Clear the mounted servers dictionary completely
231
+ self.mounted_servers.clear()
232
+
233
+ log.info(f"✅ Cleaned up {len(mounted_server_names)} mounted servers")
234
+
235
+ # Step 2: Reload configuration if not using test config
236
+ config_to_use = test_config
237
+ if test_config is None:
238
+ log.info("Reloading configuration from disk")
239
+ # Import here to avoid circular imports
240
+ from src.config import Config
241
+
242
+ config_to_use = Config.load()
243
+ log.info("✅ Configuration reloaded from disk")
244
+
245
+ # Step 3: Reinitialize all servers
246
+ log.info("Reinitializing servers with fresh configuration")
247
+ await self.initialize(config_to_use)
248
+
249
+ # Step 4: Get final status
250
+ final_mounted = await self.get_mounted_servers()
251
+
252
+ result = {
253
+ "status": "success",
254
+ "message": "MCP servers reinitialized successfully",
255
+ "cleaned_up_servers": mounted_server_names,
256
+ "final_mounted_servers": [server["name"] for server in final_mounted],
257
+ "total_final_mounted": len(final_mounted),
258
+ }
259
+
260
+ log.info(
261
+ f"✅ Reinitialization complete. Final mounted servers: {result['final_mounted_servers']}"
262
+ )
263
+ return result
264
+
265
+ except Exception as e:
266
+ log.error(f"❌ Failed to reinitialize MCP servers: {e}")
267
+ return {
268
+ "status": "error",
269
+ "message": f"Failed to reinitialize MCP servers: {str(e)}",
270
+ "error": str(e),
271
+ }
272
+
273
273
  def _calculate_risk_level(self, trifecta: dict[str, bool]) -> str:
274
274
  """
275
275
  Calculate a human-readable risk level based on trifecta flags.
@@ -1,14 +0,0 @@
1
- src/__init__.py,sha256=QWeZdjAm2D2B0eWhd8m2-DPpWvIP26KcNJxwEoU1oEQ,254
2
- src/__main__.py,sha256=kQsaVyzRa_ESC57JpKDSQJAHExuXme0rM5beJsYxFeA,161
3
- src/cli.py,sha256=9cJN6mRvjbCcpTyTdUVl47J7OB7bxzSy0h8tfVbHuQU,9982
4
- src/config.py,sha256=2a5rdImQmNGggL690PQprqZVsRUAJcdo8KS2Foj9N-U,9345
5
- src/server.py,sha256=h8sKLoHix27J_hgUXGZiJSJ1qcFSEpcrOmsTSpg0IWw,26544
6
- src/single_user_mcp.py,sha256=Ic8kOyUHN2VgytFyHk1OZ1JufXbGa3Cwm-plC-QQ7eY,14379
7
- src/telemetry.py,sha256=M8iZ7nTPA6BhbPna_xsEoTOOa7A81YyvZ0CkVYa_pPg,12619
8
- src/middleware/data_access_tracker.py,sha256=RZh1RCBYDEbvVIJPkDUz0bfLmK-xYIdV0lGbIxbJYc0,25966
9
- src/middleware/session_tracking.py,sha256=O-n8RvEVCUGAFGYny_gA7-MMQYSlvND-lj3oBZLCT3U,20046
10
- open_edison-0.1.17.dist-info/METADATA,sha256=aPZmsRIcpAizxFdwN6rZ8GfU3KsDlTfIjB3z8T_bFsA,9377
11
- open_edison-0.1.17.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
12
- open_edison-0.1.17.dist-info/entry_points.txt,sha256=qNAkJcnoTXRhj8J--3PDmXz_TQKdB8H_0C9wiCtDIyA,72
13
- open_edison-0.1.17.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
14
- open_edison-0.1.17.dist-info/RECORD,,