open-edison 0.1.17__py3-none-any.whl → 0.1.26__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.17.dist-info → open_edison-0.1.26.dist-info}/METADATA +124 -51
- open_edison-0.1.26.dist-info/RECORD +17 -0
- src/cli.py +2 -1
- src/config.py +63 -51
- src/events.py +153 -0
- src/middleware/data_access_tracker.py +165 -406
- src/middleware/session_tracking.py +93 -29
- src/oauth_manager.py +281 -0
- src/permissions.py +292 -0
- src/server.py +525 -98
- src/single_user_mcp.py +215 -153
- src/telemetry.py +4 -40
- open_edison-0.1.17.dist-info/RECORD +0 -14
- {open_edison-0.1.17.dist-info → open_edison-0.1.26.dist-info}/WHEEL +0 -0
- {open_edison-0.1.17.dist-info → open_edison-0.1.26.dist-info}/entry_points.txt +0 -0
- {open_edison-0.1.17.dist-info → open_edison-0.1.26.dist-info}/licenses/LICENSE +0 -0
src/single_user_mcp.py
CHANGED
@@ -7,20 +7,23 @@ Handles MCP protocol communication with running servers using a unified composit
|
|
7
7
|
|
8
8
|
from typing import Any, TypedDict
|
9
9
|
|
10
|
-
from fastmcp import
|
10
|
+
from fastmcp import Client as FastMCPClient
|
11
|
+
from fastmcp import Context, FastMCP
|
11
12
|
from loguru import logger as log
|
12
13
|
|
13
|
-
from src.config import
|
14
|
+
from src.config import Config, MCPServerConfig
|
14
15
|
from src.middleware.session_tracking import (
|
15
16
|
SessionTrackingMiddleware,
|
16
17
|
get_current_session_data_tracker,
|
17
18
|
)
|
19
|
+
from src.oauth_manager import OAuthManager, OAuthStatus, get_oauth_manager
|
20
|
+
from src.permissions import Permissions, PermissionsError
|
18
21
|
|
19
22
|
|
20
23
|
class MountedServerInfo(TypedDict):
|
21
24
|
"""Type definition for mounted server information."""
|
22
25
|
|
23
|
-
config: MCPServerConfig
|
26
|
+
config: MCPServerConfig # noqa
|
24
27
|
proxy: FastMCP[Any] | None
|
25
28
|
|
26
29
|
|
@@ -28,10 +31,14 @@ class ServerStatusInfo(TypedDict):
|
|
28
31
|
"""Type definition for server status information."""
|
29
32
|
|
30
33
|
name: str
|
31
|
-
config: dict[str, str | list[str] | bool | dict[str, str] | None]
|
34
|
+
config: dict[str, str | list[str] | bool | dict[str, str] | None] # noqa
|
32
35
|
mounted: bool
|
33
36
|
|
34
37
|
|
38
|
+
# Module level because needs to be read by permissions etc
|
39
|
+
mounted_servers: dict[str, MountedServerInfo] = {}
|
40
|
+
|
41
|
+
|
35
42
|
class SingleUserMCP(FastMCP[Any]):
|
36
43
|
"""
|
37
44
|
Single-user MCP server implementation for Open Edison.
|
@@ -43,8 +50,6 @@ class SingleUserMCP(FastMCP[Any]):
|
|
43
50
|
|
44
51
|
def __init__(self):
|
45
52
|
super().__init__(name="open-edison-single-user")
|
46
|
-
self.mounted_servers: dict[str, MountedServerInfo] = {}
|
47
|
-
self.composite_proxy: FastMCP[Any] | None = None
|
48
53
|
|
49
54
|
# Add session tracking middleware for data access monitoring
|
50
55
|
self.add_middleware(SessionTrackingMiddleware())
|
@@ -67,10 +72,6 @@ class SingleUserMCP(FastMCP[Any]):
|
|
67
72
|
mcp_servers: dict[str, dict[str, Any]] = {}
|
68
73
|
|
69
74
|
for server_config in enabled_servers:
|
70
|
-
# Skip test servers for composite proxy
|
71
|
-
if server_config.command == "echo":
|
72
|
-
continue
|
73
|
-
|
74
75
|
server_entry: dict[str, Any] = {
|
75
76
|
"command": server_config.command,
|
76
77
|
"args": server_config.args,
|
@@ -85,15 +86,6 @@ class SingleUserMCP(FastMCP[Any]):
|
|
85
86
|
|
86
87
|
return {"mcpServers": mcp_servers}
|
87
88
|
|
88
|
-
async def _mount_test_server(self, server_config: MCPServerConfig) -> bool:
|
89
|
-
"""Mount a test server with mock configuration."""
|
90
|
-
log.info(f"Mock mounting test server: {server_config.name}")
|
91
|
-
self.mounted_servers[server_config.name] = MountedServerInfo(
|
92
|
-
config=server_config, proxy=None
|
93
|
-
)
|
94
|
-
log.info(f"✅ Mounted test server: {server_config.name}")
|
95
|
-
return True
|
96
|
-
|
97
89
|
async def create_composite_proxy(self, enabled_servers: list[MCPServerConfig]) -> bool:
|
98
90
|
"""
|
99
91
|
Create a unified composite proxy for all enabled MCP servers.
|
@@ -107,161 +99,195 @@ class SingleUserMCP(FastMCP[Any]):
|
|
107
99
|
Returns:
|
108
100
|
True if composite proxy was created successfully, False otherwise
|
109
101
|
"""
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
return True
|
102
|
+
if not enabled_servers:
|
103
|
+
log.info("No real servers to mount in composite proxy")
|
104
|
+
return True
|
114
105
|
|
115
|
-
|
116
|
-
fastmcp_config = self._convert_to_fastmcp_config(enabled_servers)
|
106
|
+
oauth_manager = get_oauth_manager()
|
117
107
|
|
118
|
-
|
119
|
-
|
120
|
-
)
|
108
|
+
for server_config in enabled_servers:
|
109
|
+
server_name = server_config.name
|
121
110
|
|
122
|
-
#
|
123
|
-
|
124
|
-
|
125
|
-
|
111
|
+
# Skip if this server would produce an empty config (e.g., misconfigured)
|
112
|
+
fastmcp_config = self._convert_to_fastmcp_config([server_config])
|
113
|
+
if not fastmcp_config.get("mcpServers"):
|
114
|
+
log.warning(f"Skipping server '{server_name}' due to empty MCP config")
|
115
|
+
continue
|
116
|
+
|
117
|
+
try:
|
118
|
+
await self._mount_single_server(server_config, fastmcp_config, oauth_manager)
|
119
|
+
except Exception as e:
|
120
|
+
log.error(f"❌ Failed to mount server {server_name}: {e}")
|
121
|
+
# Continue with other servers even if one fails
|
122
|
+
continue
|
126
123
|
|
127
|
-
|
128
|
-
|
129
|
-
|
124
|
+
log.info(
|
125
|
+
f"✅ Created composite proxy with {len(enabled_servers)} servers ({mounted_servers.keys()})"
|
126
|
+
)
|
127
|
+
return True
|
130
128
|
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
129
|
+
async def _mount_single_server(
|
130
|
+
self,
|
131
|
+
server_config: MCPServerConfig,
|
132
|
+
fastmcp_config: dict[str, Any],
|
133
|
+
oauth_manager: OAuthManager,
|
134
|
+
) -> None:
|
135
|
+
"""Mount a single MCP server with appropriate OAuth handling."""
|
136
|
+
server_name = server_config.name
|
137
|
+
|
138
|
+
# Check OAuth requirements for this server
|
139
|
+
remote_url = server_config.get_remote_url()
|
140
|
+
oauth_info = await oauth_manager.check_oauth_requirement(server_name, remote_url)
|
141
|
+
|
142
|
+
# Create proxy based on server type to avoid union type issues
|
143
|
+
if server_config.is_remote_server():
|
144
|
+
# Handle remote servers (with or without OAuth)
|
145
|
+
if not remote_url:
|
146
|
+
log.error(f"❌ Remote server {server_name} has no URL")
|
147
|
+
return
|
148
|
+
|
149
|
+
if oauth_info.status == OAuthStatus.AUTHENTICATED:
|
150
|
+
# Remote server with OAuth authentication
|
151
|
+
oauth_auth = oauth_manager.get_oauth_auth(
|
152
|
+
server_name,
|
153
|
+
remote_url,
|
154
|
+
server_config.oauth_scopes,
|
155
|
+
server_config.oauth_client_name,
|
135
156
|
)
|
157
|
+
if oauth_auth:
|
158
|
+
client = FastMCPClient(remote_url, auth=oauth_auth)
|
159
|
+
log.info(
|
160
|
+
f"🔐 Created remote client with OAuth authentication for {server_name}"
|
161
|
+
)
|
162
|
+
else:
|
163
|
+
client = FastMCPClient(remote_url)
|
164
|
+
log.warning(
|
165
|
+
f"⚠️ OAuth auth creation failed, using unauthenticated client for {server_name}"
|
166
|
+
)
|
167
|
+
else:
|
168
|
+
# Remote server without OAuth or needs auth
|
169
|
+
client = FastMCPClient(remote_url)
|
170
|
+
log.info(f"🌐 Created remote client for {server_name}")
|
171
|
+
|
172
|
+
# Log OAuth status warnings
|
173
|
+
if oauth_info.status == OAuthStatus.NEEDS_AUTH:
|
174
|
+
log.warning(
|
175
|
+
f"⚠️ Server {server_name} requires OAuth but no valid tokens found. "
|
176
|
+
f"Server will be mounted without authentication and may fail."
|
177
|
+
)
|
178
|
+
elif oauth_info.status == OAuthStatus.ERROR:
|
179
|
+
log.warning(f"⚠️ OAuth check failed for {server_name}: {oauth_info.error_message}")
|
136
180
|
|
137
|
-
|
138
|
-
|
181
|
+
# Create proxy from remote client
|
182
|
+
proxy = FastMCP.as_proxy(client)
|
139
183
|
|
140
|
-
|
141
|
-
|
142
|
-
|
184
|
+
else:
|
185
|
+
# Local server - create proxy directly from config (avoids union type issue)
|
186
|
+
log.info(f"🔧 Creating local process proxy for {server_name}")
|
187
|
+
proxy = FastMCP.as_proxy(fastmcp_config)
|
143
188
|
|
144
|
-
|
145
|
-
|
146
|
-
Mount a single MCP server by rebuilding the composite proxy.
|
189
|
+
super().mount(proxy, prefix=server_name)
|
190
|
+
mounted_servers[server_name] = MountedServerInfo(config=server_config, proxy=proxy)
|
147
191
|
|
148
|
-
|
149
|
-
|
192
|
+
server_type = "remote" if server_config.is_remote_server() else "local"
|
193
|
+
log.info(
|
194
|
+
f"✅ Mounted {server_type} server {server_name} (OAuth: {oauth_info.status.value})"
|
195
|
+
)
|
150
196
|
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
log.info(f"Server {server_config.name} is already mounted")
|
158
|
-
return True
|
197
|
+
async def get_mounted_servers(self) -> list[ServerStatusInfo]:
|
198
|
+
"""Get list of currently mounted servers."""
|
199
|
+
return [
|
200
|
+
ServerStatusInfo(name=name, config=mounted["config"].__dict__, mounted=True)
|
201
|
+
for name, mounted in mounted_servers.items()
|
202
|
+
]
|
159
203
|
|
160
|
-
|
161
|
-
|
162
|
-
|
204
|
+
async def mount_server(self, server_name: str) -> bool:
|
205
|
+
"""
|
206
|
+
Mount a server by name if not already mounted.
|
163
207
|
|
164
|
-
|
165
|
-
|
208
|
+
Returns True if newly mounted, False if it was already mounted or failed.
|
209
|
+
"""
|
210
|
+
if server_name in mounted_servers:
|
211
|
+
log.info(f"🔁 Server {server_name} already mounted")
|
212
|
+
return False
|
166
213
|
|
167
|
-
|
168
|
-
|
214
|
+
# Find server configuration
|
215
|
+
server_config: MCPServerConfig | None = next(
|
216
|
+
(s for s in Config().mcp_servers if s.name == server_name), None
|
217
|
+
)
|
169
218
|
|
170
|
-
|
171
|
-
|
172
|
-
|
219
|
+
if server_config is None:
|
220
|
+
log.error(f"❌ Server configuration not found: {server_name}")
|
221
|
+
return False
|
173
222
|
|
174
|
-
|
175
|
-
|
223
|
+
# Build minimal FastMCP backend config for just this server
|
224
|
+
fastmcp_config = self._convert_to_fastmcp_config([server_config])
|
225
|
+
if not fastmcp_config.get("mcpServers"):
|
226
|
+
log.error(f"❌ Invalid/empty MCP config for server: {server_name}")
|
227
|
+
return False
|
176
228
|
|
177
|
-
|
178
|
-
|
229
|
+
try:
|
230
|
+
oauth_manager = get_oauth_manager()
|
231
|
+
await self._mount_single_server(server_config, fastmcp_config, oauth_manager)
|
232
|
+
# Warm lists after mount
|
233
|
+
_ = await self._tool_manager.list_tools()
|
234
|
+
_ = await self._resource_manager.list_resources()
|
235
|
+
_ = await self._prompt_manager.list_prompts()
|
236
|
+
return True
|
237
|
+
except Exception as e: # noqa: BLE001
|
238
|
+
log.error(f"❌ Failed to mount server {server_name}: {e}")
|
179
239
|
return False
|
180
240
|
|
181
|
-
async def
|
241
|
+
async def unmount(self, server_name: str) -> bool:
|
182
242
|
"""
|
183
|
-
Unmount
|
243
|
+
Unmount a previously mounted server by name.
|
184
244
|
|
185
|
-
|
186
|
-
the entire composite proxy without the specified server.
|
245
|
+
Returns True if it was unmounted, False if it wasn't mounted.
|
187
246
|
"""
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
mounted = self.mounted_servers[server_name]
|
192
|
-
if mounted["config"].command == "echo":
|
193
|
-
# Test server - handle individually
|
194
|
-
await self._cleanup_mounted_server(server_name)
|
195
|
-
return True
|
196
|
-
|
197
|
-
# Real server in composite proxy - needs full rebuild
|
198
|
-
log.warning(f"Unmounting {server_name} requires rebuilding composite proxy")
|
199
|
-
return await self._rebuild_composite_proxy_without(server_name)
|
200
|
-
|
201
|
-
log.warning(f"Server {server_name} not found in mounted servers")
|
202
|
-
return False
|
203
|
-
|
204
|
-
except Exception as e:
|
205
|
-
log.error(f"❌ Failed to unmount MCP server {server_name}: {e}")
|
247
|
+
info = mounted_servers.pop(server_name, None)
|
248
|
+
if info is None:
|
249
|
+
log.info(f"ℹ️ Server {server_name} was not mounted")
|
206
250
|
return False
|
207
251
|
|
208
|
-
|
209
|
-
"""Rebuild the composite proxy without the specified server."""
|
210
|
-
try:
|
211
|
-
# Remove from mounted servers
|
212
|
-
await self._cleanup_mounted_server(excluded_server)
|
213
|
-
|
214
|
-
# Get remaining servers that should be in composite proxy
|
215
|
-
remaining_configs = [
|
216
|
-
mounted["config"]
|
217
|
-
for name, mounted in self.mounted_servers.items()
|
218
|
-
if mounted["config"].command != "echo" and name != excluded_server
|
219
|
-
]
|
252
|
+
proxy = info.get("proxy")
|
220
253
|
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
254
|
+
# Manually remove from FastMCP managers' mounted lists
|
255
|
+
for manager_name in ("_tool_manager", "_resource_manager", "_prompt_manager"):
|
256
|
+
manager = getattr(self, manager_name, None)
|
257
|
+
mounted_list = getattr(manager, "_mounted_servers", None)
|
258
|
+
if mounted_list is None:
|
259
|
+
continue
|
225
260
|
|
226
|
-
#
|
227
|
-
|
228
|
-
|
261
|
+
# Prefer removing by both prefix and object identity; fallback to prefix-only
|
262
|
+
new_list = [
|
263
|
+
m
|
264
|
+
for m in mounted_list
|
265
|
+
if not (m.prefix == server_name and (proxy is None or m.server is proxy))
|
266
|
+
]
|
267
|
+
if len(new_list) == len(mounted_list):
|
268
|
+
new_list = [m for m in mounted_list if m.prefix != server_name]
|
229
269
|
|
230
|
-
|
231
|
-
log.error(f"Failed to rebuild composite proxy: {e}")
|
232
|
-
return False
|
270
|
+
mounted_list[:] = new_list
|
233
271
|
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
log.info(f"✅ Unmounted MCP server: {server_name}")
|
272
|
+
# Invalidate and warm lists to ensure reload
|
273
|
+
_ = await self._tool_manager.list_tools()
|
274
|
+
_ = await self._resource_manager.list_resources()
|
275
|
+
_ = await self._prompt_manager.list_prompts()
|
239
276
|
|
240
|
-
|
241
|
-
|
242
|
-
return [
|
243
|
-
ServerStatusInfo(name=name, config=mounted["config"].__dict__, mounted=True)
|
244
|
-
for name, mounted in self.mounted_servers.items()
|
245
|
-
]
|
277
|
+
log.info(f"🧹 Unmounted server {server_name} and cleared references")
|
278
|
+
return True
|
246
279
|
|
247
|
-
async def initialize(self
|
280
|
+
async def initialize(self) -> None:
|
248
281
|
"""Initialize the FastMCP server using unified composite proxy approach."""
|
249
282
|
log.info("Initializing Single User MCP server with composite proxy")
|
250
|
-
|
251
|
-
log.debug(f"Available MCP servers in config: {[s.name for s in config_to_use.mcp_servers]}")
|
283
|
+
log.debug(f"Available MCP servers in config: {[s.name for s in Config().mcp_servers]}")
|
252
284
|
|
253
285
|
# Get all enabled servers
|
254
|
-
enabled_servers = [s for s in
|
286
|
+
enabled_servers = [s for s in Config().mcp_servers if s.enabled]
|
255
287
|
log.info(
|
256
288
|
f"Found {len(enabled_servers)} enabled servers: {[s.name for s in enabled_servers]}"
|
257
289
|
)
|
258
290
|
|
259
|
-
# Mount test servers individually (they don't go in composite proxy)
|
260
|
-
test_servers = [s for s in enabled_servers if s.command == "echo"]
|
261
|
-
for server_config in test_servers:
|
262
|
-
log.info(f"Mounting test server individually: {server_config.name}")
|
263
|
-
_ = await self._mount_test_server(server_config)
|
264
|
-
|
265
291
|
# Create composite proxy for all real servers
|
266
292
|
success = await self.create_composite_proxy(enabled_servers)
|
267
293
|
if not success:
|
@@ -298,8 +324,8 @@ class SingleUserMCP(FastMCP[Any]):
|
|
298
324
|
def _setup_demo_tools(self) -> None:
|
299
325
|
"""Set up built-in demo tools for testing."""
|
300
326
|
|
301
|
-
@self.tool()
|
302
|
-
def
|
327
|
+
@self.tool() # noqa
|
328
|
+
def builtin_echo(text: str) -> str:
|
303
329
|
"""
|
304
330
|
Echo back the provided text.
|
305
331
|
|
@@ -312,8 +338,8 @@ class SingleUserMCP(FastMCP[Any]):
|
|
312
338
|
log.info(f"🔊 Echo tool called with: {text}")
|
313
339
|
return f"Echo: {text}"
|
314
340
|
|
315
|
-
@self.tool()
|
316
|
-
def
|
341
|
+
@self.tool() # noqa
|
342
|
+
def builtin_get_server_info() -> dict[str, str | list[str] | int]:
|
317
343
|
"""
|
318
344
|
Get information about the Open Edison server.
|
319
345
|
|
@@ -323,13 +349,13 @@ class SingleUserMCP(FastMCP[Any]):
|
|
323
349
|
log.info("ℹ️ Server info tool called")
|
324
350
|
return {
|
325
351
|
"name": "Open Edison Single User",
|
326
|
-
"version":
|
327
|
-
"mounted_servers": list(
|
328
|
-
"total_mounted": len(
|
352
|
+
"version": Config().version,
|
353
|
+
"mounted_servers": list(mounted_servers.keys()),
|
354
|
+
"total_mounted": len(mounted_servers),
|
329
355
|
}
|
330
356
|
|
331
|
-
@self.tool()
|
332
|
-
def
|
357
|
+
@self.tool() # noqa
|
358
|
+
def builtin_get_security_status() -> dict[str, Any]:
|
333
359
|
"""
|
334
360
|
Get the current session's security status and data access summary.
|
335
361
|
|
@@ -353,18 +379,54 @@ class SingleUserMCP(FastMCP[Any]):
|
|
353
379
|
|
354
380
|
return security_data
|
355
381
|
|
356
|
-
|
382
|
+
@self.tool() # noqa
|
383
|
+
async def builtin_get_available_tools() -> list[str]:
|
384
|
+
"""
|
385
|
+
Get a list of all available tools. Use this tool to get an updated list of available tools.
|
386
|
+
"""
|
387
|
+
tool_list = await self._tool_manager.list_tools()
|
388
|
+
available_tools: list[str] = []
|
389
|
+
log.debug(f"Raw tool list: {tool_list}")
|
390
|
+
perms = Permissions()
|
391
|
+
for tool in tool_list:
|
392
|
+
# Use the prefixed key (e.g., "filesystem_read_file") to match flattened permissions
|
393
|
+
perm_key = tool.key
|
394
|
+
try:
|
395
|
+
is_enabled: bool = perms.is_tool_enabled(perm_key)
|
396
|
+
except PermissionsError:
|
397
|
+
# Unknown in permissions → treat as disabled
|
398
|
+
is_enabled = False
|
399
|
+
if is_enabled:
|
400
|
+
# Return the invocable name (key), which matches the MCP-exposed name
|
401
|
+
available_tools.append(tool.key)
|
402
|
+
return available_tools
|
403
|
+
|
404
|
+
@self.tool() # noqa
|
405
|
+
async def builtin_tools_changed(ctx: Context) -> str:
|
406
|
+
"""
|
407
|
+
Notify the MCP client that the tool list has changed. You should call this tool periodically
|
408
|
+
to ensure the client has the latest list of available tools.
|
409
|
+
"""
|
410
|
+
await ctx.send_tool_list_changed()
|
411
|
+
await ctx.send_resource_list_changed()
|
412
|
+
await ctx.send_prompt_list_changed()
|
413
|
+
|
414
|
+
return "Notifications sent"
|
415
|
+
|
416
|
+
log.info(
|
417
|
+
"✅ Added built-in demo tools: echo, get_server_info, get_security_status, builtin_get_available_tools, builtin_tools_changed"
|
418
|
+
)
|
357
419
|
|
358
420
|
def _setup_demo_resources(self) -> None:
|
359
421
|
"""Set up built-in demo resources for testing."""
|
360
422
|
|
361
|
-
@self.resource("config://app")
|
362
|
-
def
|
423
|
+
@self.resource("config://app") # noqa
|
424
|
+
def builtin_get_app_config() -> dict[str, Any]:
|
363
425
|
"""Get application configuration."""
|
364
426
|
return {
|
365
|
-
"version":
|
366
|
-
"mounted_servers": list(
|
367
|
-
"total_mounted": len(
|
427
|
+
"version": Config().version,
|
428
|
+
"mounted_servers": list(mounted_servers.keys()),
|
429
|
+
"total_mounted": len(mounted_servers),
|
368
430
|
}
|
369
431
|
|
370
432
|
log.info("✅ Added built-in demo resources: config://app")
|
@@ -372,8 +434,8 @@ class SingleUserMCP(FastMCP[Any]):
|
|
372
434
|
def _setup_demo_prompts(self) -> None:
|
373
435
|
"""Set up built-in demo prompts for testing."""
|
374
436
|
|
375
|
-
@self.prompt()
|
376
|
-
def
|
437
|
+
@self.prompt() # noqa
|
438
|
+
def builtin_summarize_text(text: str) -> str:
|
377
439
|
"""Create a prompt to summarize the given text."""
|
378
440
|
return f"""
|
379
441
|
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=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,,
|
File without changes
|
File without changes
|
File without changes
|