From 1201eb2d3d73e700bdbfe0f87cadfeeaf00218be Mon Sep 17 00:00:00 2001 From: Mathieu Virbel Date: Wed, 2 Apr 2025 23:27:37 +0200 Subject: [PATCH] feat(goose): update config using uv script with pyyaml (#6) --- mcontainer/drivers/goose/Dockerfile | 17 +- mcontainer/drivers/goose/init-status.sh | 2 +- mcontainer/drivers/goose/mc-init.sh | 19 ++- .../drivers/goose/update-goose-config.py | 94 ++++++++++++ .../drivers/goose/update-goose-config.sh | 145 ------------------ 5 files changed, 117 insertions(+), 160 deletions(-) create mode 100644 mcontainer/drivers/goose/update-goose-config.py delete mode 100644 mcontainer/drivers/goose/update-goose-config.sh diff --git a/mcontainer/drivers/goose/Dockerfile b/mcontainer/drivers/goose/Dockerfile index 905fb5a..6030b03 100644 --- a/mcontainer/drivers/goose/Dockerfile +++ b/mcontainer/drivers/goose/Dockerfile @@ -24,17 +24,18 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ RUN mkdir -p /var/run/sshd && chmod 0755 /var/run/sshd # Do NOT enable root login or set root password here -# Install python dependencies -# This is done before copying scripts for better cache management -# Consider moving this WORKDIR /tmp section if goose CLI isn't strictly needed for base image setup +# Install deps WORKDIR /tmp +RUN curl -fsSL https://astral.sh/uv/install.sh -o install.sh && \ + sh install.sh && \ + mv /root/.local/bin/uv /usr/local/bin/uv && \ + mv /root/.local/bin/uvx /usr/local/bin/uvx && \ + rm install.sh RUN curl -fsSL https://github.com/block/goose/releases/download/stable/download_cli.sh -o download_cli.sh && \ chmod +x download_cli.sh && \ ./download_cli.sh && \ - # Move goose to a system-wide location mv /root/.local/bin/goose /usr/local/bin/goose && \ - # Clean up - rm -rf /root/.local download_cli.sh /tmp/goose-* + rm -rf download_cli.sh /tmp/goose-* # Create app directory WORKDIR /app @@ -44,13 +45,13 @@ COPY mc-init.sh /mc-init.sh COPY entrypoint.sh /entrypoint.sh COPY mc-driver.yaml /mc-driver.yaml COPY init-status.sh /init-status.sh -COPY update-goose-config.sh /usr/local/bin/update-goose-config.sh +COPY update-goose-config.py /usr/local/bin/update-goose-config.py # Extend env via bashrc # Make scripts executable RUN chmod +x /mc-init.sh /entrypoint.sh /init-status.sh \ - /usr/local/bin/update-goose-config.sh + /usr/local/bin/update-goose-config.py # Set up initialization status check on login RUN echo '[ -x /init-status.sh ] && /init-status.sh' >> /etc/bash.bashrc diff --git a/mcontainer/drivers/goose/init-status.sh b/mcontainer/drivers/goose/init-status.sh index 2600d66..4efda67 100644 --- a/mcontainer/drivers/goose/init-status.sh +++ b/mcontainer/drivers/goose/init-status.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Script to check and display initialization status - optimized version +# Script to check and display initialization status # Only proceed if running as root if [ "$(id -u)" != "0" ]; then diff --git a/mcontainer/drivers/goose/mc-init.sh b/mcontainer/drivers/goose/mc-init.sh index 75a755f..aeb3295 100755 --- a/mcontainer/drivers/goose/mc-init.sh +++ b/mcontainer/drivers/goose/mc-init.sh @@ -41,10 +41,17 @@ fi # Create home directory and set permissions mkdir -p /home/mcuser chown $MC_USER_ID:$MC_GROUP_ID /home/mcuser -# Ensure /app exists and has correct ownership (important for volume mounts) mkdir -p /app chown $MC_USER_ID:$MC_GROUP_ID /app +# Copy /root/.local/bin to the user's home directory +if [ -d /root/.local/bin ]; then + echo "Copying /root/.local/bin to /home/mcuser/.local/bin..." + mkdir -p /home/mcuser/.local/bin + cp -r /root/.local/bin/* /home/mcuser/.local/bin/ + chown -R $MC_USER_ID:$MC_GROUP_ID /home/mcuser/.local/bin +fi + # Start SSH server only if explicitly enabled if [ "$MC_SSH_ENABLED" = "true" ]; then echo "Starting SSH server..." @@ -141,14 +148,14 @@ if [ -n "$MC_PERSISTENT_LINKS" ]; then fi # Update Goose configuration with available MCP servers (run as mcuser after symlinks are created) -if [ -f "/usr/local/bin/update-goose-config.sh" ]; then +if [ -f "/usr/local/bin/update-goose-config.py" ]; then echo "Updating Goose configuration with MCP servers as mcuser..." - gosu mcuser bash /usr/local/bin/update-goose-config.sh -elif [ -f "$(dirname "$0")/update-goose-config.sh" ]; then + gosu mcuser /usr/local/bin/update-goose-config.py +elif [ -f "$(dirname "$0")/update-goose-config.py" ]; then echo "Updating Goose configuration with MCP servers as mcuser..." - gosu mcuser bash "$(dirname "$0")/update-goose-config.sh" + gosu mcuser "$(dirname "$0")/update-goose-config.py" else - echo "Warning: update-goose-config.sh script not found. Goose configuration will not be updated." + echo "Warning: update-goose-config.py script not found. Goose configuration will not be updated." fi # Mark initialization as complete diff --git a/mcontainer/drivers/goose/update-goose-config.py b/mcontainer/drivers/goose/update-goose-config.py new file mode 100644 index 0000000..26375c6 --- /dev/null +++ b/mcontainer/drivers/goose/update-goose-config.py @@ -0,0 +1,94 @@ +#!/usr/bin/env -S uv run --script +# /// script +# dependencies = ["ruamel.yaml"] +# /// +import json +import os +from pathlib import Path + +from ruamel.yaml import YAML + +# Path to goose config +GOOSE_CONFIG = Path.home() / ".config/goose/config.yaml" +CONFIG_DIR = GOOSE_CONFIG.parent + +# Create config directory if it doesn't exist +CONFIG_DIR.mkdir(parents=True, exist_ok=True) + + +def update_config(): + """Update Goose configuration based on environment variables and config file""" + + yaml = YAML() + + # Load or initialize the YAML configuration + if not GOOSE_CONFIG.exists(): + config_data = {"extensions": {}} + else: + with GOOSE_CONFIG.open("r") as f: + config_data = yaml.load(f) + if "extensions" not in config_data: + config_data["extensions"] = {} + + # Get MCP information from environment variables + mcp_count = int(os.environ.get("MCP_COUNT", "0")) + mcp_names_str = os.environ.get("MCP_NAMES", "[]") + + try: + mcp_names = json.loads(mcp_names_str) + print(f"Found {mcp_count} MCP servers: {', '.join(mcp_names)}") + except json.JSONDecodeError: + mcp_names = [] + print("Error parsing MCP_NAMES environment variable") + + # Process each MCP - collect the MCP configs to add or update + for idx in range(mcp_count): + mcp_name = os.environ.get(f"MCP_{idx}_NAME") + mcp_type = os.environ.get(f"MCP_{idx}_TYPE") + mcp_host = os.environ.get(f"MCP_{idx}_HOST") + + # Always use container's SSE port (8080) not the host-bound port + if mcp_name and mcp_host: + # Use standard MCP SSE port (8080) + mcp_url = f"http://{mcp_host}:8080/sse" + print(f"Processing MCP extension: {mcp_name} ({mcp_type}) - {mcp_url}") + config_data["extensions"][mcp_name] = { + "enabled": True, + "name": mcp_name, + "timeout": 60, + "type": "sse", + "uri": mcp_url, + "envs": {}, + } + elif mcp_name and os.environ.get(f"MCP_{idx}_URL"): + # For remote MCPs, use the URL provided in environment + mcp_url = os.environ.get(f"MCP_{idx}_URL") + print( + f"Processing remote MCP extension: {mcp_name} ({mcp_type}) - {mcp_url}" + ) + config_data["extensions"][mcp_name] = { + "enabled": True, + "name": mcp_name, + "timeout": 60, + "type": "sse", + "uri": mcp_url, + "envs": {}, + } + + # Write the updated configuration back to the file + with GOOSE_CONFIG.open("w") as f: + yaml.dump(config_data, f) + + print(f"Updated Goose configuration at {GOOSE_CONFIG}") + + +if __name__ == "__main__": + mcp_count_str = os.environ.get("MCP_COUNT", "0") + mcp_count = int(mcp_count_str) + + if mcp_count > 0: + print("Updating Goose configuration with MCP servers...") + update_config() + print("Goose configuration updated successfully!") + else: + print("No MCP servers found, using default Goose configuration.") diff --git a/mcontainer/drivers/goose/update-goose-config.sh b/mcontainer/drivers/goose/update-goose-config.sh deleted file mode 100644 index 4d7220a..0000000 --- a/mcontainer/drivers/goose/update-goose-config.sh +++ /dev/null @@ -1,145 +0,0 @@ -#!/bin/bash -# Script to update Goose configuration with MCP servers using Python standard library - -# Define config path -GOOSE_CONFIG="$HOME/.config/goose/config.yaml" -CONFIG_DIR="$(dirname "$GOOSE_CONFIG")" - -# Create config directory if it doesn't exist -mkdir -p "$CONFIG_DIR" - -# Function to update config using Python without yaml module -update_config() { - python3 - << 'EOF' -import os -import json -import re - -# Path to goose config -config_path = os.path.expanduser('~/.config/goose/config.yaml') - -# Check if file exists, create if not -if not os.path.exists(config_path): - with open(config_path, 'w') as f: - f.write("extensions:\n") - -# Read the entire file -with open(config_path, 'r') as f: - content = f.read() - -# Get MCP information from environment variables -mcp_count = int(os.environ.get('MCP_COUNT', '0')) -mcp_names_str = os.environ.get('MCP_NAMES', '[]') - -try: - mcp_names = json.loads(mcp_names_str) - print(f"Found {mcp_count} MCP servers: {', '.join(mcp_names)}") -except: - mcp_names = [] - print("Error parsing MCP_NAMES environment variable") - -# Check if extensions key exists, add if not -if 'extensions:' not in content: - content = "extensions:\n" + content - -# Process each MCP - we'll collect the mcp configs to add or update -mcp_configs = [] -for idx in range(mcp_count): - mcp_name = os.environ.get(f'MCP_{idx}_NAME') - mcp_type = os.environ.get(f'MCP_{idx}_TYPE') - mcp_host = os.environ.get(f'MCP_{idx}_HOST') - - # Always use container's SSE port (8080) not the host-bound port - if mcp_name and mcp_host: - # Use standard MCP SSE port (8080) - mcp_url = f"http://{mcp_host}:8080/sse" - print(f"Processing MCP extension: {mcp_name} ({mcp_type}) - {mcp_url}") - mcp_configs.append((mcp_name, mcp_url)) - elif mcp_name and os.environ.get(f'MCP_{idx}_URL'): - # For remote MCPs, use the URL provided in environment - mcp_url = os.environ.get(f'MCP_{idx}_URL') - print(f"Processing remote MCP extension: {mcp_name} ({mcp_type}) - {mcp_url}") - mcp_configs.append((mcp_name, mcp_url)) - -# Now we'll update the config file line by line, preserving all content -lines = content.split('\n') -output_lines = [] -in_extensions = False -current_ext = None -extension_added = set() # Track which extensions we've processed - -# First pass - update existing extensions and track them -for line in lines: - # Check if we're entering extensions section - if line.strip() == 'extensions:': - in_extensions = True - output_lines.append(line) - continue - - # Look for extension definition (2-space indentation) - if in_extensions and re.match(r'^ (\w+):', line): - match = re.match(r'^ (\w+):', line) - current_ext = match.group(1) - output_lines.append(line) - - # Mark as seen if this is one of our MCPs - for mcp_name, _ in mcp_configs: - if mcp_name == current_ext: - extension_added.add(mcp_name) - continue - - # If we're in an MCP extension that we need to update - if in_extensions and current_ext and current_ext in [n for n, _ in mcp_configs]: - # If this is a URI line, replace it with our URL - if line.strip().startswith('uri:'): - for mcp_name, mcp_url in mcp_configs: - if mcp_name == current_ext: - output_lines.append(f' uri: {mcp_url}') - break - # If this is a type line, ensure it's SSE - elif line.strip().startswith('type:'): - output_lines.append(' type: sse') - # If this is enabled line, ensure it's true - elif line.strip().startswith('enabled:'): - output_lines.append(' enabled: true') - # Otherwise keep the line - else: - output_lines.append(line) - continue - - # If we're moving to a non-2-space indented line, we're out of the current extension - if in_extensions and current_ext and not line.startswith(' ') and line.strip(): - current_ext = None - - # For any other line, just add it - output_lines.append(line) - -# Add any MCP extensions that weren't found in the existing config -if in_extensions: - for mcp_name, mcp_url in mcp_configs: - if mcp_name not in extension_added: - print(f"Adding new MCP extension: {mcp_name}") - output_lines.append(f' {mcp_name}:') - output_lines.append(f' enabled: true') - output_lines.append(f' name: {mcp_name}') - output_lines.append(f' timeout: 60') - output_lines.append(f' type: sse') - output_lines.append(f' uri: {mcp_url}') - output_lines.append(f' envs: {{}}') - -# Write the updated content back -with open(config_path, 'w') as f: - f.write('\n'.join(output_lines)) - -print(f"Updated Goose configuration at {config_path}") -EOF -} - -# Check if MCP servers are defined -if [ -n "$MCP_COUNT" ] && [ "$MCP_COUNT" -gt 0 ]; then - echo "Updating Goose configuration with MCP servers..." - update_config - echo "Goose configuration updated successfully!" -else - echo "No MCP servers found, using default Goose configuration." -fi \ No newline at end of file