Full Go port of local-mcp with all core features. Copied from local-mcp-go
worktree to consolidate into single-branch repo (easier maintenance).
Architecture:
- internal/config: Environment variable configuration
- internal/models: Shared types (Instruction, Settings, AgentActivity, etc.)
- internal/db: SQLite init with modernc.org/sqlite (pure Go, no CGo)
- internal/store: Database operations + WakeupSignal + AgentTracker
- internal/events: SSE broker for browser /api/events endpoint
- internal/mcp: get_user_request MCP tool with 5s keepalive progress bars
- internal/api: chi HTTP router with Bearer auth middleware
- main.go: Entry point with auto port switching and Windows interactive banner
Dependencies:
- github.com/mark3labs/mcp-go@v0.46.0
- github.com/go-chi/chi/v5@v5.2.5
- modernc.org/sqlite@v1.47.0 (pure Go SQLite)
- github.com/google/uuid@v1.6.0
Static assets embedded via //go:embed static
Features matching Python:
- Same wait strategy: 50s with 5s progress keepalives
- Same hardcoded constants (DEFAULT_WAIT_SECONDS, DEFAULT_EMPTY_RESPONSE)
- Auto port switching (tries 8000-8009)
- Windows interactive mode (formatted banner on double-click launch)
Build: cd go-server && go build -o local-mcp.exe .
Run: ./local-mcp.exe
Binary size: ~18 MB (vs Python ~60+ MB memory footprint)
Startup: ~10 ms (vs Python ~1-2s)
Changes:
- Add DEFAULT_WAIT_SECONDS = 50 (replaces config.default_wait_seconds)
- Add DEFAULT_EMPTY_RESPONSE = 'call this tool... ' (replaces config.default_empty_response)
- Remove wait and empty response fields from WebUI Settings panel
(only agent_stale_after_seconds remains configurable)
Rationale:
These values are fundamental to the 60s timeout mitigation strategy and
should not be user-configurable. The 50s wait with 5s keepalives is the
optimal balance for staying under the Copilot 60s wall-clock limit while
providing responsive progress feedback.
EXPERIMENT (NOT FOR PRODUCTION YET)
Changes:
- KEEPALIVE_INTERVAL_SECONDS reduced from 20s to 5s
- Keepalive messages now show progress bar with dots: ●●●●○○○○○○
- Show elapsed time, total wait, and remaining seconds
- Example: ⏳ Waiting for instructions... ●●●●○○○○○○ 20s / 50s (agent=copilot, 30s remaining)
Goal: Test if more frequent progress updates provide better UX and prevent
perceived freezing during the 50s wait. No functional change - the
60s client timeout limit remains the binding constraint.
git may invoke less as a pager which blocks the terminal waiting for
user interaction. Always use 'git --no-pager <command>' or pipe
through Out-String/Format-List in PowerShell sessions.
When the tool is waiting for an instruction, send ctx.info() log
notifications to the client every KEEPALIVE_INTERVAL_SECONDS (default 20).
Purpose
-------
These notifications write bytes to the SSE stream, resetting any
transport-level HTTP read timeout on the client side (e.g. httpx
Timeout(read=N)). This prevents premature connection drops when
wait periods exceed the client's inactivity window.
Caveat
------
Application-level wall-clock timers (anyio.fail_after / JS SDK
equivalents) are NOT affected by SSE events -- they count from
request start regardless. This is confirmed by experiments in
tests/test_keepalive.py and tests/run_keepalive_experiments.py.
Experiment results (summarised in tests/run_keepalive_experiments.py)
----------------------------------------------------------------------
- Exp 1: anyio.fail_after(5s) fires at 5.98s with NO keepalives.
- Exp 2: anyio.fail_after(10s) fires at 10.90s WITH keepalives every 2s.
Keepalives have ZERO effect on app-level timers.
- Exp 3b: httpx read=8s, keepalive=2s -> SUCCESS at 51s.
Keepalives DO prevent transport-level read timeouts.
The Copilot extension 60s limit is almost certainly application-level
(hardcoded wall-clock), so default_wait_seconds=50 remains the correct
mitigation (returns before the 60s deadline). The keepalives provide
defence-in-depth against any proxy/NAT inactivity drops.
On Windows the venv Python binary lives at .venv/Scripts/python.exe,
not .venv/bin/python. Fall back to the Windows path when the Unix
path does not exist so the script works cross-platform.
Wait time is now fully server-controlled via default_wait_seconds setting.
Agents can no longer request a different wait duration - only the user
controls this via the web UI.
- Remove wait_seconds param from get_user_request signature
- Simplify actual_wait to min(cfg.default_wait_seconds, MAX_WAIT)
- Update Settings panel label from 'Min Wait' to 'Wait (sec)'
- Update hint text to explain server-only control
- Update README: input schema, behavior rules, settings description, changelog