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.
Files changed (38) hide show
  1. {open_edison-0.1.16 → open_edison-0.1.17}/PKG-INFO +19 -3
  2. {open_edison-0.1.16 → open_edison-0.1.17}/README.md +18 -2
  3. {open_edison-0.1.16 → open_edison-0.1.17}/config.json +1 -1
  4. {open_edison-0.1.16 → open_edison-0.1.17}/pyproject.toml +6 -4
  5. {open_edison-0.1.16 → open_edison-0.1.17}/src/middleware/session_tracking.py +1 -1
  6. {open_edison-0.1.16 → open_edison-0.1.17}/src/server.py +41 -145
  7. {open_edison-0.1.16 → open_edison-0.1.17}/src/single_user_mcp.py +5 -22
  8. {open_edison-0.1.16 → open_edison-0.1.17}/src/telemetry.py +17 -1
  9. open_edison-0.1.16/src/frontend_dist/assets/index-_NTxjOfh.js +0 -51
  10. open_edison-0.1.16/src/frontend_dist/assets/index-h6k8aL6h.css +0 -1
  11. open_edison-0.1.16/src/frontend_dist/index.html +0 -21
  12. open_edison-0.1.16/src/mcp_manager.py +0 -137
  13. {open_edison-0.1.16 → open_edison-0.1.17}/.gitignore +0 -0
  14. {open_edison-0.1.16 → open_edison-0.1.17}/LICENSE +0 -0
  15. {open_edison-0.1.16 → open_edison-0.1.17}/desktop_ext/README.md +0 -0
  16. {open_edison-0.1.16 → open_edison-0.1.17}/docs/README.md +0 -0
  17. {open_edison-0.1.16 → open_edison-0.1.17}/docs/architecture/single_user_design.md +0 -0
  18. {open_edison-0.1.16 → open_edison-0.1.17}/docs/core/configuration.md +0 -0
  19. {open_edison-0.1.16 → open_edison-0.1.17}/docs/core/project_structure.md +0 -0
  20. {open_edison-0.1.16 → open_edison-0.1.17}/docs/core/proxy_usage.md +0 -0
  21. {open_edison-0.1.16 → open_edison-0.1.17}/docs/deployment/docker.md +0 -0
  22. {open_edison-0.1.16 → open_edison-0.1.17}/docs/deployment/local.md +0 -0
  23. {open_edison-0.1.16 → open_edison-0.1.17}/docs/development/contributing.md +0 -0
  24. {open_edison-0.1.16 → open_edison-0.1.17}/docs/development/development_guide.md +0 -0
  25. {open_edison-0.1.16 → open_edison-0.1.17}/docs/development/testing.md +0 -0
  26. {open_edison-0.1.16 → open_edison-0.1.17}/docs/quick-reference/api_reference.md +0 -0
  27. {open_edison-0.1.16 → open_edison-0.1.17}/docs/quick-reference/config_quick_start.md +0 -0
  28. {open_edison-0.1.16 → open_edison-0.1.17}/frontend/configurations/prompt_permissions.json +0 -0
  29. {open_edison-0.1.16 → open_edison-0.1.17}/frontend/configurations/resource_permissions.json +0 -0
  30. {open_edison-0.1.16 → open_edison-0.1.17}/frontend/configurations/tool_permissions.json +0 -0
  31. {open_edison-0.1.16 → open_edison-0.1.17}/prompt_permissions.json +0 -0
  32. {open_edison-0.1.16 → open_edison-0.1.17}/resource_permissions.json +0 -0
  33. {open_edison-0.1.16 → open_edison-0.1.17}/src/__init__.py +0 -0
  34. {open_edison-0.1.16 → open_edison-0.1.17}/src/__main__.py +0 -0
  35. {open_edison-0.1.16 → open_edison-0.1.17}/src/cli.py +0 -0
  36. {open_edison-0.1.16 → open_edison-0.1.17}/src/config.py +0 -0
  37. {open_edison-0.1.16 → open_edison-0.1.17}/src/middleware/data_access_tracker.py +0 -0
  38. {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.16
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": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"], "enabled": true },
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": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"], "enabled": true },
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"
@@ -2,7 +2,7 @@
2
2
  "server": {
3
3
  "host": "0.0.0.0",
4
4
  "port": 3000,
5
- "api_key": "dev-api-key-change-me"
5
+ "api_key": "dev-api-key-change-me-2"
6
6
  },
7
7
  "logging": {
8
8
  "level": "INFO",
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "open-edison"
3
- version = "0.1.16"
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.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
 
@@ -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, mcp_manager: MCPManager):
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
- # Filter out test servers for composite proxy
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(real_servers)
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 real_servers:
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(real_servers)} servers")
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
- _ = _ensure_install_id()
228
+ _ = install_id
213
229
  _initialized = True
214
230
  log.info("📈 Telemetry initialized")
215
231