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.
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 MCPServerConfig, config
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
- super().__init__(name="open-edison-single-user")
47
- self.mounted_servers: dict[str, MountedServerInfo] = {}
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
- # Import the composite proxy into this main server
116
- # Tools and resources will be automatically namespaced by server name
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
- proxy = FastMCP.as_proxy(FastMCPClient(fastmcp_config))
125
- self.mount(proxy, prefix=server_name)
126
- self.mounted_servers[server_name] = MountedServerInfo(config=server_config, proxy=proxy)
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 ({self.mounted_servers.keys()})"
126
+ f"✅ Created composite proxy with {len(enabled_servers)} servers ({mounted_servers.keys()})"
130
127
  )
131
128
  return True
132
129
 
133
- async def _rebuild_composite_proxy_without(self, excluded_server: str) -> bool:
134
- """Rebuild the composite proxy without the specified server."""
135
- try:
136
- # Remove from mounted servers
137
- await self._cleanup_mounted_server(excluded_server)
138
-
139
- # Get remaining servers that should be in composite proxy
140
- remaining_configs = [
141
- mounted["config"]
142
- for name, mounted in self.mounted_servers.items()
143
- if mounted["config"].command != "echo" and name != excluded_server
144
- ]
145
-
146
- if not remaining_configs:
147
- log.info("No servers remaining for composite proxy")
148
- self.composite_proxy = None
149
- return True
150
-
151
- # Rebuild composite proxy with remaining servers
152
- log.info(f"Rebuilding composite proxy without {excluded_server}")
153
- return await self.create_composite_proxy(remaining_configs)
154
-
155
- except Exception as e:
156
- log.error(f"Failed to rebuild composite proxy: {e}")
157
- return False
158
-
159
- async def _cleanup_mounted_server(self, server_name: str) -> None:
160
- """Clean up mounted server resources."""
161
- # TODO not sure this is possible for the self object? i.e. there is no self.unmount
162
- if server_name in self.mounted_servers:
163
- del self.mounted_servers[server_name]
164
- log.info(f" Unmounted MCP server: {server_name}")
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 self.mounted_servers.items()
202
+ for name, mounted in mounted_servers.items()
171
203
  ]
172
204
 
173
- async def initialize(self, test_config: Any | None = None) -> None:
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
- config_to_use = test_config if test_config is not None else config
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 config_to_use.mcp_servers if s.enabled]
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
- # Mount test servers individually (they don't go in composite proxy)
186
- test_servers = [s for s in enabled_servers if s.command == "echo"]
187
- for server_config in test_servers:
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
- async def reinitialize(self, test_config: Any | None = None) -> dict[str, Any]:
200
- """
201
- Reinitialize all MCP servers by cleaning up existing ones and reloading config.
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 echo(text: str) -> str: # noqa: ARG001
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 get_server_info() -> dict[str, str | list[str] | int]: # noqa: ARG001
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": config.version,
327
- "mounted_servers": list(self.mounted_servers.keys()),
328
- "total_mounted": len(self.mounted_servers),
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 get_security_status() -> dict[str, Any]: # noqa: ARG001
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
- log.info("✅ Added built-in demo tools: echo, get_server_info, get_security_status")
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 get_app_config() -> dict[str, Any]: # noqa: ARG001
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": config.version,
366
- "mounted_servers": list(self.mounted_servers.keys()),
367
- "total_mounted": len(self.mounted_servers),
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 summarize_text(text: str) -> str:
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 TelemetryConfig, config, get_config_dir
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 = config.telemetry or TelemetryConfig()
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 (config.telemetry or TelemetryConfig())
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 = getattr(config, "version", "unknown")
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,,