open-edison 0.1.15__py3-none-any.whl → 0.1.17__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.15
3
+ Version: 0.1.17
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
@@ -25,12 +25,38 @@ Requires-Dist: pytest>=8.3.3; extra == 'dev'
25
25
  Requires-Dist: ruff>=0.12.3; extra == 'dev'
26
26
  Description-Content-Type: text/markdown
27
27
 
28
- # Open Edison
28
+ # OpenEdison
29
29
 
30
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.
31
31
 
32
+ Just want to run it?
33
+
34
+ ```bash
35
+ # Installs uv (via Astral installer) and launches open-edison with uvx.
36
+ # Note: This does NOT install Node/npx. Install Node if you plan to use npx-based tools like mcp-remote.
37
+ curl -fsSL https://raw.githubusercontent.com/Edison-Watch/open-edison/main/curl_pipe_bash.sh | bash
38
+ ```
39
+
32
40
  Run locally with uvx: `uvx open-edison --config-dir ~/edison-config`
33
41
 
42
+ If you need `npx` (for Node-based MCP tools like `mcp-remote`), install Node.js as well:
43
+
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`
53
+
54
+ After installation, ensure that `npx` is available on PATH.
55
+
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>
59
+
34
60
  ## Features
35
61
 
36
62
  - **Single-user MCP proxy** - No multi-user complexity, just a simple proxy for your MCP servers
@@ -65,6 +91,25 @@ open-edison run --config-dir ~/edison-config
65
91
  OPEN_EDISON_CONFIG_DIR=~/edison-config open-edison run
66
92
  ```
67
93
 
94
+ ### Run with Docker
95
+
96
+ There is a dockerfile for simple local setup.
97
+
98
+ ```bash
99
+ # Single-line:
100
+ git clone https://github.com/GatlingX/open-edison.git && cd open-edison && make docker_run
101
+
102
+ # Or
103
+ # Clone repo
104
+ git clone https://github.com/GatlingX/open-edison.git
105
+ # Enter repo
106
+ cd open-edison
107
+ # Build and run
108
+ make docker_run
109
+ ```
110
+
111
+ The MCP server will be available at `http://localhost:3000` and the api + frontend at `http://localhost:3001`.
112
+
68
113
  ### Run from source
69
114
 
70
115
  1. Clone the repository:
@@ -74,33 +119,26 @@ git clone https://github.com/GatlingX/open-edison.git
74
119
  cd open-edison
75
120
  ```
76
121
 
77
- 2. Set up the project:
122
+ 1. Set up the project:
78
123
 
79
124
  ```bash
80
125
  make setup
81
126
  ```
82
127
 
83
- 3. Edit `config.json` to configure your MCP servers:
128
+ 1. Edit `config.json` to configure your MCP servers. See the full file: [config.json](config.json), it looks like:
84
129
 
85
130
  ```json
86
131
  {
87
- "server": {
88
- "host": "localhost",
89
- "port": 3000,
90
- "api_key": "your-secure-api-key"
91
- },
132
+ "server": { "host": "0.0.0.0", "port": 3000, "api_key": "..." },
133
+ "logging": { "level": "INFO", "database_path": "sessions.db" },
92
134
  "mcp_servers": [
93
- {
94
- "name": "filesystem",
95
- "command": "uvx",
96
- "args": ["mcp-server-filesystem", "/path/to/directory"],
97
- "enabled": true
98
- }
135
+ { "name": "filesystem", "command": "uvx", "args": ["mcp-server-filesystem", "/tmp"], "enabled": true },
136
+ { "name": "github", "enabled": false, "env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "..." } }
99
137
  ]
100
138
  }
101
139
  ```
102
140
 
103
- 4. Run the server:
141
+ 1. Run the server:
104
142
 
105
143
  ```bash
106
144
  make run
@@ -110,18 +148,9 @@ open-edison run
110
148
 
111
149
  The server will be available at `http://localhost:3000`.
112
150
 
113
- ### Run with Docker
114
-
115
- ```bash
116
- # After cloning the repo
117
- make docker_run
118
- ```
119
-
120
- The MCP server will be available at `http://localhost:3000` and the api + frontend at `http://localhost:3001`.
121
-
122
151
  ## MCP Connection
123
152
 
124
- Connect any MCP client to Open Edison:
153
+ Connect any MCP client to Open Edison (requires Node.js/npm for `npx`):
125
154
 
126
155
  ```bash
127
156
  npx -y mcp-remote http://localhost:3000/mcp/ --http-only --header "Authorization: Bearer your-api-key"
@@ -144,64 +173,28 @@ Or add to your MCP client config:
144
173
 
145
174
  ### API Endpoints
146
175
 
147
- Api is on port 3001 (or configured MCP server port + 1).
148
-
149
- - `GET /health` - Health check
150
- - `GET /mcp/status` - Get status of configured MCP servers
151
- - `POST /mcp/{server_name}/start` - Start a specific MCP server
152
- - `POST /mcp/{server_name}/stop` - Stop a specific MCP server
153
- - `POST /mcp/call` - Proxy MCP calls to running servers
154
- - `GET /sessions` - Get session logs (coming soon)
155
-
156
- All endpoints except `/health` require the `Authorization: Bearer <api_key>` header.
176
+ See [API Reference](docs/quick-reference/api_reference.md) for full API documentation.
157
177
 
158
178
  ## Development
159
179
 
160
- ```bash
161
- # Install dependencies
162
- make sync
180
+ ### Setup
163
181
 
164
- # Run with auto-reload
165
- make dev
182
+ Setup from source as above.
166
183
 
167
- # Run tests
168
- make test
184
+ ### Run
169
185
 
170
- # Lint code
171
- make lint
172
-
173
- # Format code
174
- make format
175
- ```
176
-
177
- ### Website (Sessions Dashboard)
178
-
179
- A minimal React + Vite frontend is included at `open-edison/frontend/`.
180
-
181
- Run it with a single command from the repo root or via the CLI:
186
+ Server doesn't have any auto-reload at the moment, so you'll need to run & ctrl-c this during development.
182
187
 
183
188
  ```bash
184
- make website
185
- # or
186
- open-edison website
189
+ make run
187
190
  ```
188
191
 
189
- This will install frontend deps (first run) and start the dev server. Open the URL shown (typically `http://localhost:5173` or `5174`).
192
+ ### Tests/code quality
190
193
 
191
- Notes:
192
-
193
- - The dashboard reads session data directly from the SQLite database `edison.db` in the repo root via sql.js.
194
- - The Configs tab provides JSON editors (with syntax highlighting) for `config.json`, `tool_permissions.json`, `resource_permissions.json`, and `prompt_permissions.json`.
195
- - You can Save changes directly while the dev server is running; writes are constrained to the project root.
196
-
197
- ## Docker
194
+ We expect `make ci` to return cleanly.
198
195
 
199
196
  ```bash
200
- # Build Docker image
201
- make docker_build
202
-
203
- # Run in Docker
204
- make docker_run
197
+ make ci
205
198
  ```
206
199
 
207
200
  ## Configuration
@@ -230,80 +223,48 @@ Open Edison includes a comprehensive security monitoring system that tracks the
230
223
  2. **Untrusted content exposure** - Exposure to external/web content
231
224
  3. **External communication** - Ability to write/send data externally
232
225
 
233
- The system monitors these risks across **tools**, **resources**, and **prompts** using separate configuration files.
226
+ The configuration allows you to classify these risks across **tools**, **resources**, and **prompts** using separate configuration files.
227
+
228
+ In addition to trifecta, we track Access Control Level (ACL) for each tool call,
229
+ that is, each tool has an ACL level (one of PUBLIC, PRIVATE, or SECRET), and we track the highest ACL level for each session.
230
+ If a write operation is attempted to a lower ACL level, it is blocked.
234
231
 
235
232
  ### Tool Permissions (`tool_permissions.json`)
236
233
 
237
- Defines security classifications for MCP tools. Each tool is classified with three boolean flags:
234
+ Defines security classifications for MCP tools. See full file: [tool_permissions.json](tool_permissions.json), it looks like:
238
235
 
239
236
  ```json
240
237
  {
241
- "filesystem_read_file": {
242
- "write_operation": false,
243
- "read_private_data": true,
244
- "read_untrusted_public_data": false
238
+ "_metadata": { "last_updated": "2025-08-07" },
239
+ "builtin": {
240
+ "get_security_status": { "enabled": true, "write_operation": false, "read_private_data": false, "read_untrusted_public_data": false, "acl": "PUBLIC" }
245
241
  },
246
- "sqlite_create_record": {
247
- "write_operation": true,
248
- "read_private_data": true,
249
- "read_untrusted_public_data": false
242
+ "filesystem": {
243
+ "read_file": { "enabled": true, "write_operation": false, "read_private_data": true, "read_untrusted_public_data": false, "acl": "PRIVATE" },
244
+ "write_file": { "enabled": true, "write_operation": true, "read_private_data": true, "read_untrusted_public_data": false, "acl": "PRIVATE" }
250
245
  }
251
246
  }
252
247
  ```
253
248
 
254
249
  ### Resource Permissions (`resource_permissions.json`)
255
250
 
256
- Defines security classifications for resource access patterns. Currently empty - add classifications as needed:
251
+ Defines security classifications for resource access patterns. See full file: [resource_permissions.json](resource_permissions.json), it looks like:
257
252
 
258
253
  ```json
259
254
  {
260
- "_metadata": {
261
- "description": "Resource security classifications for Open Edison data access tracker",
262
- "last_updated": "2025-08-07"
263
- },
264
- "file:*": {
265
- "write_operation": false,
266
- "read_private_data": true,
267
- "read_untrusted_public_data": false
268
- },
269
- "http:*": {
270
- "write_operation": false,
271
- "read_private_data": false,
272
- "read_untrusted_public_data": true
273
- },
274
- "database:*": {
275
- "write_operation": false,
276
- "read_private_data": true,
277
- "read_untrusted_public_data": false
278
- }
255
+ "_metadata": { "last_updated": "2025-08-07" },
256
+ "builtin": { "config://app": { "enabled": true, "write_operation": false, "read_private_data": false, "read_untrusted_public_data": false } }
279
257
  }
280
258
  ```
281
259
 
282
260
  ### Prompt Permissions (`prompt_permissions.json`)
283
261
 
284
- Defines security classifications for prompt types. Currently empty - add classifications as needed:
262
+ Defines security classifications for prompt types. See full file: [prompt_permissions.json](prompt_permissions.json), it looks like:
285
263
 
286
264
  ```json
287
265
  {
288
- "_metadata": {
289
- "description": "Prompt security classifications for Open Edison data access tracker",
290
- "last_updated": "2025-08-07"
291
- },
292
- "system": {
293
- "write_operation": false,
294
- "read_private_data": false,
295
- "read_untrusted_public_data": false
296
- },
297
- "external_prompt": {
298
- "write_operation": false,
299
- "read_private_data": false,
300
- "read_untrusted_public_data": true
301
- },
302
- "prompt:file:*": {
303
- "write_operation": false,
304
- "read_private_data": true,
305
- "read_untrusted_public_data": false
306
- }
266
+ "_metadata": { "last_updated": "2025-08-07" },
267
+ "builtin": { "summarize_text": { "enabled": true, "write_operation": false, "read_private_data": false, "read_untrusted_public_data": false } }
307
268
  }
308
269
  ```
309
270
 
@@ -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=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,,
@@ -102,7 +102,7 @@ def create_db_session() -> Generator[Session, None, None]:
102
102
 
103
103
  # Ensure changes are flushed to the main database file (avoid WAL for sql.js compatibility)
104
104
  @event.listens_for(engine, "connect")
105
- def _set_sqlite_pragmas(dbapi_connection, connection_record): # type: ignore[no-untyped-def]
105
+ def _set_sqlite_pragmas(dbapi_connection, connection_record): # type: ignore[no-untyped-def] # noqa
106
106
  cur = dbapi_connection.cursor() # type: ignore[attr-defined]
107
107
  try:
108
108
  cur.execute("PRAGMA journal_mode=DELETE") # type: ignore[attr-defined]
@@ -296,7 +296,7 @@ class SessionTrackingMiddleware(Middleware):
296
296
 
297
297
  assert session.data_access_tracker is not None
298
298
  log.debug(f"🔍 Analyzing tool {context.message.name} for security implications")
299
- _ = session.data_access_tracker.add_tool_call(context.message.name)
299
+ session.data_access_tracker.add_tool_call(context.message.name)
300
300
  # Telemetry: record tool call
301
301
  record_tool_call(context.message.name)
302
302
 
src/server.py CHANGED
@@ -6,6 +6,7 @@ No multi-user support, no complex routing - just a straightforward proxy.
6
6
  """
7
7
 
8
8
  import asyncio
9
+ import json
9
10
  import traceback
10
11
  from collections.abc import Awaitable, Callable, Coroutine
11
12
  from pathlib import Path
@@ -23,7 +24,6 @@ from pydantic import BaseModel, Field
23
24
 
24
25
  from src.config import MCPServerConfig, config
25
26
  from src.config import get_config_dir as _get_cfg_dir # type: ignore[attr-defined]
26
- from src.mcp_manager import MCPManager
27
27
  from src.middleware.session_tracking import (
28
28
  MCPSessionModel,
29
29
  create_db_session,
@@ -57,8 +57,7 @@ class OpenEdisonProxy:
57
57
  self.port: int = port
58
58
 
59
59
  # Initialize components
60
- self.mcp_manager: MCPManager = MCPManager()
61
- self.single_user_mcp: SingleUserMCP = SingleUserMCP(self.mcp_manager)
60
+ self.single_user_mcp: SingleUserMCP = SingleUserMCP()
62
61
 
63
62
  # Initialize FastAPI app for management
64
63
  self.fastapi_app: FastAPI = self._create_fastapi_app()
@@ -184,30 +183,33 @@ class OpenEdisonProxy:
184
183
  """
185
184
  Resolve a JSON config file path consistently with src.config defaults.
186
185
 
187
- Precedence for reads (and writes if chosen):
188
- 1) Repository root next to src/ (editable/dev) if file exists
189
- 2) Config dir (OPEN_EDISON_CONFIG_DIR or platform default)
190
- - If missing, bootstrap from repo root default when available
191
- 3) Current working directory as last resort
186
+ Precedence for reads and writes:
187
+ 1) Config dir (OPEN_EDISON_CONFIG_DIR or platform default) if file exists
188
+ 2) Repository/package defaults next to src/ — and bootstrap a copy into the config dir if missing
189
+ 3) Config dir target path (even if not yet created) as last resort
192
190
  """
193
- # 1) Prefer repository root next to src/
194
- repo_candidate = Path(__file__).parent.parent / filename
195
- if repo_candidate.exists():
196
- return repo_candidate
197
-
198
- # 2) Config directory
191
+ # 1) Config directory (preferred)
199
192
  try:
200
193
  base = _get_cfg_dir()
201
194
  except Exception:
202
195
  base = Path.cwd()
203
196
  target = base / filename
204
- if (not target.exists()) and repo_candidate.exists():
197
+ if target.exists():
198
+ return target
199
+
200
+ # 2) Repository/package defaults next to src/
201
+ repo_candidate = Path(__file__).parent.parent / filename
202
+ if repo_candidate.exists():
203
+ # Bootstrap a copy into config dir when possible
205
204
  try:
206
205
  target.parent.mkdir(parents=True, exist_ok=True)
207
206
  target.write_text(repo_candidate.read_text(encoding="utf-8"), encoding="utf-8")
208
207
  except Exception:
209
208
  pass
210
- return target if target.exists() else repo_candidate
209
+ return target if target.exists() else repo_candidate
210
+
211
+ # 3) Fall back to config dir path (will be created on save)
212
+ return target
211
213
 
212
214
  async def _serve_json(filename: str) -> Response: # type: ignore[override]
213
215
  if filename not in allowed_json_files:
@@ -238,23 +240,28 @@ class OpenEdisonProxy:
238
240
  content = body.get("content", "")
239
241
  if not isinstance(content, str):
240
242
  raise ValueError("content must be string")
243
+ source: str = "unknown"
241
244
  if isinstance(name, str) and name in allowed_json_files:
242
245
  target = _resolve_json_path(name)
246
+ source = f"name={name}"
243
247
  elif isinstance(path_val, str):
244
- base = Path.cwd()
245
- # Normalize path but restrict to allowed filenames
248
+ # Normalize path but restrict to allowed filenames, then resolve like reads
246
249
  candidate = Path(path_val)
247
250
  filename = candidate.name
248
251
  if filename not in allowed_json_files:
249
252
  raise ValueError("filename not allowed")
250
- target = base / filename
253
+ target = _resolve_json_path(filename)
254
+ source = f"path={path_val} -> filename={filename}"
251
255
  else:
252
256
  raise ValueError("invalid target file")
253
- # Basic validation to ensure valid JSON
254
- import json as _json
255
257
 
256
- _ = _json.loads(content or "{}")
258
+ log.debug(
259
+ f"Saving JSON config ({source}), resolved target: {target} (bytes={len(content.encode('utf-8'))})"
260
+ )
261
+
262
+ _ = json.loads(content or "{}")
257
263
  target.write_text(content or "{}", encoding="utf-8")
264
+ log.debug(f"Saved JSON config to {target}")
258
265
  return {"status": "ok"}
259
266
  except Exception as e: # noqa: BLE001
260
267
  raise HTTPException(status_code=400, detail=f"Save failed: {e}") from e
@@ -348,12 +355,6 @@ class OpenEdisonProxy:
348
355
  log.info("🚀 Starting both FastAPI and FastMCP servers...")
349
356
  _ = await asyncio.gather(*servers_to_run)
350
357
 
351
- async def shutdown(self) -> None:
352
- """Shutdown the proxy server and all MCP servers"""
353
- log.info("🛑 Shutting down Open Edison proxy server")
354
- await self.mcp_manager.shutdown()
355
- log.info("✅ Open Edison proxy server shutdown complete")
356
-
357
358
  def _register_routes(self, app: FastAPI) -> None:
358
359
  """Register all routes for the FastAPI app"""
359
360
  # Register routes with their decorators
@@ -364,48 +365,18 @@ class OpenEdisonProxy:
364
365
  methods=["GET"],
365
366
  dependencies=[Depends(self.verify_api_key)],
366
367
  )
367
- app.add_api_route(
368
- "/mcp/{server_name}/start",
369
- self.start_mcp_server,
370
- methods=["POST"],
371
- dependencies=[Depends(self.verify_api_key)],
372
- )
373
368
  app.add_api_route(
374
369
  "/mcp/validate",
375
370
  self.validate_mcp_server,
376
371
  methods=["POST"],
377
372
  # Intentionally no auth required for validation for now
378
373
  )
379
- app.add_api_route(
380
- "/mcp/{server_name}/stop",
381
- self.stop_mcp_server,
382
- methods=["POST"],
383
- dependencies=[Depends(self.verify_api_key)],
384
- )
385
- app.add_api_route(
386
- "/mcp/call",
387
- self.proxy_mcp_call,
388
- methods=["POST"],
389
- dependencies=[Depends(self.verify_api_key)],
390
- )
391
374
  app.add_api_route(
392
375
  "/mcp/mounted",
393
376
  self.get_mounted_servers,
394
377
  methods=["GET"],
395
378
  dependencies=[Depends(self.verify_api_key)],
396
379
  )
397
- app.add_api_route(
398
- "/mcp/{server_name}/mount",
399
- self.mount_server,
400
- methods=["POST"],
401
- dependencies=[Depends(self.verify_api_key)],
402
- )
403
- app.add_api_route(
404
- "/mcp/{server_name}/unmount",
405
- self.unmount_server,
406
- methods=["POST"],
407
- dependencies=[Depends(self.verify_api_key)],
408
- )
409
380
  # Public sessions endpoint (no auth) for simple local dashboard
410
381
  app.add_api_route(
411
382
  "/sessions",
@@ -426,6 +397,18 @@ class OpenEdisonProxy:
426
397
  raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid API key")
427
398
  return credentials.credentials
428
399
 
400
+ async def mcp_status(self) -> dict[str, list[dict[str, Any]]]:
401
+ """Get status of configured MCP servers (auth required)."""
402
+ return {
403
+ "servers": [
404
+ {
405
+ "name": server.name,
406
+ "enabled": server.enabled,
407
+ }
408
+ for server in config.mcp_servers
409
+ ]
410
+ }
411
+
429
412
  def _handle_server_operation_error(
430
413
  self, operation: str, server_name: str, error: Exception
431
414
  ) -> HTTPException:
@@ -451,63 +434,6 @@ class OpenEdisonProxy:
451
434
  """Health check endpoint"""
452
435
  return {"status": "healthy", "version": "0.1.0", "mcp_servers": len(config.mcp_servers)}
453
436
 
454
- async def mcp_status(self) -> dict[str, list[dict[str, str | bool]]]:
455
- """Get status of configured MCP servers"""
456
- return {
457
- "servers": [
458
- {
459
- "name": server.name,
460
- "enabled": server.enabled,
461
- "running": await self.mcp_manager.is_server_running(server.name),
462
- }
463
- for server in config.mcp_servers
464
- ]
465
- }
466
-
467
- async def start_mcp_server(self, server_name: str) -> dict[str, str]:
468
- """Start a specific MCP server"""
469
- try:
470
- _ = await self.mcp_manager.start_server(server_name)
471
- return {"message": f"Server {server_name} started successfully"}
472
- except Exception as e:
473
- raise self._handle_server_operation_error("start", server_name, e) from e
474
-
475
- async def stop_mcp_server(self, server_name: str) -> dict[str, str]:
476
- """Stop a specific MCP server"""
477
- try:
478
- await self.mcp_manager.stop_server(server_name)
479
- return {"message": f"Server {server_name} stopped successfully"}
480
- except Exception as e:
481
- raise self._handle_server_operation_error("stop", server_name, e) from e
482
-
483
- async def proxy_mcp_call(self, request: dict[str, Any]) -> dict[str, Any]:
484
- """
485
- Proxy MCP calls to mounted servers.
486
-
487
- This now routes requests through the mounted FastMCP servers.
488
- """
489
- try:
490
- log.info(f"Proxying MCP request: {request.get('method', 'unknown')}")
491
-
492
- mounted = await self.single_user_mcp.get_mounted_servers()
493
- mounted_names = [server["name"] for server in mounted]
494
-
495
- return {
496
- "jsonrpc": "2.0",
497
- "id": request.get("id"),
498
- "result": {
499
- "message": "MCP request routed through FastMCP",
500
- "request": request,
501
- "mounted_servers": mounted_names,
502
- },
503
- }
504
- except Exception as e:
505
- log.error(f"Failed to proxy MCP call: {e}")
506
- raise HTTPException(
507
- status_code=500,
508
- detail=f"Failed to proxy MCP call: {str(e)}",
509
- ) from e
510
-
511
437
  async def get_mounted_servers(self) -> dict[str, Any]:
512
438
  """Get list of currently mounted MCP servers."""
513
439
  try:
@@ -520,36 +446,6 @@ class OpenEdisonProxy:
520
446
  detail=f"Failed to get mounted servers: {str(e)}",
521
447
  ) from e
522
448
 
523
- async def mount_server(self, server_name: str) -> dict[str, str]:
524
- """Mount a specific MCP server."""
525
- try:
526
- server_config = self._find_server_config(server_name)
527
- success = await self.single_user_mcp.mount_server(server_config)
528
- if success:
529
- return {"message": f"Server {server_name} mounted successfully"}
530
- raise HTTPException(
531
- status_code=500,
532
- detail=f"Failed to mount server: {server_name}",
533
- )
534
- except HTTPException:
535
- raise
536
- except Exception as e:
537
- raise self._handle_server_operation_error("mount", server_name, e) from e
538
-
539
- async def unmount_server(self, server_name: str) -> dict[str, str]:
540
- """Unmount a specific MCP server."""
541
- try:
542
- if server_name == "test-echo":
543
- log.info("Special handling for test-echo server unmount")
544
- _ = await self.single_user_mcp.unmount_server(server_name)
545
- return {"message": f"Server {server_name} unmounted successfully"}
546
- _ = await self.single_user_mcp.unmount_server(server_name)
547
- return {"message": f"Server {server_name} unmounted successfully"}
548
- except HTTPException:
549
- raise
550
- except Exception as e:
551
- raise self._handle_server_operation_error("unmount", server_name, e) from e
552
-
553
449
  async def get_sessions(self) -> dict[str, Any]:
554
450
  """Return recent MCP session summaries from local SQLite.
555
451