open-edison 0.1.16__tar.gz → 0.1.17__tar.gz
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.16 → open_edison-0.1.17}/PKG-INFO +19 -3
- {open_edison-0.1.16 → open_edison-0.1.17}/README.md +18 -2
- {open_edison-0.1.16 → open_edison-0.1.17}/config.json +1 -1
- {open_edison-0.1.16 → open_edison-0.1.17}/pyproject.toml +6 -4
- {open_edison-0.1.16 → open_edison-0.1.17}/src/middleware/session_tracking.py +1 -1
- {open_edison-0.1.16 → open_edison-0.1.17}/src/server.py +41 -145
- {open_edison-0.1.16 → open_edison-0.1.17}/src/single_user_mcp.py +5 -22
- {open_edison-0.1.16 → open_edison-0.1.17}/src/telemetry.py +17 -1
- open_edison-0.1.16/src/frontend_dist/assets/index-_NTxjOfh.js +0 -51
- open_edison-0.1.16/src/frontend_dist/assets/index-h6k8aL6h.css +0 -1
- open_edison-0.1.16/src/frontend_dist/index.html +0 -21
- open_edison-0.1.16/src/mcp_manager.py +0 -137
- {open_edison-0.1.16 → open_edison-0.1.17}/.gitignore +0 -0
- {open_edison-0.1.16 → open_edison-0.1.17}/LICENSE +0 -0
- {open_edison-0.1.16 → open_edison-0.1.17}/desktop_ext/README.md +0 -0
- {open_edison-0.1.16 → open_edison-0.1.17}/docs/README.md +0 -0
- {open_edison-0.1.16 → open_edison-0.1.17}/docs/architecture/single_user_design.md +0 -0
- {open_edison-0.1.16 → open_edison-0.1.17}/docs/core/configuration.md +0 -0
- {open_edison-0.1.16 → open_edison-0.1.17}/docs/core/project_structure.md +0 -0
- {open_edison-0.1.16 → open_edison-0.1.17}/docs/core/proxy_usage.md +0 -0
- {open_edison-0.1.16 → open_edison-0.1.17}/docs/deployment/docker.md +0 -0
- {open_edison-0.1.16 → open_edison-0.1.17}/docs/deployment/local.md +0 -0
- {open_edison-0.1.16 → open_edison-0.1.17}/docs/development/contributing.md +0 -0
- {open_edison-0.1.16 → open_edison-0.1.17}/docs/development/development_guide.md +0 -0
- {open_edison-0.1.16 → open_edison-0.1.17}/docs/development/testing.md +0 -0
- {open_edison-0.1.16 → open_edison-0.1.17}/docs/quick-reference/api_reference.md +0 -0
- {open_edison-0.1.16 → open_edison-0.1.17}/docs/quick-reference/config_quick_start.md +0 -0
- {open_edison-0.1.16 → open_edison-0.1.17}/frontend/configurations/prompt_permissions.json +0 -0
- {open_edison-0.1.16 → open_edison-0.1.17}/frontend/configurations/resource_permissions.json +0 -0
- {open_edison-0.1.16 → open_edison-0.1.17}/frontend/configurations/tool_permissions.json +0 -0
- {open_edison-0.1.16 → open_edison-0.1.17}/prompt_permissions.json +0 -0
- {open_edison-0.1.16 → open_edison-0.1.17}/resource_permissions.json +0 -0
- {open_edison-0.1.16 → open_edison-0.1.17}/src/__init__.py +0 -0
- {open_edison-0.1.16 → open_edison-0.1.17}/src/__main__.py +0 -0
- {open_edison-0.1.16 → open_edison-0.1.17}/src/cli.py +0 -0
- {open_edison-0.1.16 → open_edison-0.1.17}/src/config.py +0 -0
- {open_edison-0.1.16 → open_edison-0.1.17}/src/middleware/data_access_tracker.py +0 -0
- {open_edison-0.1.16 → open_edison-0.1.17}/tool_permissions.json +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: open-edison
|
3
|
-
Version: 0.1.
|
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
|
@@ -32,11 +32,27 @@ Open-source MCP security gateway that prevents data exfiltration—via direct ac
|
|
32
32
|
Just want to run it?
|
33
33
|
|
34
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.
|
35
37
|
curl -fsSL https://raw.githubusercontent.com/Edison-Watch/open-edison/main/curl_pipe_bash.sh | bash
|
36
38
|
```
|
37
39
|
|
38
40
|
Run locally with uvx: `uvx open-edison --config-dir ~/edison-config`
|
39
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
|
+
|
40
56
|
<div align="center">
|
41
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>
|
42
58
|
</div>
|
@@ -116,7 +132,7 @@ make setup
|
|
116
132
|
"server": { "host": "0.0.0.0", "port": 3000, "api_key": "..." },
|
117
133
|
"logging": { "level": "INFO", "database_path": "sessions.db" },
|
118
134
|
"mcp_servers": [
|
119
|
-
{ "name": "filesystem", "command": "
|
135
|
+
{ "name": "filesystem", "command": "uvx", "args": ["mcp-server-filesystem", "/tmp"], "enabled": true },
|
120
136
|
{ "name": "github", "enabled": false, "env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "..." } }
|
121
137
|
]
|
122
138
|
}
|
@@ -134,7 +150,7 @@ The server will be available at `http://localhost:3000`.
|
|
134
150
|
|
135
151
|
## MCP Connection
|
136
152
|
|
137
|
-
Connect any MCP client to Open Edison:
|
153
|
+
Connect any MCP client to Open Edison (requires Node.js/npm for `npx`):
|
138
154
|
|
139
155
|
```bash
|
140
156
|
npx -y mcp-remote http://localhost:3000/mcp/ --http-only --header "Authorization: Bearer your-api-key"
|
@@ -5,11 +5,27 @@ Open-source MCP security gateway that prevents data exfiltration—via direct ac
|
|
5
5
|
Just want to run it?
|
6
6
|
|
7
7
|
```bash
|
8
|
+
# Installs uv (via Astral installer) and launches open-edison with uvx.
|
9
|
+
# Note: This does NOT install Node/npx. Install Node if you plan to use npx-based tools like mcp-remote.
|
8
10
|
curl -fsSL https://raw.githubusercontent.com/Edison-Watch/open-edison/main/curl_pipe_bash.sh | bash
|
9
11
|
```
|
10
12
|
|
11
13
|
Run locally with uvx: `uvx open-edison --config-dir ~/edison-config`
|
12
14
|
|
15
|
+
If you need `npx` (for Node-based MCP tools like `mcp-remote`), install Node.js as well:
|
16
|
+
|
17
|
+
- macOS:
|
18
|
+
- uv: `curl -fsSL https://astral.sh/uv/install.sh | sh`
|
19
|
+
- Node/npx: `brew install node`
|
20
|
+
- Linux (Debian/Ubuntu):
|
21
|
+
- uv: `curl -fsSL https://astral.sh/uv/install.sh | sh`
|
22
|
+
- Node/npx: `sudo apt-get update && sudo apt-get install -y nodejs npm`
|
23
|
+
- Windows (PowerShell):
|
24
|
+
- uv: `powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"`
|
25
|
+
- Node/npx: `winget install -e --id OpenJS.NodeJS`
|
26
|
+
|
27
|
+
After installation, ensure that `npx` is available on PATH.
|
28
|
+
|
13
29
|
<div align="center">
|
14
30
|
<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>
|
15
31
|
</div>
|
@@ -89,7 +105,7 @@ make setup
|
|
89
105
|
"server": { "host": "0.0.0.0", "port": 3000, "api_key": "..." },
|
90
106
|
"logging": { "level": "INFO", "database_path": "sessions.db" },
|
91
107
|
"mcp_servers": [
|
92
|
-
{ "name": "filesystem", "command": "
|
108
|
+
{ "name": "filesystem", "command": "uvx", "args": ["mcp-server-filesystem", "/tmp"], "enabled": true },
|
93
109
|
{ "name": "github", "enabled": false, "env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "..." } }
|
94
110
|
]
|
95
111
|
}
|
@@ -107,7 +123,7 @@ The server will be available at `http://localhost:3000`.
|
|
107
123
|
|
108
124
|
## MCP Connection
|
109
125
|
|
110
|
-
Connect any MCP client to Open Edison:
|
126
|
+
Connect any MCP client to Open Edison (requires Node.js/npm for `npx`):
|
111
127
|
|
112
128
|
```bash
|
113
129
|
npx -y mcp-remote http://localhost:3000/mcp/ --http-only --header "Authorization: Bearer your-api-key"
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[project]
|
2
2
|
name = "open-edison"
|
3
|
-
version = "0.1.
|
3
|
+
version = "0.1.17"
|
4
4
|
description = "Open-source MCP security, aggregation, and monitoring. Single-user, self-hosted MCP proxy."
|
5
5
|
readme = "README.md"
|
6
6
|
authors = [
|
@@ -39,6 +39,7 @@ dev-dependencies = [
|
|
39
39
|
"ruff>=0.12.3",
|
40
40
|
"pytest>=8.3.3",
|
41
41
|
"pytest-asyncio>=1.0.0",
|
42
|
+
"vulture>=2.11",
|
42
43
|
"twine>=5.1.1",
|
43
44
|
]
|
44
45
|
|
@@ -76,11 +77,9 @@ include = [
|
|
76
77
|
"prompt_permissions.json",
|
77
78
|
"src/**",
|
78
79
|
"docs/**",
|
79
|
-
|
80
|
-
exclude = [
|
80
|
+
# Ensure packaged dashboard assets are present when building from sdist
|
81
81
|
"src/frontend_dist/**",
|
82
82
|
]
|
83
|
-
force-include = { "src/frontend_dist" = "src/frontend_dist" }
|
84
83
|
|
85
84
|
[tool.ruff]
|
86
85
|
line-length = 100
|
@@ -112,3 +111,6 @@ reportMissingTypeStubs = true
|
|
112
111
|
reportUnusedFunction = false # Disable unused function warnings since we have many dynamically registered functions
|
113
112
|
venvPath = ".venv"
|
114
113
|
extraPaths = ["src"]
|
114
|
+
|
115
|
+
[tool.vulture]
|
116
|
+
exclude = ["tests", "src/frontend_dist"]
|
@@ -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]
|
@@ -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.
|
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
|
188
|
-
1)
|
189
|
-
2)
|
190
|
-
|
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)
|
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
|
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
|
-
|
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
|
-
|
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 =
|
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
|
-
|
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
|
|
@@ -11,7 +11,6 @@ from fastmcp import FastMCP
|
|
11
11
|
from loguru import logger as log
|
12
12
|
|
13
13
|
from src.config import MCPServerConfig, config
|
14
|
-
from src.mcp_manager import MCPManager
|
15
14
|
from src.middleware.session_tracking import (
|
16
15
|
SessionTrackingMiddleware,
|
17
16
|
get_current_session_data_tracker,
|
@@ -42,9 +41,8 @@ class SingleUserMCP(FastMCP[Any]):
|
|
42
41
|
All enabled MCP servers are mounted through a single FastMCP composite proxy.
|
43
42
|
"""
|
44
43
|
|
45
|
-
def __init__(self
|
44
|
+
def __init__(self):
|
46
45
|
super().__init__(name="open-edison-single-user")
|
47
|
-
self.mcp_manager: MCPManager = mcp_manager
|
48
46
|
self.mounted_servers: dict[str, MountedServerInfo] = {}
|
49
47
|
self.composite_proxy: FastMCP[Any] | None = None
|
50
48
|
|
@@ -110,21 +108,12 @@ class SingleUserMCP(FastMCP[Any]):
|
|
110
108
|
True if composite proxy was created successfully, False otherwise
|
111
109
|
"""
|
112
110
|
try:
|
113
|
-
|
114
|
-
real_servers = [s for s in enabled_servers if s.command != "echo"]
|
115
|
-
|
116
|
-
if not real_servers:
|
111
|
+
if not enabled_servers:
|
117
112
|
log.info("No real servers to mount in composite proxy")
|
118
113
|
return True
|
119
114
|
|
120
|
-
# Start all required server processes
|
121
|
-
for server_config in real_servers:
|
122
|
-
if not await self.mcp_manager.is_server_running(server_config.name):
|
123
|
-
log.info(f"Starting server process: {server_config.name}")
|
124
|
-
_ = await self.mcp_manager.start_server(server_config.name)
|
125
|
-
|
126
115
|
# Convert to FastMCP config format
|
127
|
-
fastmcp_config = self._convert_to_fastmcp_config(
|
116
|
+
fastmcp_config = self._convert_to_fastmcp_config(enabled_servers)
|
128
117
|
|
129
118
|
log.info(
|
130
119
|
f"Creating composite proxy for servers: {list(fastmcp_config['mcpServers'].keys())}"
|
@@ -140,12 +129,12 @@ class SingleUserMCP(FastMCP[Any]):
|
|
140
129
|
await self.import_server(self.composite_proxy)
|
141
130
|
|
142
131
|
# Track mounted servers for status reporting
|
143
|
-
for server_config in
|
132
|
+
for server_config in enabled_servers:
|
144
133
|
self.mounted_servers[server_config.name] = MountedServerInfo(
|
145
134
|
config=server_config, proxy=self.composite_proxy
|
146
135
|
)
|
147
136
|
|
148
|
-
log.info(f"✅ Created composite proxy with {len(
|
137
|
+
log.info(f"✅ Created composite proxy with {len(enabled_servers)} servers")
|
149
138
|
return True
|
150
139
|
|
151
140
|
except Exception as e:
|
@@ -221,7 +210,6 @@ class SingleUserMCP(FastMCP[Any]):
|
|
221
210
|
try:
|
222
211
|
# Remove from mounted servers
|
223
212
|
await self._cleanup_mounted_server(excluded_server)
|
224
|
-
await self._stop_server_process(excluded_server)
|
225
213
|
|
226
214
|
# Get remaining servers that should be in composite proxy
|
227
215
|
remaining_configs = [
|
@@ -249,11 +237,6 @@ class SingleUserMCP(FastMCP[Any]):
|
|
249
237
|
del self.mounted_servers[server_name]
|
250
238
|
log.info(f"✅ Unmounted MCP server: {server_name}")
|
251
239
|
|
252
|
-
async def _stop_server_process(self, server_name: str) -> None:
|
253
|
-
"""Stop the server subprocess if it's not a test server."""
|
254
|
-
if server_name != "test-echo":
|
255
|
-
await self.mcp_manager.stop_server(server_name)
|
256
|
-
|
257
240
|
async def get_mounted_servers(self) -> list[ServerStatusInfo]:
|
258
241
|
"""Get list of currently mounted servers."""
|
259
242
|
return [
|
@@ -22,6 +22,7 @@ from __future__ import annotations
|
|
22
22
|
|
23
23
|
import json
|
24
24
|
import os
|
25
|
+
import platform
|
25
26
|
import traceback
|
26
27
|
import uuid
|
27
28
|
from collections.abc import Callable
|
@@ -175,12 +176,27 @@ def initialize_telemetry(override: TelemetryConfig | None = None) -> None: # no
|
|
175
176
|
|
176
177
|
# Provider/meter
|
177
178
|
try:
|
179
|
+
# Capture platform/runtime details
|
180
|
+
install_id = _ensure_install_id()
|
181
|
+
os_type = platform.system().lower() or "unknown"
|
182
|
+
os_description = platform.platform()
|
183
|
+
host_arch = platform.machine()
|
184
|
+
runtime_version = platform.python_version()
|
185
|
+
service_version = getattr(config, "version", "unknown")
|
186
|
+
|
178
187
|
# Attach a resource so metrics include service identifiers
|
179
188
|
resource = Resource.create(
|
180
189
|
{
|
181
190
|
"service.name": "open-edison",
|
182
191
|
"service.namespace": "open-edison",
|
192
|
+
"service.version": service_version,
|
193
|
+
"service.instance.id": install_id,
|
183
194
|
"telemetry.sdk.language": "python",
|
195
|
+
"os.type": os_type,
|
196
|
+
"os.description": os_description,
|
197
|
+
"host.arch": host_arch,
|
198
|
+
"process.runtime.name": "python",
|
199
|
+
"process.runtime.version": runtime_version,
|
184
200
|
}
|
185
201
|
)
|
186
202
|
provider: Any = ot_sdk_metrics.MeterProvider(metric_readers=[reader], resource=resource)
|
@@ -209,7 +225,7 @@ def initialize_telemetry(override: TelemetryConfig | None = None) -> None: # no
|
|
209
225
|
log.error("Metrics instrument creation failed\n{}", traceback.format_exc())
|
210
226
|
return
|
211
227
|
|
212
|
-
_ =
|
228
|
+
_ = install_id
|
213
229
|
_initialized = True
|
214
230
|
log.info("📈 Telemetry initialized")
|
215
231
|
|