mirror of
https://github.com/open-webui/mcpo.git
synced 2026-07-01 21:04:00 -04:00
Merge pull request #223 from njzydark/fix-hot-reload-symlink
fix: symlink handling in config watcher to update path on modification
This commit is contained in:
@@ -34,7 +34,7 @@ jobs:
|
||||
CHANGELOG_ESCAPED=$(echo "$CHANGELOG_CONTENT" | sed ':a;N;$!ba;s/\n/%0A/g')
|
||||
echo "Extracted latest release notes from CHANGELOG.md:"
|
||||
echo -e "$CHANGELOG_CONTENT"
|
||||
echo "::set-output name=content::$CHANGELOG_ESCAPED"
|
||||
echo "content=$CHANGELOG_ESCAPED" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Create GitHub release
|
||||
uses: actions/github-script@v7
|
||||
|
||||
+2
-2
@@ -16,7 +16,7 @@ and this project adheres to Semantic Versioning.
|
||||
|
||||
### Added
|
||||
|
||||
- 🔄 **Hot Reload Support for Configuration Files**: Added `--hot-reload` flag to watch your config file for changes and dynamically reload MCP servers without restarting the application—enabling seamless development workflows and runtime configuration updates.
|
||||
- 🔄 **Hot Reload Support for Configuration Files**: Added \`--hot-reload\` flag to watch your config file for changes and dynamically reload MCP servers without restarting the application—enabling seamless development workflows and runtime configuration updates.
|
||||
- 🤫 **HTTP Request Filtering for Cleaner Logs**: Added configurable log filtering to reduce noise from frequent HTTP requests, making debugging and monitoring much clearer in production environments.
|
||||
|
||||
### Changed
|
||||
@@ -115,4 +115,4 @@ and this project adheres to Semantic Versioning.
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🧹 **Cleaner Proxy Output**: Dropped None arguments from proxy requests, resulting in reduced clutter and improved interoperability with servers expecting clean inputs—ensuring more reliable downstream performance with MCP tools.
|
||||
- 🧹 **Cleaner Proxy Output**: Dropped None arguments from proxy requests, resulting in reduced clutter and improved interoperability with servers expecting clean inputs—ensuring more reliable downstream performance with MCP tools.
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "mcpo"
|
||||
version = "0.0.16"
|
||||
version = "0.0.17"
|
||||
description = "A simple, secure MCP-to-OpenAPI proxy server"
|
||||
authors = [
|
||||
{ name = "Timothy Jaeryang Baek", email = "tim@openwebui.com" }
|
||||
|
||||
+25
-6
@@ -351,10 +351,27 @@ async def lifespan(app: FastAPI):
|
||||
f"Connection attempt for '{server_name}' finished, but status is not 'connected'."
|
||||
)
|
||||
failed_servers.append(server_name)
|
||||
except Exception:
|
||||
logger.error(
|
||||
f"Failed to establish connection for server: '{server_name}'."
|
||||
)
|
||||
except Exception as e:
|
||||
error_class_name = type(e).__name__
|
||||
if error_class_name == 'ExceptionGroup' or (hasattr(e, 'exceptions') and hasattr(e, 'message')):
|
||||
logger.error(
|
||||
f"Failed to establish connection for server: '{server_name}' - Multiple errors occurred:"
|
||||
)
|
||||
# Log each individual exception from the group
|
||||
exceptions = getattr(e, 'exceptions', [])
|
||||
for idx, exc in enumerate(exceptions):
|
||||
logger.error(f" Error {idx + 1}: {type(exc).__name__}: {exc}")
|
||||
# Also log traceback for each exception
|
||||
if hasattr(exc, '__traceback__'):
|
||||
import traceback
|
||||
tb_lines = traceback.format_exception(type(exc), exc, exc.__traceback__)
|
||||
for line in tb_lines:
|
||||
logger.debug(f" {line.rstrip()}")
|
||||
else:
|
||||
logger.error(
|
||||
f"Failed to establish connection for server: '{server_name}' - {type(e).__name__}: {e}",
|
||||
exc_info=True
|
||||
)
|
||||
failed_servers.append(server_name)
|
||||
|
||||
logger.info("\n--- Server Startup Summary ---")
|
||||
@@ -409,9 +426,11 @@ async def lifespan(app: FastAPI):
|
||||
app.state.is_connected = True
|
||||
yield
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to connect to MCP server '{app.title}': {e}")
|
||||
# Log the full exception with traceback for debugging
|
||||
logger.error(f"Failed to connect to MCP server '{app.title}': {type(e).__name__}: {e}", exc_info=True)
|
||||
app.state.is_connected = False
|
||||
return
|
||||
# Re-raise the exception so it propagates to the main app's lifespan
|
||||
raise
|
||||
|
||||
|
||||
async def run(
|
||||
|
||||
@@ -15,8 +15,10 @@ logger = logging.getLogger(__name__)
|
||||
class ConfigChangeHandler(FileSystemEventHandler):
|
||||
"""Handler for config file changes."""
|
||||
|
||||
def __init__(self, config_path: Path, reload_callback: Callable[[Dict[str, Any]], None], loop: asyncio.AbstractEventLoop):
|
||||
self.config_path = config_path.resolve() # Resolve to absolute path
|
||||
def __init__(self, origin_config_path: Path, reload_callback: Callable[[Dict[str, Any]], None], loop: asyncio.AbstractEventLoop):
|
||||
self.origin_config_path = origin_config_path
|
||||
self.config_path = origin_config_path.resolve() # Resolve to absolute path
|
||||
self.is_symlink = origin_config_path.is_symlink()
|
||||
self.reload_callback = reload_callback
|
||||
self.loop = loop # Store reference to the main event loop
|
||||
self._last_modification = 0
|
||||
@@ -24,6 +26,12 @@ class ConfigChangeHandler(FileSystemEventHandler):
|
||||
|
||||
def on_modified(self, event):
|
||||
"""Handle file modification events."""
|
||||
if self.is_symlink:
|
||||
self.config_path = self.origin_config_path.resolve() # Re-resolve to get the latest symlink target
|
||||
logger.info(f"Symlink file modified: {self.config_path}")
|
||||
self._trigger_reload()
|
||||
return
|
||||
|
||||
if event.is_directory:
|
||||
return
|
||||
|
||||
@@ -116,7 +124,8 @@ class ConfigWatcher:
|
||||
"""Watches a config file for changes and triggers reloads."""
|
||||
|
||||
def __init__(self, config_path: str, reload_callback: Callable[[Dict[str, Any]], None]):
|
||||
self.config_path = Path(config_path).resolve()
|
||||
self.origin_config_path = Path(config_path)
|
||||
self.config_path = self.origin_config_path.resolve()
|
||||
self.reload_callback = reload_callback
|
||||
self.observer: Optional[Observer] = None
|
||||
self.handler: Optional[ConfigChangeHandler] = None
|
||||
@@ -135,12 +144,12 @@ class ConfigWatcher:
|
||||
logger.error("No running event loop found, cannot start config watcher")
|
||||
return
|
||||
|
||||
self.handler = ConfigChangeHandler(self.config_path, self.reload_callback, self.loop)
|
||||
self.handler = ConfigChangeHandler(self.origin_config_path, self.reload_callback, self.loop)
|
||||
self.observer = Observer()
|
||||
|
||||
# Watch the directory containing the config file
|
||||
watch_dir = self.config_path.parent
|
||||
logger.debug(f"Watching directory: {watch_dir} for file: {self.config_path}")
|
||||
watch_dir = self.origin_config_path.parent
|
||||
logger.info(f"Watching directory: {watch_dir} for file: {self.config_path}")
|
||||
self.observer.schedule(self.handler, str(watch_dir), recursive=False)
|
||||
|
||||
self.observer.start()
|
||||
|
||||
@@ -57,8 +57,8 @@ def process_tool_response(result: CallToolResult) -> list:
|
||||
|
||||
|
||||
def name_needs_alias(name: str) -> bool:
|
||||
"""Check if a field name needs aliasing (for now if it starts with '__')."""
|
||||
return name.startswith("__")
|
||||
"""Check if a field name needs aliasing (if it starts with '_')."""
|
||||
return name.startswith("_")
|
||||
|
||||
|
||||
def generate_alias_name(original_name: str, existing_names: set) -> str:
|
||||
@@ -66,7 +66,7 @@ def generate_alias_name(original_name: str, existing_names: set) -> str:
|
||||
Generate an alias field name by stripping unwanted chars, and avoiding conflicts with existing names.
|
||||
|
||||
Args:
|
||||
original_name: The original field name (should start with '__')
|
||||
original_name: The original field name (should start with '_')
|
||||
existing_names: Set of existing names to avoid conflicts with
|
||||
|
||||
Returns:
|
||||
|
||||
Reference in New Issue
Block a user