From 5251eb6185926e19ba00375cac835b1b5dcb3d51 Mon Sep 17 00:00:00 2001 From: Moritz Graf Date: Wed, 22 Oct 2025 21:42:00 +0200 Subject: [PATCH] Commit before I will try another thing... that might then (maybe) really really work) --- k8s/n8n/garmin-mcp.yaml | 59 +++++++++----- k8s/n8n/garth-wrapper-script_configmap.yaml | 85 +++++++++++++++++++++ 2 files changed, 125 insertions(+), 19 deletions(-) create mode 100644 k8s/n8n/garth-wrapper-script_configmap.yaml diff --git a/k8s/n8n/garmin-mcp.yaml b/k8s/n8n/garmin-mcp.yaml index 6675800..1dead2d 100644 --- a/k8s/n8n/garmin-mcp.yaml +++ b/k8s/n8n/garmin-mcp.yaml @@ -12,12 +12,11 @@ # # This key MUST be GARTH_TOKEN to match the application's environment variable # GARTH_TOKEN: your_base64_encoded_token_here --- -# --- 2. Deployment for the Garth MCP Server --- -# This will create a Pod running the server application. +# deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: - name: garth-mcp-server-deployment + name: garth-mcp-server namespace: n8n labels: app: garth-mcp-server @@ -33,28 +32,50 @@ spec: spec: containers: - name: garth-mcp-server - # Use a standard Python slim image as a base - image: python:3.11-slim - # The command first installs the 'uv' tool, then uses 'uvx' to run the server. - # It's configured to listen on all interfaces (0.0.0.0) inside the container. + # Use a Python image version >= 3.13 as requested. + image: python:3.13-slim + workingDir: /app + # This command now installs dependencies and directly executes the mounted script. command: ["/bin/sh", "-c"] args: - - "pip install -U garth-mcp-server && garth-mcp-server --host 0.0.0.0 --port 8080" + - | + set -e + echo "----> Installing Python dependencies..." + pip install flask garth garth-mcp-server + echo "----> Dependencies installed." + echo "----> Starting Flask server from mounted script." + exec python /app/wrapper.py ports: - - name: http - containerPort: 8080 - protocol: TCP + - containerPort: 5000 + name: http + # Mount the wrapper.py script from the ConfigMap into the container. + volumeMounts: + - name: wrapper-script-volume + mountPath: /app/wrapper.py + subPath: wrapper.py + # Inject the Garmin token securely from the Kubernetes Secret. envFrom: - # Load environment variables from the Secret created above - secretRef: name: garth-token-secret - resources: - requests: - memory: "64Mi" - cpu: "100m" - limits: - memory: "128Mi" - cpu: "250m" + # Health probes for Kubernetes to manage the pod's lifecycle. + livenessProbe: + httpGet: + path: /healthz + port: 5000 + initialDelaySeconds: 15 + periodSeconds: 20 + readinessProbe: + httpGet: + path: /healthz + port: 5000 + # Increased delay to allow for dependency installation. + initialDelaySeconds: 60 + periodSeconds: 10 + # Define the volume that will be populated by the ConfigMap. + volumes: + - name: wrapper-script-volume + configMap: + name: garth-wrapper-script --- # --- 3. Service to expose the Deployment --- # This creates a stable internal endpoint for the server. diff --git a/k8s/n8n/garth-wrapper-script_configmap.yaml b/k8s/n8n/garth-wrapper-script_configmap.yaml new file mode 100644 index 0000000..741d5e6 --- /dev/null +++ b/k8s/n8n/garth-wrapper-script_configmap.yaml @@ -0,0 +1,85 @@ +# configmap.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: garth-wrapper-script + namespace: n8n +data: + wrapper.py: | + # wrapper.py + import subprocess + import logging + from flask import Flask, request, Response + + # Configure basic logging + logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') + + # Initialize the Flask application + app = Flask(__name__) + + # Define the main endpoint that n8n will call. It only accepts POST requests. + @app.route('/mcp', methods=['POST']) + def mcp_wrapper(): + """ + This function receives a POST request from n8n, executes the garth-mcp-server + as a subprocess, pipes the request body to the subprocess's stdin, + captures its stdout, and returns it as the HTTP response. + """ + # Retrieve the raw binary data from the incoming request body. + request_data = request.get_data() + logging.info(f"Received request with {len(request_data)} bytes of data.") + + # Define the command to execute the garth-mcp-server. + # 'uvx' is the recommended runner for this package. + command = ['uvx', 'garth-mcp-server'] + + try: + # Execute the command as a subprocess. + # - input: The data to be sent to the subprocess's stdin. + # - capture_output=True: Captures stdout and stderr. + # - check=True: Raises a CalledProcessError if the command returns a non-zero exit code. + # - timeout: Sets a 60-second timeout to prevent hanging processes. + result = subprocess.run( + command, + input=request_data, + capture_output=True, + check=True, + timeout=60 + ) + + # If the command was successful, log the success and return the captured stdout. + # The content type is set to text/plain to ensure proper handling by clients. + logging.info(f"Subprocess executed successfully. Returning {len(result.stdout)} bytes of stdout.") + return Response(result.stdout, mimetype='text/plain', status=200) + + except subprocess.CalledProcessError as e: + # If the subprocess returns a non-zero exit code, it indicates an error. + # Log the error, including the captured stderr for debugging. + error_message = e.stderr.decode('utf-8', errors='ignore') + logging.error(f"Subprocess failed with exit code {e.returncode}. Stderr: {error_message}") + return Response(f"Error executing garth-mcp-server: {error_message}", mimetype='text/plain', status=500) + + except subprocess.TimeoutExpired: + # If the subprocess takes longer than the specified timeout. + logging.error("Subprocess timed out after 60 seconds.") + return Response("Error: garth-mcp-server process timed out.", mimetype='text/plain', status=504) + + except Exception as e: + # Catch any other unexpected exceptions. + logging.error(f"An unexpected error occurred: {str(e)}") + return Response(f"An unexpected server error occurred: {str(e)}", mimetype='text/plain', status=500) + + # Define a simple health check endpoint for Kubernetes liveness and readiness probes. + @app.route('/healthz', methods=['GET']) + def health_check(): + """ + A simple endpoint that returns a 200 OK response, indicating the + Flask server is running and responsive. + """ + return Response("OK", status=200) + + # Main execution block to run the Flask development server. + # host='0.0.0.0' makes the server accessible from outside the container. + # port=5000 is the port the server will listen on. + if __name__ == '__main__': + app.run(host='0.0.0.0', port=5000) \ No newline at end of file