open-edison 0.1.19__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.
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.
@@ -44,8 +50,6 @@ class SingleUserMCP(FastMCP[Any]):
44
50
 
45
51
  def __init__(self):
46
52
  super().__init__(name="open-edison-single-user")
47
- self.mounted_servers: dict[str, MountedServerInfo] = {}
48
- self.composite_proxy: FastMCP[Any] | None = None
49
53
 
50
54
  # Add session tracking middleware for data access monitoring
51
55
  self.add_middleware(SessionTrackingMiddleware())
@@ -68,10 +72,6 @@ class SingleUserMCP(FastMCP[Any]):
68
72
  mcp_servers: dict[str, dict[str, Any]] = {}
69
73
 
70
74
  for server_config in enabled_servers:
71
- # Skip test servers for composite proxy
72
- if server_config.command == "echo":
73
- continue
74
-
75
75
  server_entry: dict[str, Any] = {
76
76
  "command": server_config.command,
77
77
  "args": server_config.args,
@@ -86,15 +86,6 @@ class SingleUserMCP(FastMCP[Any]):
86
86
 
87
87
  return {"mcpServers": mcp_servers}
88
88
 
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
89
  async def create_composite_proxy(self, enabled_servers: list[MCPServerConfig]) -> bool:
99
90
  """
100
91
  Create a unified composite proxy for all enabled MCP servers.
@@ -112,82 +103,191 @@ class SingleUserMCP(FastMCP[Any]):
112
103
  log.info("No real servers to mount in composite proxy")
113
104
  return True
114
105
 
115
- # Import the composite proxy into this main server
116
- # Tools and resources will be automatically namespaced by server name
106
+ oauth_manager = get_oauth_manager()
107
+
117
108
  for server_config in enabled_servers:
118
109
  server_name = server_config.name
110
+
119
111
  # Skip if this server would produce an empty config (e.g., misconfigured)
120
112
  fastmcp_config = self._convert_to_fastmcp_config([server_config])
121
113
  if not fastmcp_config.get("mcpServers"):
122
114
  log.warning(f"Skipping server '{server_name}' due to empty MCP config")
123
115
  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)
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
127
123
 
128
124
  log.info(
129
- f"✅ Created composite proxy with {len(enabled_servers)} servers ({self.mounted_servers.keys()})"
125
+ f"✅ Created composite proxy with {len(enabled_servers)} servers ({mounted_servers.keys()})"
130
126
  )
131
127
  return True
132
128
 
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}")
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,
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}")
180
+
181
+ # Create proxy from remote client
182
+ proxy = FastMCP.as_proxy(client)
183
+
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)
188
+
189
+ super().mount(proxy, prefix=server_name)
190
+ mounted_servers[server_name] = MountedServerInfo(config=server_config, proxy=proxy)
191
+
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
+ )
165
196
 
166
197
  async def get_mounted_servers(self) -> list[ServerStatusInfo]:
167
198
  """Get list of currently mounted servers."""
168
199
  return [
169
200
  ServerStatusInfo(name=name, config=mounted["config"].__dict__, mounted=True)
170
- for name, mounted in self.mounted_servers.items()
201
+ for name, mounted in mounted_servers.items()
171
202
  ]
172
203
 
173
- async def initialize(self, test_config: Any | None = None) -> None:
204
+ async def mount_server(self, server_name: str) -> bool:
205
+ """
206
+ Mount a server by name if not already mounted.
207
+
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
213
+
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
+ )
218
+
219
+ if server_config is None:
220
+ log.error(f"❌ Server configuration not found: {server_name}")
221
+ return False
222
+
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
228
+
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}")
239
+ return False
240
+
241
+ async def unmount(self, server_name: str) -> bool:
242
+ """
243
+ Unmount a previously mounted server by name.
244
+
245
+ Returns True if it was unmounted, False if it wasn't mounted.
246
+ """
247
+ info = mounted_servers.pop(server_name, None)
248
+ if info is None:
249
+ log.info(f"ℹ️ Server {server_name} was not mounted")
250
+ return False
251
+
252
+ proxy = info.get("proxy")
253
+
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
260
+
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]
269
+
270
+ mounted_list[:] = new_list
271
+
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()
276
+
277
+ log.info(f"🧹 Unmounted server {server_name} and cleared references")
278
+ return True
279
+
280
+ async def initialize(self) -> None:
174
281
  """Initialize the FastMCP server using unified composite proxy approach."""
175
282
  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]}")
283
+ log.debug(f"Available MCP servers in config: {[s.name for s in Config().mcp_servers]}")
178
284
 
179
285
  # Get all enabled servers
180
- enabled_servers = [s for s in config_to_use.mcp_servers if s.enabled]
286
+ enabled_servers = [s for s in Config().mcp_servers if s.enabled]
181
287
  log.info(
182
288
  f"Found {len(enabled_servers)} enabled servers: {[s.name for s in enabled_servers]}"
183
289
  )
184
290
 
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)
190
-
191
291
  # Create composite proxy for all real servers
192
292
  success = await self.create_composite_proxy(enabled_servers)
193
293
  if not success:
@@ -196,80 +296,6 @@ class SingleUserMCP(FastMCP[Any]):
196
296
 
197
297
  log.info("✅ Single User MCP server initialized with composite proxy")
198
298
 
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
- }
272
-
273
299
  def _calculate_risk_level(self, trifecta: dict[str, bool]) -> str:
274
300
  """
275
301
  Calculate a human-readable risk level based on trifecta flags.
@@ -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 echo(text: str) -> str: # noqa: ARG001
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 get_server_info() -> dict[str, str | list[str] | int]: # noqa: ARG001
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": config.version,
327
- "mounted_servers": list(self.mounted_servers.keys()),
328
- "total_mounted": len(self.mounted_servers),
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 get_security_status() -> dict[str, Any]: # noqa: ARG001
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
- log.info("✅ Added built-in demo tools: echo, get_server_info, get_security_status")
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 get_app_config() -> dict[str, Any]: # noqa: ARG001
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": config.version,
366
- "mounted_servers": list(self.mounted_servers.keys()),
367
- "total_mounted": len(self.mounted_servers),
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 summarize_text(text: str) -> str:
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 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,,