Skip to content

Repository files navigation

serve-local

Stable localhost addresses for all your local HTML tools — no config required to get started.

serve-local is a lightweight Python runtime that assigns a permanent URL to every project folder on your machine. Instead of hunting for which port your tool is on, you get:

http://my-tool.localhost:9999   ← always works, regardless of what port it actually runs on

Or via direct path routing:

http://localhost:9999/tool/my-tool/

No heavy dependencies. No daemon management. One Python entrypoint boots the whole thing.

Tests Python 3.11+ License MIT Platform Windows · macOS · Linux


What it does

Without serve-local With serve-local
localhost:5173 one day, :5174 the next http://my-app.localhost:9999 — always
Remember which port each tool uses Zero memory overhead
Manually start each server Wake-on-access: visiting the URL starts it
Dead tool = broken URL Auto-restart on crash with circuit breaker protection
Orphaned Node processes holding ports Clean process tree termination on exit
Root-relative asset 404s in SPAs Intelligent fallback routing via Referer and cookies

Features

  • Stable subdomain routing — http://<tool-id>.localhost:9999 for every tool
  • Path routing with SPA support — http://localhost:9999/tool/<tool-id>/ seamlessly routes root-relative assets (/@vite/client, /src/App.tsx, etc.) via Referer and session cookies
  • Static and Node tools — serve plain HTML files or proxy to any node server.js / npm run dev
  • Wake on access — visiting a tool URL starts it automatically
  • Watchdog with circuit breaker — automatically restarts crashed tools, with configurable max_restarts to prevent CPU-eating crash loops
  • Persistent log buffers — per-tool stdout/stderr ring buffer survives crashes and process termination, visible directly in the UI
  • Process tree termination — cleanly terminates parent and all child processes (taskkill /F /T on Windows) so ports are never held by zombie processes
  • Unbuffered streaming proxy — full support for Server-Sent Events (SSE), LLM token streams, and chunked downloads with zero proxy delay or premature timeouts
  • WebSocket proxy — Vite HMR and WS-based tools work out of the box
  • Config hot-reload — add or edit scanned directories without restarting the server
  • Responsive launcher UI — fluid, mobile-friendly control panel with split-screen preview and live SSE updates
  • Env var injection — tools declare what secrets they need; the UI securely collects and provides them
  • HTTPS support — self-signed certificate generation for APIs that require a secure context
  • CLI — serve-local add, remove, install
  • Zero required dependencies — standard library only for the core runtime; optional extras unlock TLS and encryption

Quick start

Requirements: Python 3.11+. No pip install required for core features.

1. Clone the repository

git clone https://github.com/AccurateTLM13/serve-local.git
cd serve-local

2. Configure project directories (optional)

serve-local works out of the box, but you can configure root folders to automatically scan:

cp serve-local.config.example.json serve-local.config.json

Edit serve-local.config.json to point to where your projects live (e.g., ~/projects or C:\Users\<user>\projects).

3. Launching the server

Option A: Foreground terminal (Windows, macOS, Linux)

Run with standard Python:

python server_launcher.py

Or using uv:

uv run python server_launcher.py

The launcher starts the control server on port 9999 and automatically opens http://localhost:9999 in your default browser. Press Ctrl+C in the terminal to cleanly shut down the server and all managed tools.

Option B: Windows background launcher (.bat)

Double-click start-serve-local.bat in File Explorer. This uses pythonw to run headless in the background without keeping a command prompt window open.

Creating a Windows Desktop Shortcut:

  1. Right-click start-serve-local.bat in the project directory.
  2. Select Show more options > Send to > Desktop (create shortcut).
  3. (Optional) Right-click the shortcut on your desktop, select Properties, click Change Icon..., and choose an icon.
  4. Double-clicking the shortcut will start serve-local and launch your browser.

Tip

Stopping the background launcher on Windows: Because pythonw runs headless without a terminal window, you can stop it anytime via PowerShell:

Stop-Process -Name pythonw -ErrorAction SilentlyContinue

Or open Task Manager, find pythonw.exe, and select End Task.


How it works

serve-local runs a single Python HTTP server on port 9999.

Browser → http://my-tool.localhost:9999/ (or http://localhost:9999/tool/my-tool/)
              ↓
         serve-local control server (port 9999)
              ↓  (tool detected, auto-started on internal port if stopped)
         internal static or node server (e.g. port 8001, ephemeral)
              ↓
         your tool / SPA

1. Subdomain vs Path Routing

  • Subdomain routing: http://<tool-id>.localhost:9999 All modern browsers resolve *.localhost to 127.0.0.1 locally without DNS or /etc/hosts changes. This isolates cookies and origin storage per tool.
  • Path routing: http://localhost:9999/tool/<tool-id>/ Useful when subdomain wildcards are restricted. Visiting /tool/<tool-id> automatically issues a 301 Moved Permanently redirect to /tool/<tool-id>/ with a trailing slash to ensure relative URLs resolve correctly.

2. SPA & Root-Relative Asset Routing

Single Page Applications (Vite, Next.js, React) frequently issue root-relative requests (such as /@vite/client, /src/main.tsx, or /assets/index.js). When accessed via path routing, serve-local automatically detects these un-prefixed requests using:

  1. The incoming Referer header matching /tool/<tool-id>/...
  2. The _serve_local_tool session cookie set on visit

These requests are transparently routed to the active tool's upstream server, completely avoiding 404 errors on the control plane.

3. Streaming & Server-Sent Events (SSE)

serve-local streams HTTP chunks directly using unbuffered socket relaying (read1). Responses like Server-Sent Events (text/event-stream), LLM chat tokens, and large file downloads stream instantaneously without buffering in memory or dropping due to idle socket timeouts.

4. Process Tree Lifecycle & Zombie Prevention

When tools are stopped or restarted, serve-local ensures complete cleanup:

  • Windows: Executes taskkill /F /T /PID <pid> to terminate the entire process tree synchronously. This ensures child processes (such as node.exe or Vite dev servers spawned by npm.cmd) do not become orphaned background zombies holding onto ports.
  • Unix: Delivers graceful termination signals with immediate process pruning.

5. Watchdog & Circuit Breaker

If an auto-restarting tool crashes on boot (e.g. missing dependencies or code error), the watchdog monitors restart frequency. If crashes exceed max_restarts (default: 5), the circuit breaker trips, halting restart attempts to prevent 100% CPU usage. Crash logs and status remain accessible in the UI.


Configuration

Copy serve-local.config.example.json to serve-local.config.json (gitignored):

{
  "roots": [
    {
      "label": "My Projects",
      "path": "~/projects",
      "max_depth": 2
    }
  ],
  "projects": [
    {
      "label": "Special App",
      "path": "~/Desktop/my-app"
    }
  ],
  "exclude": ["node_modules", "dist", ".git"],
  "tls": false,
  "install_dir": ""
}
Field Type Description
roots array Directories to scan for project folders
roots[].path string Path to scan (supports ~ and environment variables)
roots[].label string Label shown in the launcher UI
roots[].max_depth int Scan depth for project folders (default: 1)
projects array Explicitly registered project folders (always included)
exclude array Directory names to skip during scanning
tls bool Enable HTTPS on port 9443 (requires cryptography)
install_dir string Where serve-local install <url> extracts tools

A project is any folder that contains at least one .html file, or a tool-manifest.json.

The configuration file is hot-reloaded: save changes to serve-local.config.json and the launcher updates within ~2 seconds without a restart.


Tool manifests

Drop an optional tool-manifest.json in any project folder for custom metadata, launch options, and backend configuration:

{
  "manifest_version": 1,
  "id": "my-tool",
  "display_name": "My Tool",
  "description": "One-line description.",
  "version": "1.0.0",
  "category": "Utilities",
  "tags": ["utility"],
  "pinned": false,

  "entry_file": "index.html",
  "default_route": "/index.html",
  "server_type": "static",

  "auto_start_on_access": true,
  "auto_restart": true,
  "max_restarts": 5,
  "preferred_port": 3001,

  "launch_routes": [
    { "label": "Main", "path": "/index.html" },
    { "label": "Settings", "path": "/settings.html" }
  ]
}

Node tools

{
  "server_type": "node",
  "command": "node",
  "args": ["server.js", "--port", "{{PORT}}"],
  "environment": {
    "PORT": "{{PORT}}",
    "PROJECT_PATH": "{{PROJECT_PATH}}"
  }
}

{{PORT}} is dynamically replaced with the internal port assigned by serve-local. {{PROJECT_PATH}} resolves to the folder path.

Environment variables prompt

Tools can declare secrets they need. The launcher UI prompts for them before starting the tool and securely stores them in ~/.serve-local/env-store.json:

{
  "env_prompt": [
    { "key": "OPENAI_API_KEY", "label": "OpenAI API Key", "secret": true },
    { "key": "BASE_URL", "label": "API Base URL", "secret": false }
  ]
}

Manifest fields reference

Field Type Default Description
manifest_version int — Format version. Use 1.
id string folder name Stable tool identity used in URLs (<id>.localhost:9999)
display_name string folder name Human-readable name shown in the UI
description string "" Description displayed in the registry
version string "" Version string
category string "Uncategorized" Category filter in the UI
tags array [] Searchable tag list
pinned bool false Pin tool to the top of the list
entry_file string auto-detected Main HTML file for static tools
default_route string auto Route opened on launch
server_type string "static" "static" or "node"
command string "node" Executable to run for Node/custom servers
args array [] Command line arguments (supports {{PORT}}, {{PROJECT_PATH}})
environment object {} Extra environment variables
auto_start_on_access bool true Start tool automatically when its URL is visited
auto_restart bool true Restart automatically if process exits unexpectedly
max_restarts int 5 Maximum watchdog restarts before tripping circuit breaker
preferred_port int dynamic Request a specific internal port (falls back if occupied)
launch_routes array [] Alternate routes shown in the UI menu
env_prompt array [] Secret credentials to prompt before launch
embedded bool false Hint: tool is designed for the split-panel iframe view

CLI

# Add a directory as a scanned root
python server_launcher.py add ~/my-projects --label "Work" --depth 2

# Add a single project explicitly
python server_launcher.py add ~/Desktop/my-app --project --label "My App"

# Remove a path from the config
python server_launcher.py remove ~/old-project

# Install a tool from a URL (expects a zip containing tool-manifest.json)
python server_launcher.py install https://example.com/my-tool.zip

HTTPS (optional)

Enable HTTPS for tools that require secure contexts (crypto.subtle, SharedArrayBuffer, secure WebSockets):

pip install cryptography

Set "tls": true in serve-local.config.json. On start, serve-local generates a self-signed certificate at ~/.serve-local/localhost.pem and binds HTTPS to port 9443.


Running tests

The test suite covers full request proxying, streaming chunk delivery, process tree management, watchdog circuit breaking, config hot-reloads, and responsive layout styles:

# Using standard pytest
pip install pytest
pytest tests/ -v

# Or using uv
uv run --with pytest pytest

All 67 tests run in under 3 seconds without requiring external services.


Project structure

serve-local/
├── server_launcher.py          # Entrypoint (thin — wires components and runs HTTP/S)
├── serve_local/
│   ├── config.py               # Config loading + mtime hot-reload watcher
│   ├── registry.py             # Project discovery, caching, slug/key helpers
│   ├── process_manager.py      # Process lifecycle, taskkill tree cleanup, watchdog breaker
│   ├── proxy.py                # Streaming HTTP proxy + WebSocket tunnel
│   ├── api_handler.py          # HTTP handler, path routing, SPA fallback routing
│   ├── sse.py                  # Server-Sent Events broker
│   ├── tls.py                  # Self-signed cert generation + HTTPS server
│   ├── env_store.py            # Encrypted env var storage
│   ├── cli.py                  # CLI: add / remove / install
│   └── constants.py            # Shared ports, paths, and constants
├── tests/
│   ├── test_config.py          # Config loading and hot reload tests
│   ├── test_registry.py        # Project discovery and slug tests
│   ├── test_process_manager.py # Process tree kill, circuit breaker, log persistence
│   ├── test_proxy.py           # Streaming chunk forwarding, SSE, headers
│   ├── test_tool_path_routing.py # Path routing, trailing slash redirect, SPA fallback
│   └── test_ui_layout.py       # Frontend CSS layout and responsiveness tests
├── index.html                  # Single-file control panel UI (vanilla HTML/CSS/JS)
├── serve-local.config.example.json
├── tool-manifest.example.json
├── start-serve-local.bat       # Windows background launcher
├── MANIFEST.md                 # tool-manifest.json field reference
├── requirements.txt
└── LICENSE

Contributing

See CONTRIBUTING.md.


License

MIT — see LICENSE.

About

Stable localhost addresses for your local HTML tools. No config required.

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages