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.
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 FastMCP
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 MCPServerConfig, config
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
- try:
111
- if not enabled_servers:
112
- log.info("No real servers to mount in composite proxy")
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
- # Convert to FastMCP config format
116
- fastmcp_config = self._convert_to_fastmcp_config(enabled_servers)
106
+ oauth_manager = get_oauth_manager()
117
107
 
118
- log.info(
119
- f"Creating composite proxy for servers: {list(fastmcp_config['mcpServers'].keys())}"
120
- )
108
+ for server_config in enabled_servers:
109
+ server_name = server_config.name
121
110
 
122
- # Create the composite proxy using FastMCP's multi-server support
123
- self.composite_proxy = FastMCP.as_proxy(
124
- backend=fastmcp_config, name="open-edison-composite-proxy"
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
- # Import the composite proxy into this main server
128
- # Tools and resources will be automatically namespaced by server name
129
- await self.import_server(self.composite_proxy)
124
+ log.info(
125
+ f"✅ Created composite proxy with {len(enabled_servers)} servers ({mounted_servers.keys()})"
126
+ )
127
+ return True
130
128
 
131
- # Track mounted servers for status reporting
132
- for server_config in enabled_servers:
133
- self.mounted_servers[server_config.name] = MountedServerInfo(
134
- config=server_config, proxy=self.composite_proxy
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
- log.info(f"✅ Created composite proxy with {len(enabled_servers)} servers")
138
- return True
181
+ # Create proxy from remote client
182
+ proxy = FastMCP.as_proxy(client)
139
183
 
140
- except Exception as e:
141
- log.error(f"❌ Failed to create composite proxy: {e}")
142
- return False
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
- async def mount_server(self, server_config: MCPServerConfig) -> bool:
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
- Args:
149
- server_config: Configuration for the server to mount
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
- Returns:
152
- True if mounting was successful, False otherwise
153
- """
154
- try:
155
- # Check if server is already mounted
156
- if server_config.name in self.mounted_servers:
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
- # Handle test servers separately
161
- if server_config.command == "echo":
162
- return await self._mount_test_server(server_config)
204
+ async def mount_server(self, server_name: str) -> bool:
205
+ """
206
+ Mount a server by name if not already mounted.
163
207
 
164
- # For real servers, we need to rebuild the composite proxy
165
- log.info(f"Mounting server {server_config.name} via composite proxy rebuild")
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
- # Get currently mounted servers and add the new one
168
- current_configs = [mounted["config"] for mounted in self.mounted_servers.values()]
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
- # Add the new server if not already there
171
- if server_config not in current_configs:
172
- current_configs.append(server_config)
219
+ if server_config is None:
220
+ log.error(f"❌ Server configuration not found: {server_name}")
221
+ return False
173
222
 
174
- # Rebuild composite proxy with new server list
175
- return await self.create_composite_proxy(current_configs)
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
- except Exception as e:
178
- log.error(f"❌ Failed to mount server {server_config.name}: {e}")
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 unmount_server(self, server_name: str) -> bool:
241
+ async def unmount(self, server_name: str) -> bool:
182
242
  """
183
- Unmount an MCP server and stop its subprocess.
243
+ Unmount a previously mounted server by name.
184
244
 
185
- NOTE: For servers in the composite proxy, this will require rebuilding
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
- try:
189
- # Check if this is a test server (individually mounted)
190
- if server_name in self.mounted_servers:
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
- async def _rebuild_composite_proxy_without(self, excluded_server: str) -> bool:
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
- if not remaining_configs:
222
- log.info("No servers remaining for composite proxy")
223
- self.composite_proxy = None
224
- return True
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
- # Rebuild composite proxy with remaining servers
227
- log.info(f"Rebuilding composite proxy without {excluded_server}")
228
- return await self.create_composite_proxy(remaining_configs)
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
- except Exception as e:
231
- log.error(f"Failed to rebuild composite proxy: {e}")
232
- return False
270
+ mounted_list[:] = new_list
233
271
 
234
- async def _cleanup_mounted_server(self, server_name: str) -> None:
235
- """Clean up mounted server resources."""
236
- if server_name in self.mounted_servers:
237
- del self.mounted_servers[server_name]
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
- async def get_mounted_servers(self) -> list[ServerStatusInfo]:
241
- """Get list of currently mounted servers."""
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, test_config: Any | None = None) -> None:
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
- config_to_use = test_config if test_config is not None else config
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 config_to_use.mcp_servers if s.enabled]
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 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=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,,