Skip to main content
Glama
michaelneale

Goose App Maker MCP

by michaelneale

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
NameRequiredDescriptionDefault
app_nameYes

Output Schema

TableJSON Schema
NameRequiredDescriptionDefault
resultYes

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]:
  • 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
    """
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden. It discloses that the server 'will automatically find an available port' and 'can only serve one app at a time,' which are useful behavioral traits. However, it doesn't mention permissions, rate limits, or what happens if the app is already running, leaving gaps for a mutation tool.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is appropriately sized and front-loaded with the main purpose. The sentences are efficient, though the 'Args' and 'Returns' sections could be integrated more smoothly into the flow, but overall it's concise with minimal waste.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool has an output schema (returns 'A dictionary containing the result of the operation'), the description doesn't need to detail return values. However, as a mutation tool with no annotations and only basic behavioral context, it lacks information on error handling or prerequisites, making it adequate but with clear gaps.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The description adds meaning beyond the input schema by explaining that 'app_name' is the 'Name of the application to serve.' Since there is only one parameter and schema description coverage is 0%, this compensates well, though it doesn't detail format or constraints like valid app names.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's purpose: 'Serve an existing web application on a local HTTP server.' It specifies the verb ('serve') and resource ('existing web application'), but doesn't explicitly differentiate from siblings like 'app_open' or 'app_stop_server' that might involve similar concepts.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides some implied usage context with 'Can only serve one app at a time,' which suggests a constraint but doesn't explicitly state when to use this tool versus alternatives like 'app_open' or 'app_stop_server.' No clear alternatives or exclusions are named.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/michaelneale/goose-app-maker-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server