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.
- {open_edison-0.1.15.dist-info → open_edison-0.1.17.dist-info}/METADATA +82 -121
- open_edison-0.1.17.dist-info/RECORD +14 -0
- src/middleware/session_tracking.py +2 -2
- src/server.py +41 -145
- src/single_user_mcp.py +5 -22
- src/telemetry.py +17 -1
- open_edison-0.1.15.dist-info/RECORD +0 -18
- src/frontend_dist/assets/index-_NTxjOfh.js +0 -51
- src/frontend_dist/assets/index-h6k8aL6h.css +0 -1
- src/frontend_dist/index.html +0 -21
- src/mcp_manager.py +0 -137
- {open_edison-0.1.15.dist-info → open_edison-0.1.17.dist-info}/WHEEL +0 -0
- {open_edison-0.1.15.dist-info → open_edison-0.1.17.dist-info}/entry_points.txt +0 -0
- {open_edison-0.1.15.dist-info → open_edison-0.1.17.dist-info}/licenses/LICENSE +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
|
@@ -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
|
-
#
|
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
|
-
|
122
|
+
1. Set up the project:
|
78
123
|
|
79
124
|
```bash
|
80
125
|
make setup
|
81
126
|
```
|
82
127
|
|
83
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
161
|
-
# Install dependencies
|
162
|
-
make sync
|
180
|
+
### Setup
|
163
181
|
|
164
|
-
|
165
|
-
make dev
|
182
|
+
Setup from source as above.
|
166
183
|
|
167
|
-
|
168
|
-
make test
|
184
|
+
### Run
|
169
185
|
|
170
|
-
|
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
|
185
|
-
# or
|
186
|
-
open-edison website
|
189
|
+
make run
|
187
190
|
```
|
188
191
|
|
189
|
-
|
192
|
+
### Tests/code quality
|
190
193
|
|
191
|
-
|
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
|
-
|
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
|
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.
|
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
|
-
"
|
242
|
-
|
243
|
-
"
|
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
|
-
"
|
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.
|
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
|
-
|
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.
|
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
|
-
|
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
|
-
|
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.
|
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
|
|