open-edison 0.1.44__py3-none-any.whl → 0.1.72rc1__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.44.dist-info → open_edison-0.1.72rc1.dist-info}/METADATA +2 -21
- open_edison-0.1.72rc1.dist-info/RECORD +41 -0
- src/cli.py +30 -113
- src/config.py +30 -9
- src/events.py +5 -2
- src/frontend_dist/assets/index-D05VN_1l.css +1 -0
- src/frontend_dist/assets/index-D6ziuTsl.js +51 -0
- src/frontend_dist/index.html +2 -2
- src/frontend_dist/sw.js +22 -2
- src/mcp_importer/__main__.py +0 -2
- src/mcp_importer/api.py +254 -44
- src/mcp_importer/export_cli.py +2 -2
- src/mcp_importer/exporters.py +1 -1
- src/mcp_importer/import_api.py +0 -2
- src/mcp_importer/parsers.py +47 -9
- src/mcp_importer/quick_cli.py +0 -2
- src/mcp_importer/types.py +0 -2
- src/mcp_stdio_capture.py +144 -0
- src/middleware/data_access_tracker.py +49 -4
- src/middleware/session_tracking.py +123 -34
- src/oauth_manager.py +5 -3
- src/oauth_override.py +10 -0
- src/permissions.py +110 -10
- src/server.py +57 -16
- src/setup_tui/main.py +160 -21
- src/single_user_mcp.py +246 -105
- src/tools/io.py +35 -0
- src/vulture_whitelist.py +3 -0
- open_edison-0.1.44.dist-info/RECORD +0 -37
- src/frontend_dist/assets/index-BUUcUfTt.js +0 -51
- src/frontend_dist/assets/index-o6_8mdM8.css +0 -1
- {open_edison-0.1.44.dist-info → open_edison-0.1.72rc1.dist-info}/WHEEL +0 -0
- {open_edison-0.1.44.dist-info → open_edison-0.1.72rc1.dist-info}/entry_points.txt +0 -0
- {open_edison-0.1.44.dist-info → open_edison-0.1.72rc1.dist-info}/licenses/LICENSE +0 -0
src/single_user_mcp.py
CHANGED
@@ -5,11 +5,21 @@ FastMCP instance for the single-user Open Edison setup.
|
|
5
5
|
Handles MCP protocol communication with running servers using a unified composite proxy.
|
6
6
|
"""
|
7
7
|
|
8
|
+
import asyncio
|
9
|
+
import time
|
8
10
|
from typing import Any, TypedDict
|
9
11
|
|
10
12
|
from fastmcp import Client as FastMCPClient
|
11
13
|
from fastmcp import Context, FastMCP
|
14
|
+
from fastmcp.server.server import add_resource_prefix, has_resource_prefix
|
15
|
+
|
16
|
+
# Low level FastMCP imports
|
17
|
+
from fastmcp.tools.tool import Tool
|
18
|
+
from fastmcp.tools.tool_transform import (
|
19
|
+
apply_transformations_to_tools,
|
20
|
+
)
|
12
21
|
from loguru import logger as log
|
22
|
+
from mcp.server.lowlevel.server import LifespanResultT
|
13
23
|
|
14
24
|
from src.config import Config, MCPServerConfig
|
15
25
|
from src.middleware.session_tracking import (
|
@@ -60,6 +70,114 @@ class SingleUserMCP(FastMCP[Any]):
|
|
60
70
|
self._setup_demo_resources()
|
61
71
|
self._setup_demo_prompts()
|
62
72
|
|
73
|
+
async def import_server(
|
74
|
+
self,
|
75
|
+
server: FastMCP[LifespanResultT],
|
76
|
+
prefix: str | None = None,
|
77
|
+
tool_separator: str | None = None,
|
78
|
+
resource_separator: str | None = None,
|
79
|
+
prompt_separator: str | None = None,
|
80
|
+
) -> None:
|
81
|
+
"""
|
82
|
+
Import the MCP objects from another FastMCP server into this one with a given prefix.
|
83
|
+
Overloads FastMCP's import_server method to improve performance.
|
84
|
+
|
85
|
+
Args:
|
86
|
+
server: The FastMCP server to import
|
87
|
+
prefix: prefix to use for the imported server's objects. If None,
|
88
|
+
objects are imported with their original names.
|
89
|
+
tool_separator: Deprecated. Required to be None.
|
90
|
+
resource_separator: Deprecated. Required to be None.
|
91
|
+
prompt_separator: Deprecated. Required to be None.
|
92
|
+
"""
|
93
|
+
|
94
|
+
if prefix is None:
|
95
|
+
raise ValueError("Prefix is required")
|
96
|
+
|
97
|
+
if tool_separator is not None:
|
98
|
+
raise ValueError("Tool separator is deprecated and not supported")
|
99
|
+
|
100
|
+
if resource_separator is not None:
|
101
|
+
raise ValueError("Resource separator is deprecated and not supported")
|
102
|
+
|
103
|
+
if prompt_separator is not None:
|
104
|
+
raise ValueError("Prompt separator is deprecated and not supported")
|
105
|
+
|
106
|
+
log.debug(f"🔧 Importing server {prefix} ({server.name}) into single user MCP'")
|
107
|
+
|
108
|
+
# Fetch all server objects in parallel
|
109
|
+
tools, resources, templates, prompts = await asyncio.gather(
|
110
|
+
server.get_tools(),
|
111
|
+
server.get_resources(),
|
112
|
+
server.get_resource_templates(),
|
113
|
+
server.get_prompts(),
|
114
|
+
return_exceptions=True,
|
115
|
+
)
|
116
|
+
|
117
|
+
# Validate and normalize all results
|
118
|
+
tools = self._validate_server_result(tools, "tools", server.name)
|
119
|
+
resources = self._validate_server_result(resources, "resources", server.name)
|
120
|
+
templates = self._validate_server_result(templates, "templates", server.name)
|
121
|
+
prompts = self._validate_server_result(prompts, "prompts", server.name)
|
122
|
+
|
123
|
+
# Import all components
|
124
|
+
self._import_tools(tools, prefix)
|
125
|
+
self._import_resources(resources, prefix)
|
126
|
+
self._import_templates(templates, prefix)
|
127
|
+
self._import_prompts(prompts, prefix)
|
128
|
+
|
129
|
+
log.debug(f"Imported server {server.name} with prefix '{prefix}'")
|
130
|
+
|
131
|
+
def _validate_server_result(
|
132
|
+
self, result: Any, result_type: str, server_name: str
|
133
|
+
) -> dict[str, Any]:
|
134
|
+
"""Validate and normalize server result from asyncio.gather with return_exceptions=True."""
|
135
|
+
if isinstance(result, Exception):
|
136
|
+
log.warning(f'Server {server_name} does not appear to contain "{result_type}"')
|
137
|
+
log.debug(f"Server {server_name} _validate_server_result exception result: {result}")
|
138
|
+
return {}
|
139
|
+
if not isinstance(result, dict):
|
140
|
+
log.warning(f"Server {server_name} returned an unexpected response")
|
141
|
+
log.debug(
|
142
|
+
f"Server {server_name} _validate_server_result unexpected type {type(result)} with value: {result}"
|
143
|
+
)
|
144
|
+
return {}
|
145
|
+
return result # type: ignore[return-value]
|
146
|
+
|
147
|
+
def _import_tools(self, tools: dict[str, Any], prefix: str) -> None:
|
148
|
+
"""Import tools from server"""
|
149
|
+
for key, tool in tools.items():
|
150
|
+
if prefix:
|
151
|
+
tool = tool.model_copy(key=f"{prefix}_{key}")
|
152
|
+
self._tool_manager.add_tool(tool)
|
153
|
+
|
154
|
+
def _import_resources(self, resources: dict[str, Any], prefix: str) -> None:
|
155
|
+
"""Import resources from server"""
|
156
|
+
for key, resource in resources.items():
|
157
|
+
if prefix:
|
158
|
+
resource_key = add_resource_prefix(key, prefix, self.resource_prefix_format)
|
159
|
+
resource = resource.model_copy(
|
160
|
+
update={"name": f"{prefix}_{resource.name}"}, key=resource_key
|
161
|
+
)
|
162
|
+
self._resource_manager.add_resource(resource)
|
163
|
+
|
164
|
+
def _import_templates(self, templates: dict[str, Any], prefix: str) -> None:
|
165
|
+
"""Import templates from server"""
|
166
|
+
for key, template in templates.items():
|
167
|
+
if prefix:
|
168
|
+
template_key = add_resource_prefix(key, prefix, self.resource_prefix_format)
|
169
|
+
template = template.model_copy(
|
170
|
+
update={"name": f"{prefix}_{template.name}"}, key=template_key
|
171
|
+
)
|
172
|
+
self._resource_manager.add_template(template)
|
173
|
+
|
174
|
+
def _import_prompts(self, prompts: dict[str, Any], prefix: str) -> None:
|
175
|
+
"""Import prompts from server"""
|
176
|
+
for key, prompt in prompts.items():
|
177
|
+
if prefix:
|
178
|
+
prompt = prompt.model_copy(key=f"{prefix}_{key}")
|
179
|
+
self._prompt_manager.add_prompt(prompt)
|
180
|
+
|
63
181
|
def _convert_to_fastmcp_config(self, enabled_servers: list[MCPServerConfig]) -> dict[str, Any]:
|
64
182
|
"""
|
65
183
|
Convert Open Edison config format to FastMCP MCPConfig format.
|
@@ -87,46 +205,6 @@ class SingleUserMCP(FastMCP[Any]):
|
|
87
205
|
|
88
206
|
return {"mcpServers": mcp_servers}
|
89
207
|
|
90
|
-
async def create_composite_proxy(self, enabled_servers: list[MCPServerConfig]) -> bool:
|
91
|
-
"""
|
92
|
-
Create a unified composite proxy for all enabled MCP servers.
|
93
|
-
|
94
|
-
This replaces individual server mounting with a single FastMCP composite proxy
|
95
|
-
that handles all configured servers with automatic namespacing.
|
96
|
-
|
97
|
-
Args:
|
98
|
-
enabled_servers: List of enabled MCP server configurations
|
99
|
-
|
100
|
-
Returns:
|
101
|
-
True if composite proxy was created successfully, False otherwise
|
102
|
-
"""
|
103
|
-
if not enabled_servers:
|
104
|
-
log.info("No real servers to mount in composite proxy")
|
105
|
-
return True
|
106
|
-
|
107
|
-
oauth_manager = get_oauth_manager()
|
108
|
-
|
109
|
-
for server_config in enabled_servers:
|
110
|
-
server_name = server_config.name
|
111
|
-
|
112
|
-
# Skip if this server would produce an empty config (e.g., misconfigured)
|
113
|
-
fastmcp_config = self._convert_to_fastmcp_config([server_config])
|
114
|
-
if not fastmcp_config.get("mcpServers"):
|
115
|
-
log.warning(f"Skipping server '{server_name}' due to empty MCP config")
|
116
|
-
continue
|
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
|
124
|
-
|
125
|
-
log.info(
|
126
|
-
f"✅ Created composite proxy with {len(enabled_servers)} servers ({mounted_servers.keys()})"
|
127
|
-
)
|
128
|
-
return True
|
129
|
-
|
130
208
|
async def _mount_single_server(
|
131
209
|
self,
|
132
210
|
server_config: MCPServerConfig,
|
@@ -140,6 +218,7 @@ class SingleUserMCP(FastMCP[Any]):
|
|
140
218
|
remote_url = server_config.get_remote_url()
|
141
219
|
oauth_info = await oauth_manager.check_oauth_requirement(server_name, remote_url)
|
142
220
|
|
221
|
+
client_timeout = 10
|
143
222
|
# Create proxy based on server type to avoid union type issues
|
144
223
|
if server_config.is_remote_server():
|
145
224
|
# Handle remote servers (with or without OAuth)
|
@@ -156,18 +235,22 @@ class SingleUserMCP(FastMCP[Any]):
|
|
156
235
|
server_config.oauth_client_name,
|
157
236
|
)
|
158
237
|
if oauth_auth:
|
159
|
-
client = FastMCPClient(
|
238
|
+
client = FastMCPClient(
|
239
|
+
remote_url,
|
240
|
+
auth=oauth_auth,
|
241
|
+
timeout=client_timeout,
|
242
|
+
)
|
160
243
|
log.info(
|
161
244
|
f"🔐 Created remote client with OAuth authentication for {server_name}"
|
162
245
|
)
|
163
246
|
else:
|
164
|
-
client = FastMCPClient(remote_url)
|
247
|
+
client = FastMCPClient(remote_url, timeout=client_timeout)
|
165
248
|
log.warning(
|
166
249
|
f"⚠️ OAuth auth creation failed, using unauthenticated client for {server_name}"
|
167
250
|
)
|
168
251
|
else:
|
169
252
|
# Remote server without OAuth or needs auth
|
170
|
-
client = FastMCPClient(remote_url)
|
253
|
+
client = FastMCPClient(remote_url, timeout=client_timeout)
|
171
254
|
log.info(f"🌐 Created remote client for {server_name}")
|
172
255
|
|
173
256
|
# Log OAuth status warnings
|
@@ -184,10 +267,12 @@ class SingleUserMCP(FastMCP[Any]):
|
|
184
267
|
|
185
268
|
else:
|
186
269
|
# Local server - create proxy directly from config (avoids union type issue)
|
187
|
-
log.
|
270
|
+
log.debug(f"🔧 Creating local process proxy for {server_name}")
|
188
271
|
proxy = FastMCP.as_proxy(fastmcp_config)
|
189
272
|
|
190
|
-
|
273
|
+
log.debug(f"🔧 Importing server {server_name} into single user MCP")
|
274
|
+
await self.import_server(proxy, prefix=server_name)
|
275
|
+
# await super().import_server(proxy, prefix=server_name)
|
191
276
|
mounted_servers[server_name] = MountedServerInfo(config=server_config, proxy=proxy)
|
192
277
|
|
193
278
|
server_type = "remote" if server_config.is_remote_server() else "local"
|
@@ -230,10 +315,7 @@ class SingleUserMCP(FastMCP[Any]):
|
|
230
315
|
try:
|
231
316
|
oauth_manager = get_oauth_manager()
|
232
317
|
await self._mount_single_server(server_config, fastmcp_config, oauth_manager)
|
233
|
-
|
234
|
-
_ = await self._tool_manager.list_tools()
|
235
|
-
_ = await self._resource_manager.list_resources()
|
236
|
-
_ = await self._prompt_manager.list_prompts()
|
318
|
+
|
237
319
|
return True
|
238
320
|
except Exception as e: # noqa: BLE001
|
239
321
|
log.error(f"❌ Failed to mount server {server_name}: {e}")
|
@@ -242,7 +324,6 @@ class SingleUserMCP(FastMCP[Any]):
|
|
242
324
|
async def unmount(self, server_name: str) -> bool:
|
243
325
|
"""
|
244
326
|
Unmount a previously mounted server by name.
|
245
|
-
|
246
327
|
Returns True if it was unmounted, False if it wasn't mounted.
|
247
328
|
"""
|
248
329
|
info = mounted_servers.pop(server_name, None)
|
@@ -250,85 +331,145 @@ class SingleUserMCP(FastMCP[Any]):
|
|
250
331
|
log.info(f"ℹ️ Server {server_name} was not mounted")
|
251
332
|
return False
|
252
333
|
|
253
|
-
|
254
|
-
|
255
|
-
# Manually remove from FastMCP managers' mounted lists
|
334
|
+
# Remove the server from mounted_servers lists in all managers
|
256
335
|
for manager_name in ("_tool_manager", "_resource_manager", "_prompt_manager"):
|
257
336
|
manager = getattr(self, manager_name, None)
|
337
|
+
if manager is None:
|
338
|
+
continue
|
258
339
|
mounted_list = getattr(manager, "_mounted_servers", None)
|
259
340
|
if mounted_list is None:
|
260
341
|
continue
|
261
342
|
|
262
|
-
#
|
263
|
-
|
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]
|
343
|
+
# Remove servers with matching prefix
|
344
|
+
mounted_list[:] = [m for m in mounted_list if m.prefix != server_name]
|
270
345
|
|
271
|
-
|
346
|
+
# Remove tools with matching prefix (server name)
|
347
|
+
self._tool_manager._tools = { # type: ignore
|
348
|
+
key: value
|
349
|
+
for key, value in self._tool_manager._tools.items() # type: ignore
|
350
|
+
if not key.startswith(f"{server_name}_")
|
351
|
+
}
|
272
352
|
|
273
|
-
#
|
274
|
-
|
275
|
-
|
276
|
-
|
353
|
+
# Remove transformations with matching prefix (server name)
|
354
|
+
self._tool_manager.transformations = { # type: ignore
|
355
|
+
key: value
|
356
|
+
for key, value in self._tool_manager.transformations.items() # type: ignore
|
357
|
+
if not key.startswith(f"{server_name}_")
|
358
|
+
}
|
359
|
+
|
360
|
+
# Remove resources with matching prefix (server name)
|
361
|
+
self._resource_manager._resources = { # type: ignore
|
362
|
+
key: value
|
363
|
+
for key, value in self._resource_manager._resources.items() # type: ignore
|
364
|
+
if not has_resource_prefix(key, server_name, self.resource_prefix_format) # type: ignore
|
365
|
+
}
|
366
|
+
|
367
|
+
# Remove templates with matching prefix (server name)
|
368
|
+
self._resource_manager._templates = { # type: ignore
|
369
|
+
key: value
|
370
|
+
for key, value in self._resource_manager._templates.items() # type: ignore
|
371
|
+
if not has_resource_prefix(key, server_name, self.resource_prefix_format) # type: ignore
|
372
|
+
}
|
373
|
+
|
374
|
+
# Remove prompts with matching prefix (server name)
|
375
|
+
self._prompt_manager._prompts = { # type: ignore
|
376
|
+
key: value
|
377
|
+
for key, value in self._prompt_manager._prompts.items() # type: ignore
|
378
|
+
if not key.startswith(f"{server_name}_")
|
379
|
+
}
|
277
380
|
|
278
381
|
log.info(f"🧹 Unmounted server {server_name} and cleared references")
|
279
382
|
return True
|
280
383
|
|
281
|
-
async def
|
282
|
-
"""
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
384
|
+
async def list_all_servers_tools_parallel(self) -> list[Tool]:
|
385
|
+
"""Reload all servers' tools in parallel.
|
386
|
+
Reimplements FastMCP's ToolManager._list_tools method with parallel execution.
|
387
|
+
"""
|
388
|
+
|
389
|
+
# Execute all server reloads in parallel
|
390
|
+
list_tasks = [
|
391
|
+
server.server._list_tools()
|
392
|
+
for server in self._tool_manager._mounted_servers # type: ignore
|
393
|
+
]
|
394
|
+
|
395
|
+
log.debug(f"Starting reload for {len(list_tasks)} servers' tools in parallel")
|
396
|
+
start_time = time.perf_counter()
|
397
|
+
all_tools: dict[str, Tool] = {}
|
398
|
+
if list_tasks:
|
399
|
+
# Use return_exceptions=True to prevent one failing server from breaking everything
|
400
|
+
tools_lists = await asyncio.gather(*list_tasks, return_exceptions=True)
|
401
|
+
for server, tools_result in zip(
|
402
|
+
self._tool_manager._mounted_servers, # type: ignore
|
403
|
+
tools_lists,
|
404
|
+
strict=False,
|
405
|
+
):
|
406
|
+
if isinstance(tools_result, Exception):
|
407
|
+
log.warning(f"Failed to get tools from server {server.prefix}: {tools_result}")
|
408
|
+
continue
|
409
|
+
|
410
|
+
tools_list = tools_result
|
411
|
+
if not tools_list or not isinstance(tools_list, list):
|
412
|
+
continue
|
413
|
+
|
414
|
+
tools_dict = {t.key: t for t in tools_list} # type: ignore
|
415
|
+
if server.prefix:
|
416
|
+
for tool in tools_dict.values():
|
417
|
+
prefixed_tool = tool.model_copy( # type: ignore
|
418
|
+
key=f"{server.prefix}_{tool.key}" # type: ignore
|
419
|
+
)
|
420
|
+
all_tools[prefixed_tool.key] = prefixed_tool # type: ignore
|
421
|
+
else:
|
422
|
+
all_tools.update(tools_dict) # type: ignore
|
423
|
+
log.debug(
|
424
|
+
f"Saved {len(all_tools)} tools from {len([r for r in tools_lists if not isinstance(r, Exception)])} servers"
|
425
|
+
)
|
426
|
+
else:
|
427
|
+
all_tools = {}
|
428
|
+
|
429
|
+
# Add local tools
|
430
|
+
all_tools.update(self._tool_manager._tools) # type: ignore
|
431
|
+
|
432
|
+
transformed_tools = apply_transformations_to_tools(
|
433
|
+
tools=all_tools,
|
434
|
+
transformations=self._tool_manager.transformations,
|
435
|
+
)
|
436
|
+
|
437
|
+
final_tools_list = list(transformed_tools.values())
|
438
|
+
|
439
|
+
end_time = time.perf_counter()
|
440
|
+
log.debug(f"Time taken to reload all servers' tools: {end_time - start_time:.1f} seconds")
|
441
|
+
return final_tools_list
|
301
442
|
|
302
443
|
async def initialize(self) -> None:
|
303
444
|
"""Initialize the FastMCP server using unified composite proxy approach."""
|
304
445
|
log.info("Initializing Single User MCP server with composite proxy")
|
305
446
|
log.debug(f"Available MCP servers in config: {[s.name for s in Config().mcp_servers]}")
|
306
|
-
|
447
|
+
start_time = time.perf_counter()
|
307
448
|
# Get all enabled servers
|
308
449
|
enabled_servers = [s for s in Config().mcp_servers if s.enabled]
|
309
450
|
log.info(
|
310
451
|
f"Found {len(enabled_servers)} enabled servers: {[s.name for s in enabled_servers]}"
|
311
452
|
)
|
312
453
|
|
313
|
-
#
|
314
|
-
for
|
315
|
-
|
454
|
+
# Figure out which servers are to be unmounted
|
455
|
+
enabled_server_names = {s.name for s in enabled_servers}
|
456
|
+
servers_to_unmount = [s for s in mounted_servers if s not in enabled_server_names]
|
316
457
|
|
317
|
-
#
|
318
|
-
|
319
|
-
if not success:
|
320
|
-
log.error("Failed to create composite proxy")
|
321
|
-
return
|
458
|
+
# Figure out which servers are to be mounted
|
459
|
+
servers_to_mount = [s.name for s in enabled_servers if s.name not in mounted_servers]
|
322
460
|
|
323
|
-
|
461
|
+
# Unmount those servers (quick)
|
462
|
+
for server_name in servers_to_unmount:
|
463
|
+
await self.unmount(server_name)
|
324
464
|
|
325
|
-
#
|
326
|
-
|
327
|
-
|
328
|
-
_ = await self._prompt_manager.list_prompts()
|
465
|
+
# Mount those servers (async gathered bc import does network roundtrip)
|
466
|
+
mount_tasks = [self.mount_server(server_name) for server_name in servers_to_mount]
|
467
|
+
await asyncio.gather(*mount_tasks)
|
329
468
|
|
330
|
-
|
331
|
-
|
469
|
+
log.info("✅ Single User MCP server initialized with composite proxy")
|
470
|
+
log.debug(
|
471
|
+
f"Time taken to initialize Single User MCP server: {time.perf_counter() - start_time:.1f} seconds"
|
472
|
+
)
|
332
473
|
|
333
474
|
def _calculate_risk_level(self, trifecta: dict[str, bool]) -> str:
|
334
475
|
"""
|
@@ -418,7 +559,7 @@ class SingleUserMCP(FastMCP[Any]):
|
|
418
559
|
"""
|
419
560
|
Get a list of all available tools. Use this tool to get an updated list of available tools.
|
420
561
|
"""
|
421
|
-
tool_list = await self.
|
562
|
+
tool_list = await self.list_all_servers_tools_parallel()
|
422
563
|
available_tools: list[str] = []
|
423
564
|
log.trace(f"Raw tool list: {tool_list}")
|
424
565
|
perms = Permissions()
|
@@ -454,7 +595,7 @@ class SingleUserMCP(FastMCP[Any]):
|
|
454
595
|
def _setup_demo_resources(self) -> None:
|
455
596
|
"""Set up built-in demo resources for testing."""
|
456
597
|
|
457
|
-
@self.resource("
|
598
|
+
@self.resource("info://builtin/app") # noqa
|
458
599
|
def builtin_get_app_config() -> dict[str, Any]:
|
459
600
|
"""Get application configuration."""
|
460
601
|
return {
|
@@ -463,7 +604,7 @@ class SingleUserMCP(FastMCP[Any]):
|
|
463
604
|
"total_mounted": len(mounted_servers),
|
464
605
|
}
|
465
606
|
|
466
|
-
log.info("✅ Added built-in demo resources:
|
607
|
+
log.info("✅ Added built-in demo resources: info://builtin/app")
|
467
608
|
|
468
609
|
def _setup_demo_prompts(self) -> None:
|
469
610
|
"""Set up built-in demo prompts for testing."""
|
src/tools/io.py
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
import os
|
2
|
+
from collections.abc import Iterator
|
3
|
+
from contextlib import contextmanager
|
4
|
+
|
5
|
+
|
6
|
+
@contextmanager
|
7
|
+
def suppress_fds(*, suppress_stdout: bool = False, suppress_stderr: bool = True) -> Iterator[None]:
|
8
|
+
"""Temporarily redirect process-level stdout/stderr to os.devnull.
|
9
|
+
|
10
|
+
Args:
|
11
|
+
suppress_stdout: If True, redirect fd 1 to devnull
|
12
|
+
suppress_stderr: If True, redirect fd 2 to devnull
|
13
|
+
|
14
|
+
Yields:
|
15
|
+
None
|
16
|
+
"""
|
17
|
+
saved: list[tuple[int, int]] = []
|
18
|
+
try:
|
19
|
+
if suppress_stdout:
|
20
|
+
saved.append((1, os.dup(1)))
|
21
|
+
devnull_out = os.open(os.devnull, os.O_WRONLY)
|
22
|
+
os.dup2(devnull_out, 1)
|
23
|
+
os.close(devnull_out)
|
24
|
+
if suppress_stderr:
|
25
|
+
saved.append((2, os.dup(2)))
|
26
|
+
devnull_err = os.open(os.devnull, os.O_WRONLY)
|
27
|
+
os.dup2(devnull_err, 2)
|
28
|
+
os.close(devnull_err)
|
29
|
+
yield
|
30
|
+
finally:
|
31
|
+
for fd, backup in saved:
|
32
|
+
try:
|
33
|
+
os.dup2(backup, fd)
|
34
|
+
finally:
|
35
|
+
os.close(backup)
|
src/vulture_whitelist.py
ADDED
@@ -1,37 +0,0 @@
|
|
1
|
-
src/__init__.py,sha256=bEYMwBiuW9jzF07iWhas4Vb30EcpnqfpNfz_Q6yO1jU,209
|
2
|
-
src/__main__.py,sha256=kQsaVyzRa_ESC57JpKDSQJAHExuXme0rM5beJsYxFeA,161
|
3
|
-
src/cli.py,sha256=fqX-HuRDePRasexpnURQ_pVYeycJuWxllMcwfqDxMQw,8490
|
4
|
-
src/config.py,sha256=RSsAYzl8cj6eaDN1RORMcfKKWBcp4bKTQp2BdhAL9mg,10258
|
5
|
-
src/config.pyi,sha256=FgehEGli8ZXSjGlANBgMGv5497q4XskQciOc1fUcxqM,2033
|
6
|
-
src/events.py,sha256=aFQrVXDIZwt55Dz6OtyoXu2yi9evqo-8jZzo3CR2Tto,4965
|
7
|
-
src/oauth_manager.py,sha256=W9QSo0vfGDQ_i-QWCngkv7YLSL3Rk5jfPmqjU1J2rnU,9911
|
8
|
-
src/permissions.py,sha256=NGAnlG_z59HEiVA-k3cYvwmmiuHzxuNb5Tbd5umbL00,10483
|
9
|
-
src/server.py,sha256=cnO5bgxT-lrfuwk9AIvB_HBV8SWOtFClfGUn5_zFWyo,45652
|
10
|
-
src/single_user_mcp.py,sha256=rJrlqHcIubGkos_24ux5rb3OoKYDzvagCHghhfDeXTI,18535
|
11
|
-
src/telemetry.py,sha256=-RZPIjpI53zbsKmp-63REeZ1JirWHV5WvpSRa2nqZEk,11321
|
12
|
-
src/frontend_dist/index.html,sha256=s95FMkH8VLisvawLH7bZxbLzRUFvMhHkH6ZMzpVBngs,673
|
13
|
-
src/frontend_dist/sw.js,sha256=rihX1es-vWwjmtnXyaksJjs2dio6MVAOTAWwQPeJUYw,2164
|
14
|
-
src/frontend_dist/assets/index-BUUcUfTt.js,sha256=awoyPI6u0v6ao2iarZdSkrSDUvyU8aNkMLqHMvgVgyY,257666
|
15
|
-
src/frontend_dist/assets/index-o6_8mdM8.css,sha256=nwmX_6q55mB9463XN2JM8BdeihjkALpQK83Fc3_iGvE,15936
|
16
|
-
src/mcp_importer/__init__.py,sha256=Mk59pVr7OMGfYGWeSYk8-URfhIcrs3SPLYS7fmJbMII,275
|
17
|
-
src/mcp_importer/__main__.py,sha256=0jVfxKzyr6koVu1ghhWseah5ilKIoGovE6zkEZ-u-Og,515
|
18
|
-
src/mcp_importer/api.py,sha256=47tur0xgl1NBI1Vnh3cpScEmDS64bKMYcWjZDuqx7HQ,6644
|
19
|
-
src/mcp_importer/cli.py,sha256=Pe0GLWm1nMd1VuNXOSkxIrFZuGNFc9dNvfBsvf-bdBI,3487
|
20
|
-
src/mcp_importer/export_cli.py,sha256=daEadB6nL8P4OpEGFx0GshuN1a091L7BhiitpV1bPqA,6294
|
21
|
-
src/mcp_importer/exporters.py,sha256=fSgl6seduoXFp7YnKH26UEaC1sFBnd4whSut7CJLBQs,11348
|
22
|
-
src/mcp_importer/import_api.py,sha256=xWaKoE3vibSWpA5roVL7qEMS73vcmAC0tcHP6CsZw6E,95
|
23
|
-
src/mcp_importer/importers.py,sha256=zGN8lT7qQJ95jDTd-ck09j_w5PSvH-uj33TILoHfHbs,2191
|
24
|
-
src/mcp_importer/merge.py,sha256=KIGT7UgbAm07-LdyoUXEJ7ABSIiPTFlj_qjz669yFxg,1569
|
25
|
-
src/mcp_importer/parsers.py,sha256=JRE7y_Gg-QmlAARvZdrI9CmUyy-ODvDPbS695pb3Aw8,4856
|
26
|
-
src/mcp_importer/paths.py,sha256=4L-cPr7KCM9X9gAUP7Da6ictLNrPWuQ_IM419zqY-2I,2700
|
27
|
-
src/mcp_importer/quick_cli.py,sha256=4mJe10q_lZCYLm75QBt1rYy2j8mGEsRZoAqA0agjfSM,1834
|
28
|
-
src/mcp_importer/types.py,sha256=h03TbAnJbap6OWWd0dT0QcFWNvSaiVFWH9V9PD6x4s0,138
|
29
|
-
src/middleware/data_access_tracker.py,sha256=bArBffWgYmvxOx9z_pgXQhogvnWQcc1m6WvEblDD4gw,15039
|
30
|
-
src/middleware/session_tracking.py,sha256=5W1VH9HNqIZeX0HNxDEm41U4GY6SqKSXtApDEeZK2qo,23084
|
31
|
-
src/setup_tui/__init__.py,sha256=mDFrQoiOtQOHc0sFfGKrNXVLEDeB1S0O5aISBVzfxYo,184
|
32
|
-
src/setup_tui/main.py,sha256=892X2KVKOYmKzUu1Ok8SnApNYxpcFHFHmFLvpPZP4qY,5501
|
33
|
-
open_edison-0.1.44.dist-info/METADATA,sha256=hY2fd8IeT-YeBAUjg_FBtpGf50VGOpgRp4xdBhx7ED4,12375
|
34
|
-
open_edison-0.1.44.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
35
|
-
open_edison-0.1.44.dist-info/entry_points.txt,sha256=YiGNm9x2I00hgT10HDyB4gxC1LcaV_mu8bXFjolu0Yw,171
|
36
|
-
open_edison-0.1.44.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
37
|
-
open_edison-0.1.44.dist-info/RECORD,,
|