open-edison 0.1.19__py3-none-any.whl → 0.1.29__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.19.dist-info → open_edison-0.1.29.dist-info}/METADATA +66 -45
- open_edison-0.1.29.dist-info/RECORD +17 -0
- src/cli.py +2 -1
- src/config.py +71 -71
- src/events.py +153 -0
- src/middleware/data_access_tracker.py +164 -434
- src/middleware/session_tracking.py +133 -37
- src/oauth_manager.py +281 -0
- src/permissions.py +281 -0
- src/server.py +491 -134
- src/single_user_mcp.py +230 -158
- src/telemetry.py +4 -40
- open_edison-0.1.19.dist-info/RECORD +0 -14
- {open_edison-0.1.19.dist-info → open_edison-0.1.29.dist-info}/WHEEL +0 -0
- {open_edison-0.1.19.dist-info → open_edison-0.1.29.dist-info}/entry_points.txt +0 -0
- {open_edison-0.1.19.dist-info → open_edison-0.1.29.dist-info}/licenses/LICENSE +0 -0
src/single_user_mcp.py
CHANGED
@@ -8,20 +8,22 @@ Handles MCP protocol communication with running servers using a unified composit
|
|
8
8
|
from typing import Any, TypedDict
|
9
9
|
|
10
10
|
from fastmcp import Client as FastMCPClient
|
11
|
-
from fastmcp import FastMCP
|
11
|
+
from fastmcp import Context, FastMCP
|
12
12
|
from loguru import logger as log
|
13
13
|
|
14
|
-
from src.config import
|
14
|
+
from src.config import Config, MCPServerConfig
|
15
15
|
from src.middleware.session_tracking import (
|
16
16
|
SessionTrackingMiddleware,
|
17
17
|
get_current_session_data_tracker,
|
18
18
|
)
|
19
|
+
from src.oauth_manager import OAuthManager, OAuthStatus, get_oauth_manager
|
20
|
+
from src.permissions import Permissions, PermissionsError
|
19
21
|
|
20
22
|
|
21
23
|
class MountedServerInfo(TypedDict):
|
22
24
|
"""Type definition for mounted server information."""
|
23
25
|
|
24
|
-
config: MCPServerConfig
|
26
|
+
config: MCPServerConfig # noqa
|
25
27
|
proxy: FastMCP[Any] | None
|
26
28
|
|
27
29
|
|
@@ -29,10 +31,14 @@ class ServerStatusInfo(TypedDict):
|
|
29
31
|
"""Type definition for server status information."""
|
30
32
|
|
31
33
|
name: str
|
32
|
-
config: dict[str, str | list[str] | bool | dict[str, str] | None]
|
34
|
+
config: dict[str, str | list[str] | bool | dict[str, str] | None] # noqa
|
33
35
|
mounted: bool
|
34
36
|
|
35
37
|
|
38
|
+
# Module level because needs to be read by permissions etc
|
39
|
+
mounted_servers: dict[str, MountedServerInfo] = {}
|
40
|
+
|
41
|
+
|
36
42
|
class SingleUserMCP(FastMCP[Any]):
|
37
43
|
"""
|
38
44
|
Single-user MCP server implementation for Open Edison.
|
@@ -43,9 +49,8 @@ class SingleUserMCP(FastMCP[Any]):
|
|
43
49
|
"""
|
44
50
|
|
45
51
|
def __init__(self):
|
46
|
-
|
47
|
-
|
48
|
-
self.composite_proxy: FastMCP[Any] | None = None
|
52
|
+
# Disable error masking so upstream error details are preserved in responses
|
53
|
+
super().__init__(name="open-edison-single-user", mask_error_details=False)
|
49
54
|
|
50
55
|
# Add session tracking middleware for data access monitoring
|
51
56
|
self.add_middleware(SessionTrackingMiddleware())
|
@@ -68,10 +73,6 @@ class SingleUserMCP(FastMCP[Any]):
|
|
68
73
|
mcp_servers: dict[str, dict[str, Any]] = {}
|
69
74
|
|
70
75
|
for server_config in enabled_servers:
|
71
|
-
# Skip test servers for composite proxy
|
72
|
-
if server_config.command == "echo":
|
73
|
-
continue
|
74
|
-
|
75
76
|
server_entry: dict[str, Any] = {
|
76
77
|
"command": server_config.command,
|
77
78
|
"args": server_config.args,
|
@@ -86,15 +87,6 @@ class SingleUserMCP(FastMCP[Any]):
|
|
86
87
|
|
87
88
|
return {"mcpServers": mcp_servers}
|
88
89
|
|
89
|
-
async def _mount_test_server(self, server_config: MCPServerConfig) -> bool:
|
90
|
-
"""Mount a test server with mock configuration."""
|
91
|
-
log.info(f"Mock mounting test server: {server_config.name}")
|
92
|
-
self.mounted_servers[server_config.name] = MountedServerInfo(
|
93
|
-
config=server_config, proxy=None
|
94
|
-
)
|
95
|
-
log.info(f"✅ Mounted test server: {server_config.name}")
|
96
|
-
return True
|
97
|
-
|
98
90
|
async def create_composite_proxy(self, enabled_servers: list[MCPServerConfig]) -> bool:
|
99
91
|
"""
|
100
92
|
Create a unified composite proxy for all enabled MCP servers.
|
@@ -112,81 +104,194 @@ class SingleUserMCP(FastMCP[Any]):
|
|
112
104
|
log.info("No real servers to mount in composite proxy")
|
113
105
|
return True
|
114
106
|
|
115
|
-
|
116
|
-
|
107
|
+
oauth_manager = get_oauth_manager()
|
108
|
+
|
117
109
|
for server_config in enabled_servers:
|
118
110
|
server_name = server_config.name
|
111
|
+
|
119
112
|
# Skip if this server would produce an empty config (e.g., misconfigured)
|
120
113
|
fastmcp_config = self._convert_to_fastmcp_config([server_config])
|
121
114
|
if not fastmcp_config.get("mcpServers"):
|
122
115
|
log.warning(f"Skipping server '{server_name}' due to empty MCP config")
|
123
116
|
continue
|
124
|
-
|
125
|
-
|
126
|
-
|
117
|
+
|
118
|
+
try:
|
119
|
+
await self._mount_single_server(server_config, fastmcp_config, oauth_manager)
|
120
|
+
except Exception as e:
|
121
|
+
log.error(f"❌ Failed to mount server {server_name}: {e}")
|
122
|
+
# Continue with other servers even if one fails
|
123
|
+
continue
|
127
124
|
|
128
125
|
log.info(
|
129
|
-
f"✅ Created composite proxy with {len(enabled_servers)} servers ({
|
126
|
+
f"✅ Created composite proxy with {len(enabled_servers)} servers ({mounted_servers.keys()})"
|
130
127
|
)
|
131
128
|
return True
|
132
129
|
|
133
|
-
async def
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
130
|
+
async def _mount_single_server(
|
131
|
+
self,
|
132
|
+
server_config: MCPServerConfig,
|
133
|
+
fastmcp_config: dict[str, Any],
|
134
|
+
oauth_manager: OAuthManager,
|
135
|
+
) -> None:
|
136
|
+
"""Mount a single MCP server with appropriate OAuth handling."""
|
137
|
+
server_name = server_config.name
|
138
|
+
|
139
|
+
# Check OAuth requirements for this server
|
140
|
+
remote_url = server_config.get_remote_url()
|
141
|
+
oauth_info = await oauth_manager.check_oauth_requirement(server_name, remote_url)
|
142
|
+
|
143
|
+
# Create proxy based on server type to avoid union type issues
|
144
|
+
if server_config.is_remote_server():
|
145
|
+
# Handle remote servers (with or without OAuth)
|
146
|
+
if not remote_url:
|
147
|
+
log.error(f"❌ Remote server {server_name} has no URL")
|
148
|
+
return
|
149
|
+
|
150
|
+
if oauth_info.status == OAuthStatus.AUTHENTICATED:
|
151
|
+
# Remote server with OAuth authentication
|
152
|
+
oauth_auth = oauth_manager.get_oauth_auth(
|
153
|
+
server_name,
|
154
|
+
remote_url,
|
155
|
+
server_config.oauth_scopes,
|
156
|
+
server_config.oauth_client_name,
|
157
|
+
)
|
158
|
+
if oauth_auth:
|
159
|
+
client = FastMCPClient(remote_url, auth=oauth_auth)
|
160
|
+
log.info(
|
161
|
+
f"🔐 Created remote client with OAuth authentication for {server_name}"
|
162
|
+
)
|
163
|
+
else:
|
164
|
+
client = FastMCPClient(remote_url)
|
165
|
+
log.warning(
|
166
|
+
f"⚠️ OAuth auth creation failed, using unauthenticated client for {server_name}"
|
167
|
+
)
|
168
|
+
else:
|
169
|
+
# Remote server without OAuth or needs auth
|
170
|
+
client = FastMCPClient(remote_url)
|
171
|
+
log.info(f"🌐 Created remote client for {server_name}")
|
172
|
+
|
173
|
+
# Log OAuth status warnings
|
174
|
+
if oauth_info.status == OAuthStatus.NEEDS_AUTH:
|
175
|
+
log.warning(
|
176
|
+
f"⚠️ Server {server_name} requires OAuth but no valid tokens found. "
|
177
|
+
f"Server will be mounted without authentication and may fail."
|
178
|
+
)
|
179
|
+
elif oauth_info.status == OAuthStatus.ERROR:
|
180
|
+
log.warning(f"⚠️ OAuth check failed for {server_name}: {oauth_info.error_message}")
|
181
|
+
|
182
|
+
# Create proxy from remote client
|
183
|
+
proxy = FastMCP.as_proxy(client)
|
184
|
+
|
185
|
+
else:
|
186
|
+
# Local server - create proxy directly from config (avoids union type issue)
|
187
|
+
log.info(f"🔧 Creating local process proxy for {server_name}")
|
188
|
+
proxy = FastMCP.as_proxy(fastmcp_config)
|
189
|
+
|
190
|
+
super().mount(proxy, prefix=server_name)
|
191
|
+
mounted_servers[server_name] = MountedServerInfo(config=server_config, proxy=proxy)
|
192
|
+
|
193
|
+
server_type = "remote" if server_config.is_remote_server() else "local"
|
194
|
+
log.info(
|
195
|
+
f"✅ Mounted {server_type} server {server_name} (OAuth: {oauth_info.status.value})"
|
196
|
+
)
|
165
197
|
|
166
198
|
async def get_mounted_servers(self) -> list[ServerStatusInfo]:
|
167
199
|
"""Get list of currently mounted servers."""
|
168
200
|
return [
|
169
201
|
ServerStatusInfo(name=name, config=mounted["config"].__dict__, mounted=True)
|
170
|
-
for name, mounted in
|
202
|
+
for name, mounted in mounted_servers.items()
|
171
203
|
]
|
172
204
|
|
173
|
-
async def
|
205
|
+
async def mount_server(self, server_name: str) -> bool:
|
206
|
+
"""
|
207
|
+
Mount a server by name if not already mounted.
|
208
|
+
|
209
|
+
Returns True if newly mounted, False if it was already mounted or failed.
|
210
|
+
"""
|
211
|
+
if server_name in mounted_servers:
|
212
|
+
log.info(f"🔁 Server {server_name} already mounted")
|
213
|
+
return False
|
214
|
+
|
215
|
+
# Find server configuration
|
216
|
+
server_config: MCPServerConfig | None = next(
|
217
|
+
(s for s in Config().mcp_servers if s.name == server_name), None
|
218
|
+
)
|
219
|
+
|
220
|
+
if server_config is None:
|
221
|
+
log.error(f"❌ Server configuration not found: {server_name}")
|
222
|
+
return False
|
223
|
+
|
224
|
+
# Build minimal FastMCP backend config for just this server
|
225
|
+
fastmcp_config = self._convert_to_fastmcp_config([server_config])
|
226
|
+
if not fastmcp_config.get("mcpServers"):
|
227
|
+
log.error(f"❌ Invalid/empty MCP config for server: {server_name}")
|
228
|
+
return False
|
229
|
+
|
230
|
+
try:
|
231
|
+
oauth_manager = get_oauth_manager()
|
232
|
+
await self._mount_single_server(server_config, fastmcp_config, oauth_manager)
|
233
|
+
# Warm lists after mount
|
234
|
+
_ = await self._tool_manager.list_tools()
|
235
|
+
_ = await self._resource_manager.list_resources()
|
236
|
+
_ = await self._prompt_manager.list_prompts()
|
237
|
+
return True
|
238
|
+
except Exception as e: # noqa: BLE001
|
239
|
+
log.error(f"❌ Failed to mount server {server_name}: {e}")
|
240
|
+
return False
|
241
|
+
|
242
|
+
async def unmount(self, server_name: str) -> bool:
|
243
|
+
"""
|
244
|
+
Unmount a previously mounted server by name.
|
245
|
+
|
246
|
+
Returns True if it was unmounted, False if it wasn't mounted.
|
247
|
+
"""
|
248
|
+
info = mounted_servers.pop(server_name, None)
|
249
|
+
if info is None:
|
250
|
+
log.info(f"ℹ️ Server {server_name} was not mounted")
|
251
|
+
return False
|
252
|
+
|
253
|
+
proxy = info.get("proxy")
|
254
|
+
|
255
|
+
# Manually remove from FastMCP managers' mounted lists
|
256
|
+
for manager_name in ("_tool_manager", "_resource_manager", "_prompt_manager"):
|
257
|
+
manager = getattr(self, manager_name, None)
|
258
|
+
mounted_list = getattr(manager, "_mounted_servers", None)
|
259
|
+
if mounted_list is None:
|
260
|
+
continue
|
261
|
+
|
262
|
+
# Prefer removing by both prefix and object identity; fallback to prefix-only
|
263
|
+
new_list = [
|
264
|
+
m
|
265
|
+
for m in mounted_list
|
266
|
+
if not (m.prefix == server_name and (proxy is None or m.server is proxy))
|
267
|
+
]
|
268
|
+
if len(new_list) == len(mounted_list):
|
269
|
+
new_list = [m for m in mounted_list if m.prefix != server_name]
|
270
|
+
|
271
|
+
mounted_list[:] = new_list
|
272
|
+
|
273
|
+
# Invalidate and warm lists to ensure reload
|
274
|
+
_ = await self._tool_manager.list_tools()
|
275
|
+
_ = await self._resource_manager.list_resources()
|
276
|
+
_ = await self._prompt_manager.list_prompts()
|
277
|
+
|
278
|
+
log.info(f"🧹 Unmounted server {server_name} and cleared references")
|
279
|
+
return True
|
280
|
+
|
281
|
+
async def initialize(self) -> None:
|
174
282
|
"""Initialize the FastMCP server using unified composite proxy approach."""
|
175
283
|
log.info("Initializing Single User MCP server with composite proxy")
|
176
|
-
|
177
|
-
log.debug(f"Available MCP servers in config: {[s.name for s in config_to_use.mcp_servers]}")
|
284
|
+
log.debug(f"Available MCP servers in config: {[s.name for s in Config().mcp_servers]}")
|
178
285
|
|
179
286
|
# Get all enabled servers
|
180
|
-
enabled_servers = [s for s in
|
287
|
+
enabled_servers = [s for s in Config().mcp_servers if s.enabled]
|
181
288
|
log.info(
|
182
289
|
f"Found {len(enabled_servers)} enabled servers: {[s.name for s in enabled_servers]}"
|
183
290
|
)
|
184
291
|
|
185
|
-
#
|
186
|
-
|
187
|
-
|
188
|
-
log.info(f"Mounting test server individually: {server_config.name}")
|
189
|
-
_ = await self._mount_test_server(server_config)
|
292
|
+
# Unmount all servers
|
293
|
+
for server_name in list(mounted_servers.keys()):
|
294
|
+
await self.unmount(server_name)
|
190
295
|
|
191
296
|
# Create composite proxy for all real servers
|
192
297
|
success = await self.create_composite_proxy(enabled_servers)
|
@@ -196,79 +301,10 @@ class SingleUserMCP(FastMCP[Any]):
|
|
196
301
|
|
197
302
|
log.info("✅ Single User MCP server initialized with composite proxy")
|
198
303
|
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
This method:
|
204
|
-
1. Cleans up all mounted servers and MCP proxies
|
205
|
-
2. Reloads the configuration
|
206
|
-
3. Reinitializes all enabled servers
|
207
|
-
|
208
|
-
Args:
|
209
|
-
test_config: Optional test configuration to use instead of reloading from disk
|
210
|
-
|
211
|
-
Returns:
|
212
|
-
Dictionary with reinitialization status and details
|
213
|
-
"""
|
214
|
-
log.info("🔄 Reinitializing all MCP servers")
|
215
|
-
|
216
|
-
try:
|
217
|
-
# Step 1: Clean up existing mounted servers and proxies
|
218
|
-
log.info("Cleaning up existing mounted servers and proxies")
|
219
|
-
|
220
|
-
# Clean up composite proxy if it exists
|
221
|
-
if self.composite_proxy is not None:
|
222
|
-
log.info("Cleaning up composite proxy")
|
223
|
-
self.composite_proxy = None
|
224
|
-
|
225
|
-
# Clean up all mounted servers
|
226
|
-
mounted_server_names = list(self.mounted_servers.keys())
|
227
|
-
for server_name in mounted_server_names:
|
228
|
-
await self._cleanup_mounted_server(server_name)
|
229
|
-
|
230
|
-
# Clear the mounted servers dictionary completely
|
231
|
-
self.mounted_servers.clear()
|
232
|
-
|
233
|
-
log.info(f"✅ Cleaned up {len(mounted_server_names)} mounted servers")
|
234
|
-
|
235
|
-
# Step 2: Reload configuration if not using test config
|
236
|
-
config_to_use = test_config
|
237
|
-
if test_config is None:
|
238
|
-
log.info("Reloading configuration from disk")
|
239
|
-
# Import here to avoid circular imports
|
240
|
-
from src.config import Config
|
241
|
-
|
242
|
-
config_to_use = Config.load()
|
243
|
-
log.info("✅ Configuration reloaded from disk")
|
244
|
-
|
245
|
-
# Step 3: Reinitialize all servers
|
246
|
-
log.info("Reinitializing servers with fresh configuration")
|
247
|
-
await self.initialize(config_to_use)
|
248
|
-
|
249
|
-
# Step 4: Get final status
|
250
|
-
final_mounted = await self.get_mounted_servers()
|
251
|
-
|
252
|
-
result = {
|
253
|
-
"status": "success",
|
254
|
-
"message": "MCP servers reinitialized successfully",
|
255
|
-
"cleaned_up_servers": mounted_server_names,
|
256
|
-
"final_mounted_servers": [server["name"] for server in final_mounted],
|
257
|
-
"total_final_mounted": len(final_mounted),
|
258
|
-
}
|
259
|
-
|
260
|
-
log.info(
|
261
|
-
f"✅ Reinitialization complete. Final mounted servers: {result['final_mounted_servers']}"
|
262
|
-
)
|
263
|
-
return result
|
264
|
-
|
265
|
-
except Exception as e:
|
266
|
-
log.error(f"❌ Failed to reinitialize MCP servers: {e}")
|
267
|
-
return {
|
268
|
-
"status": "error",
|
269
|
-
"message": f"Failed to reinitialize MCP servers: {str(e)}",
|
270
|
-
"error": str(e),
|
271
|
-
}
|
304
|
+
# Invalidate and warm lists to ensure reload
|
305
|
+
_ = await self._tool_manager.list_tools()
|
306
|
+
_ = await self._resource_manager.list_resources()
|
307
|
+
_ = await self._prompt_manager.list_prompts()
|
272
308
|
|
273
309
|
def _calculate_risk_level(self, trifecta: dict[str, bool]) -> str:
|
274
310
|
"""
|
@@ -298,8 +334,8 @@ class SingleUserMCP(FastMCP[Any]):
|
|
298
334
|
def _setup_demo_tools(self) -> None:
|
299
335
|
"""Set up built-in demo tools for testing."""
|
300
336
|
|
301
|
-
@self.tool()
|
302
|
-
def
|
337
|
+
@self.tool() # noqa
|
338
|
+
def builtin_echo(text: str) -> str:
|
303
339
|
"""
|
304
340
|
Echo back the provided text.
|
305
341
|
|
@@ -312,8 +348,8 @@ class SingleUserMCP(FastMCP[Any]):
|
|
312
348
|
log.info(f"🔊 Echo tool called with: {text}")
|
313
349
|
return f"Echo: {text}"
|
314
350
|
|
315
|
-
@self.tool()
|
316
|
-
def
|
351
|
+
@self.tool() # noqa
|
352
|
+
def builtin_get_server_info() -> dict[str, str | list[str] | int]:
|
317
353
|
"""
|
318
354
|
Get information about the Open Edison server.
|
319
355
|
|
@@ -323,13 +359,13 @@ class SingleUserMCP(FastMCP[Any]):
|
|
323
359
|
log.info("ℹ️ Server info tool called")
|
324
360
|
return {
|
325
361
|
"name": "Open Edison Single User",
|
326
|
-
"version":
|
327
|
-
"mounted_servers": list(
|
328
|
-
"total_mounted": len(
|
362
|
+
"version": Config().version,
|
363
|
+
"mounted_servers": list(mounted_servers.keys()),
|
364
|
+
"total_mounted": len(mounted_servers),
|
329
365
|
}
|
330
366
|
|
331
|
-
@self.tool()
|
332
|
-
def
|
367
|
+
@self.tool() # noqa
|
368
|
+
def builtin_get_security_status() -> dict[str, Any]:
|
333
369
|
"""
|
334
370
|
Get the current session's security status and data access summary.
|
335
371
|
|
@@ -353,18 +389,54 @@ class SingleUserMCP(FastMCP[Any]):
|
|
353
389
|
|
354
390
|
return security_data
|
355
391
|
|
356
|
-
|
392
|
+
@self.tool() # noqa
|
393
|
+
async def builtin_get_available_tools() -> list[str]:
|
394
|
+
"""
|
395
|
+
Get a list of all available tools. Use this tool to get an updated list of available tools.
|
396
|
+
"""
|
397
|
+
tool_list = await self._tool_manager.list_tools()
|
398
|
+
available_tools: list[str] = []
|
399
|
+
log.trace(f"Raw tool list: {tool_list}")
|
400
|
+
perms = Permissions()
|
401
|
+
for tool in tool_list:
|
402
|
+
# Use the prefixed key (e.g., "filesystem_read_file") to match flattened permissions
|
403
|
+
perm_key = tool.key
|
404
|
+
try:
|
405
|
+
is_enabled: bool = perms.is_tool_enabled(perm_key)
|
406
|
+
except PermissionsError:
|
407
|
+
# Unknown in permissions → treat as disabled
|
408
|
+
is_enabled = False
|
409
|
+
if is_enabled:
|
410
|
+
# Return the invocable name (key), which matches the MCP-exposed name
|
411
|
+
available_tools.append(tool.key)
|
412
|
+
return available_tools
|
413
|
+
|
414
|
+
@self.tool() # noqa
|
415
|
+
async def builtin_tools_changed(ctx: Context) -> str:
|
416
|
+
"""
|
417
|
+
Notify the MCP client that the tool list has changed. You should call this tool periodically
|
418
|
+
to ensure the client has the latest list of available tools.
|
419
|
+
"""
|
420
|
+
await ctx.send_tool_list_changed()
|
421
|
+
await ctx.send_resource_list_changed()
|
422
|
+
await ctx.send_prompt_list_changed()
|
423
|
+
|
424
|
+
return "Notifications sent"
|
425
|
+
|
426
|
+
log.info(
|
427
|
+
"✅ Added built-in demo tools: echo, get_server_info, get_security_status, builtin_get_available_tools, builtin_tools_changed"
|
428
|
+
)
|
357
429
|
|
358
430
|
def _setup_demo_resources(self) -> None:
|
359
431
|
"""Set up built-in demo resources for testing."""
|
360
432
|
|
361
|
-
@self.resource("config://app")
|
362
|
-
def
|
433
|
+
@self.resource("config://app") # noqa
|
434
|
+
def builtin_get_app_config() -> dict[str, Any]:
|
363
435
|
"""Get application configuration."""
|
364
436
|
return {
|
365
|
-
"version":
|
366
|
-
"mounted_servers": list(
|
367
|
-
"total_mounted": len(
|
437
|
+
"version": Config().version,
|
438
|
+
"mounted_servers": list(mounted_servers.keys()),
|
439
|
+
"total_mounted": len(mounted_servers),
|
368
440
|
}
|
369
441
|
|
370
442
|
log.info("✅ Added built-in demo resources: config://app")
|
@@ -372,8 +444,8 @@ class SingleUserMCP(FastMCP[Any]):
|
|
372
444
|
def _setup_demo_prompts(self) -> None:
|
373
445
|
"""Set up built-in demo prompts for testing."""
|
374
446
|
|
375
|
-
@self.prompt()
|
376
|
-
def
|
447
|
+
@self.prompt() # noqa
|
448
|
+
def builtin_summarize_text(text: str) -> str:
|
377
449
|
"""Create a prompt to summarize the given text."""
|
378
450
|
return f"""
|
379
451
|
Please provide a concise, one-paragraph summary of the following text:
|
src/telemetry.py
CHANGED
@@ -18,9 +18,6 @@ Events/metrics captured (high level, install-unique ID for deaggregation):
|
|
18
18
|
Configuration: see `TelemetryConfig` in `src.config`.
|
19
19
|
"""
|
20
20
|
|
21
|
-
from __future__ import annotations
|
22
|
-
|
23
|
-
import json
|
24
21
|
import os
|
25
22
|
import platform
|
26
23
|
import traceback
|
@@ -37,7 +34,7 @@ from opentelemetry.sdk import metrics as ot_sdk_metrics
|
|
37
34
|
from opentelemetry.sdk.metrics import export as ot_metrics_export
|
38
35
|
from opentelemetry.sdk.resources import Resource # type: ignore[reportMissingTypeStubs]
|
39
36
|
|
40
|
-
from src.config import
|
37
|
+
from src.config import Config, TelemetryConfig, get_config_dir
|
41
38
|
|
42
39
|
_initialized: bool = False
|
43
40
|
_install_id: str | None = None
|
@@ -94,7 +91,7 @@ def _ensure_install_id() -> str:
|
|
94
91
|
|
95
92
|
|
96
93
|
def _telemetry_enabled() -> bool:
|
97
|
-
tel_cfg =
|
94
|
+
tel_cfg = Config().telemetry or TelemetryConfig()
|
98
95
|
return bool(tel_cfg.enabled)
|
99
96
|
|
100
97
|
|
@@ -134,7 +131,7 @@ def initialize_telemetry(override: TelemetryConfig | None = None) -> None: # no
|
|
134
131
|
if _initialized:
|
135
132
|
return
|
136
133
|
|
137
|
-
telemetry_cfg = override if override is not None else (
|
134
|
+
telemetry_cfg = override if override is not None else (Config().telemetry or TelemetryConfig())
|
138
135
|
if not telemetry_cfg.enabled:
|
139
136
|
log.debug("Telemetry disabled by config")
|
140
137
|
_initialized = True
|
@@ -182,7 +179,7 @@ def initialize_telemetry(override: TelemetryConfig | None = None) -> None: # no
|
|
182
179
|
os_description = platform.platform()
|
183
180
|
host_arch = platform.machine()
|
184
181
|
runtime_version = platform.python_version()
|
185
|
-
service_version =
|
182
|
+
service_version = Config().version
|
186
183
|
|
187
184
|
# Attach a resource so metrics include service identifiers
|
188
185
|
resource = Resource.create(
|
@@ -230,29 +227,6 @@ def initialize_telemetry(override: TelemetryConfig | None = None) -> None: # no
|
|
230
227
|
log.info("📈 Telemetry initialized")
|
231
228
|
|
232
229
|
|
233
|
-
def force_flush_metrics(timeout_ms: int = 5000) -> bool:
|
234
|
-
"""Force-flush metrics synchronously if a provider is initialized.
|
235
|
-
|
236
|
-
Returns True on success, False otherwise.
|
237
|
-
"""
|
238
|
-
try:
|
239
|
-
provider = _provider
|
240
|
-
if provider is None:
|
241
|
-
return False
|
242
|
-
# Some providers expose force_flush(timeout_millis=...), others as force_flush() -> bool
|
243
|
-
if hasattr(provider, "force_flush"):
|
244
|
-
try:
|
245
|
-
# Try with timeout argument first
|
246
|
-
result = provider.force_flush(timeout_millis=timeout_ms) # type: ignore[misc]
|
247
|
-
except TypeError:
|
248
|
-
result = provider.force_flush()
|
249
|
-
return bool(result)
|
250
|
-
return False
|
251
|
-
except Exception: # noqa: BLE001
|
252
|
-
log.error("Force flush failed\n{}", traceback.format_exc())
|
253
|
-
return False
|
254
|
-
|
255
|
-
|
256
230
|
def _common_attrs(extra: dict[str, Any] | None = None) -> dict[str, Any]:
|
257
231
|
attrs: dict[str, Any] = {"install_id": _ensure_install_id(), "app": "open-edison"}
|
258
232
|
if extra:
|
@@ -276,16 +250,6 @@ def record_tool_call_blocked(tool_name: str, reason: str) -> None:
|
|
276
250
|
)
|
277
251
|
|
278
252
|
|
279
|
-
@telemetry_recorder
|
280
|
-
def record_tool_call_metadata(tool_name: str, metadata: dict[str, Any]) -> None:
|
281
|
-
if _tool_calls_metadata_counter is None:
|
282
|
-
return
|
283
|
-
metadata_str = json.dumps(metadata, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
284
|
-
_tool_calls_metadata_counter.add(
|
285
|
-
1, attributes=_common_attrs({"tool": tool_name, "metadata_json": metadata_str})
|
286
|
-
)
|
287
|
-
|
288
|
-
|
289
253
|
@telemetry_recorder
|
290
254
|
def set_servers_installed(count: int) -> None:
|
291
255
|
if _servers_installed_gauge is None:
|
@@ -1,14 +0,0 @@
|
|
1
|
-
src/__init__.py,sha256=QWeZdjAm2D2B0eWhd8m2-DPpWvIP26KcNJxwEoU1oEQ,254
|
2
|
-
src/__main__.py,sha256=kQsaVyzRa_ESC57JpKDSQJAHExuXme0rM5beJsYxFeA,161
|
3
|
-
src/cli.py,sha256=9cJN6mRvjbCcpTyTdUVl47J7OB7bxzSy0h8tfVbHuQU,9982
|
4
|
-
src/config.py,sha256=2a5rdImQmNGggL690PQprqZVsRUAJcdo8KS2Foj9N-U,9345
|
5
|
-
src/server.py,sha256=cXW16m6UMUofQFbtM6E2EasxClhWAS-955BuasNupmM,29557
|
6
|
-
src/single_user_mcp.py,sha256=3pDBMant1DNlNPeW_NWD-uFyLrA-qNrx6sDHgDKsDfM,14457
|
7
|
-
src/telemetry.py,sha256=M8iZ7nTPA6BhbPna_xsEoTOOa7A81YyvZ0CkVYa_pPg,12619
|
8
|
-
src/middleware/data_access_tracker.py,sha256=N4g_T-JF9W7yzRIFRasY-JA7ha-Zt_Ov4nSn-TCq-Ps,27026
|
9
|
-
src/middleware/session_tracking.py,sha256=O-n8RvEVCUGAFGYny_gA7-MMQYSlvND-lj3oBZLCT3U,20046
|
10
|
-
open_edison-0.1.19.dist-info/METADATA,sha256=88QHl-ngXl0uFZwYUO2dVMtxmQ0T3eyVUE7NyP6jXAY,10905
|
11
|
-
open_edison-0.1.19.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
12
|
-
open_edison-0.1.19.dist-info/entry_points.txt,sha256=qNAkJcnoTXRhj8J--3PDmXz_TQKdB8H_0C9wiCtDIyA,72
|
13
|
-
open_edison-0.1.19.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
14
|
-
open_edison-0.1.19.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|