app_serve
Serve a web application locally on an available HTTP port to test and run applications created with Goose App Maker MCP.
Instructions
Serve an existing web application on a local HTTP server.
The server will automatically find an available port.
Can only serve one app at a time
Args:
app_name: Name of the application to serve
Returns:
A dictionary containing the result of the operation
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| app_name | Yes |
Implementation Reference
- main.py:268-479 (handler)The core handler function for the 'app_serve' tool. It starts a local HTTP server for the specified app directory, includes a custom EnvAwareHandler class for handling goose_api.js requests and environment variable substitution, and manages server lifecycle.def app_serve(app_name: str) -> Dict[str, Any]: """ Serve an existing web application on a local HTTP server. The server will automatically find an available port. Can only serve one app at a time Args: app_name: Name of the application to serve Returns: A dictionary containing the result of the operation """ global http_server, server_port, app_response, response_ready if http_server: return "There is already a server running" # Reset response state app_response = None response_ready = False try: # Find the app directory app_path = os.path.join(APP_DIR, app_name) if not os.path.exists(app_path): return { "success": False, "error": f"App '{app_name}' not found at {app_path}" } # Stop any existing server if http_server: logger.info("Stopping existing HTTP server") http_server.shutdown() http_server.server_close() http_server = None # Find a free port import socket def find_free_port(): with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.bind(('', 0)) return s.getsockname()[1] # Try the default port first, if busy find a free one try: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.bind(('', server_port)) except OSError: logger.info(f"Default port {server_port} is busy, finding a free port") server_port = find_free_port() logger.info(f"Found free port: {server_port}") # Create a custom handler that serves from the app directory # and replaces environment variables in JavaScript files class EnvAwareHandler(http.server.SimpleHTTPRequestHandler): def __init__(self, *args, **kwargs): super().__init__(*args, directory=app_path, **kwargs) def end_headers(self): # Add cache control headers to ALL responses self.send_header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0') self.send_header('Pragma', 'no-cache') self.send_header('Expires', '0') super().end_headers() def do_GET(self): # Check if this is a wait_for_response request if self.path.startswith('/wait_for_response'): global app_response, response_lock, response_ready # Reset response state for a new request if self.path.startswith('/wait_for_response/reset'): with response_lock: app_response = None response_ready = False self.send_response(200) self.send_header('Content-type', 'application/json') self.end_headers() response_data = json.dumps({"success": True, "message": "Response state reset"}) self.wfile.write(response_data.encode('utf-8')) return # Check if response already exists if app_response is not None and response_ready: # Return the response immediately self.send_response(200) self.send_header('Content-type', 'application/json') self.end_headers() response_data = json.dumps({"success": True, "data": app_response}) self.wfile.write(response_data.encode('utf-8')) # Reset the response state after sending it with response_lock: app_response = None response_ready = False return # Wait for the response with timeout with response_lock: # Wait for up to 180 seconds for the response to be ready start_time = time.time() while not response_ready and time.time() - start_time < 180: response_lock.wait(180 - (time.time() - start_time)) # Check if the response is now available if response_ready and app_response is not None: break # Check if we got the response or timed out if response_ready and app_response is not None: self.send_response(200) self.send_header('Content-type', 'application/json') self.end_headers() response_data = json.dumps({"success": True, "data": app_response}) self.wfile.write(response_data.encode('utf-8')) else: # Timeout occurred self.send_response(408) # Request Timeout self.send_header('Content-type', 'application/json') self.end_headers() response_data = json.dumps({"success": False, "error": "Timeout waiting for response"}) self.wfile.write(response_data.encode('utf-8')) return # Get the file path path = self.translate_path(self.path) # Check if the file exists if os.path.isfile(path): # Check if it's a JavaScript file that might need variable replacement if path.endswith('.js'): try: with open(path, 'r') as f: content = f.read() # Check if the file contains environment variables that need to be replaced if '$GOOSE_PORT' in content or '$GOOSE_SERVER__SECRET_KEY' in content: # Replace environment variables goose_port = os.environ.get('GOOSE_PORT', '0') secret_key = os.environ.get('GOOSE_SERVER__SECRET_KEY', '') # Replace variables content = content.replace('$GOOSE_PORT', goose_port) content = content.replace('$GOOSE_SERVER__SECRET_KEY', secret_key) # Send the modified content self.send_response(200) self.send_header('Content-type', 'application/javascript') self.send_header('Content-Length', str(len(content))) self.end_headers() self.wfile.write(content.encode('utf-8')) return except Exception as e: logger.error(f"Error processing JavaScript file: {e}") # If we didn't handle it specially, use the default handler return super().do_GET() # Start the server in a separate thread import threading # Use a thread-safe event to signal when the server is ready server_ready = threading.Event() server_error = [None] # Use a list to store error from thread def run_server(): global http_server try: with socketserver.TCPServer(("", server_port), EnvAwareHandler) as server: http_server = server # Signal that server is ready server_ready.set() logger.info(f"Serving app '{app_name}' at http://localhost:{server_port}") logger.info(f"Using GOOSE_PORT={os.environ.get('GOOSE_PORT', '3000')}") logger.info(f"Using GOOSE_SERVER__SECRET_KEY={os.environ.get('GOOSE_SERVER__SECRET_KEY', '')[:5]}...") server.serve_forever() except Exception as e: server_error[0] = str(e) server_ready.set() # Signal even on error logger.error(f"Server error: {e}") server_thread = threading.Thread(target=run_server) server_thread.daemon = True server_thread.start() # Wait for the server to start or fail, with timeout if not server_ready.wait(timeout=2.0): return { "success": False, "error": "Server failed to start within timeout period" } # Check if there was an error if server_error[0]: return { "success": False, "error": f"Failed to serve app: {server_error[0]}" } return { "success": True, "app_name": app_name, "port": server_port, "url": f"http://localhost:{server_port}", "message": f"App '{app_name}' is now being served at http://localhost:{server_port}" } except Exception as e: logger.error(f"Error serving app: {e}") return {"success": False, "error": f"Failed to serve app: {str(e)}"}
- main.py:267-268 (registration)The @mcp.tool() decorator registers the app_serve function as an MCP tool.@mcp.tool() def app_serve(app_name: str) -> Dict[str, Any]:
- main.py:269-280 (schema)Docstring defining the input (app_name: str) and output (Dict[str, Any]) schema for the tool.""" Serve an existing web application on a local HTTP server. The server will automatically find an available port. Can only serve one app at a time Args: app_name: Name of the application to serve Returns: A dictionary containing the result of the operation """