Commit 31e4887
feat(launcher): local launcher for HTTP/SSE upstreams (spec 046) (#452)
* docs(spec): plan 046 — local launcher for HTTP/SSE upstreams
Adds the implementation plan for letting mcpproxy spawn a local
command before connecting to its HTTP/SSE endpoint. Today the
command field is silently ignored when protocol is http/sse/
streamable-http; this plan decouples launcher from transport so
{command, url, protocol: http|sse|streamable-http} becomes a
first-class config combo (spawn, wait for URL, connect, own
lifecycle on disconnect/restart/shutdown).
Plan-only commit — no code yet. Phase 0 of the plan is a no-
behaviour-change refactor that lifts env/Docker/working-dir
plumbing out of connection_stdio.go into a new internal/upstream/
launcher/ package; Phase 1 wires it into the HTTP/SSE connect
path; Phase 2 lands tests + docs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(launcher): add internal/upstream/launcher package (Phase 0)
New self-contained package that mcpproxy will use to spawn the local
upstream process behind HTTP/SSE/streamable-HTTP transports. Phase 0
of spec 046: the launcher is fully implemented and tested, but no
Connect/Disconnect path uses it yet — that wiring lands in the next
commit so reviewers can read the lifecycle module on its own.
API surface:
- Spec — caller-built *exec.Cmd plus LogSink/Name/StopGrace.
- Handle — Stop/Wait/Done/Pid; Stop is idempotent and waits
for the child to be reaped before returning so
"Stop returned -> port is free" is reliable.
- Spawn(ctx,spec,log) — owns child lifecycle from cmd.Start onward.
Pumps stdout+stderr line-by-line into LogSink
(one Write per line, serialized internally so an
arbitrary io.Writer is safe).
- WaitForURL(ctx,url,timeout) — TCP-dial polling, NOT http.Get
(gotcha #2 in the spec: SSE GETs stream forever).
Infers default ports for http/https/ws/wss.
Process group handling is unix-only via applyProcAttrs (Setpgid +
Pgid=0), so SIGTERM/SIGKILL reach grandchildren spawned by sh -c ...
and docker run .... Windows gets best-effort stubs that match the
process_windows.go TODO already in core/.
Tests: 15 cases covering immediate/late/never-bound listeners, ctx
cancel, bad-URL parse rejection, default-port inference, graceful
SIGTERM exit, SIGKILL fallback after StopGrace, natural exit code
capture, idempotent Stop under concurrent callers, and LogSink
capture. Integration test exercises Spawn + WaitForURL together with
a python listener subprocess (skipped if python3 missing).
No behaviour change in this commit — the package is dead code until
the next one.
Refs spec: specs/046-local-launcher-for-http-sse/plan.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(upstream): launch local HTTP/SSE upstreams via launcher (Phase 1)
Wires internal/upstream/launcher into Client.Connect for the http,
sse, and streamable-http transports when ServerConfig.Command is also
set. The launcher spawns the child process, blocks on
launcher.WaitForURL until the listener accepts a TCP connection, then
hands off to the existing transport-level connectHTTP / connectSSE.
On Disconnect, the MCP client is closed first (so the child sees the
network drop cleanly) and the launcher Handle is Stop()ed second
(SIGTERM -> grace -> SIGKILL).
Stdio is deliberately excluded: mcp-go's Stdio transport spawns its
own child via its CommandFunc, and routing that through launcher.Spawn
would require patching mcp-go. Instead, both the stdio CommandFunc
path and the new HTTP/SSE launcher path call into the same set of
*Client command-prep helpers (setupDockerIsolation, wrapWithUserShell,
injectEnvVarsIntoDockerArgs, insertCidfileIntoShellDockerCommand),
preserving the spec's "Docker isolation in one place" requirement
without the riskier stdio refactor.
Config / API:
- ServerConfig.LauncherWaitTimeout (Duration, default 30s) caps the
wait between spawn and listener-up. CopyServerConfig carries it.
- Client gains launcherHandle / launcherCIDFile fields. Stdio Clients
leave them nil so existing cleanup paths (killProcessGroup,
processCmd.Process.Kill) keep their single owner.
Behaviour back-compat: when Command is set together with a URL but
no explicit protocol, Command still wins -> stdio, URL ignored
(matches today). To opt into the launcher you must set protocol to
"http", "sse", or "streamable-http" explicitly.
Locking: Connect holds c.mu for its duration, so the failure-cleanup
path inlines the launcher teardown and releases c.mu briefly around
handle.Stop (which can block until the child is reaped). The
connecting flag prevents a concurrent Connect from sneaking in
during that window. DisconnectWithContext releases c.mu before its
stopLauncher call, so the public method uses normal locking.
Watch goroutine: on unexpected child exit while connected,
watchLauncher invokes Disconnect so the existing reconnect loop in
internal/upstream/manager takes over instead of waiting for the
transport's keepalive to time out.
Verified by go vet + the full internal/upstream/... and
internal/config/... test suites (existing tests stay green; deadlock
in connect-failure cleanup that surfaced in TestClient_Connect_SSE_
NotSupported was fixed before commit).
Refs spec: specs/046-local-launcher-for-http-sse/plan.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(spec): cover local-launcher HTTP/SSE config + restart semantics (Phase 2)
- docs/configuration.md: new "Locally-launched HTTP / SSE servers"
section walks through the {command, url, protocol: http|sse|
streamable-http} combo, a config example with launcher_wait_timeout,
per-server log routing, and a back-compat behaviour matrix that
makes the "command + url under auto -> stdio wins" footgun explicit.
- docs/cli-management-commands.md: restart semantics note covering the
launcher stop-then-start order and the 5s SIGKILL grace timeout.
- specs/046-local-launcher-for-http-sse/execution_log.md: design
decisions, deviations from the original plan (stdio refactor scoped
down to shared command-prep helpers), bugs found and fixed during
verification (connect deadlock, bytes.Buffer race, banner false-
positive in SIGKILL test, ftp-with-port acceptance), and the
exact verification commands.
Refs spec: specs/046-local-launcher-for-http-sse/plan.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(e2e): add launcher-lifecycle gate driving a real HTTP MCP fixture
Closes the gap unit tests can't: proves a built mcpproxy binary actually
spawns a self-launched HTTP MCP server, completes the MCP handshake,
serves tools/list through the proxy, and reaps the child cleanly on
disable / restart / shutdown.
What's new:
- test/launcher-server/main.go — minimal HTTP MCP fixture (~220 LOC).
Implements initialize, tools/list (returns a "ping" tool),
tools/call, plus 405-on-GET so mcp-go's StreamableHTTP transport
falls back to POST-only. Exits cleanly on SIGTERM (5s shutdown).
Heartbeat to stdout proves the per-server log pump works.
- test/e2e-config.template.json — new "launcher-test" upstream
({protocol: http, command: ./test/launcher-server/launcher-server,
url: http://127.0.0.1:39933/mcp, launcher_wait_timeout: 10s}).
- scripts/test-api-e2e.sh:
* Prereq step now `go build`s the fixture before booting mcpproxy.
* New wait_for_launcher_test_server() polls /servers until
launcher-test reports connected.
* New test_launcher_lifecycle() runs six sub-assertions:
1. tools/list returns the fixture's "ping" tool through the
proxy (proves Spawn -> WaitForURL -> connectHTTP -> MCP
initialize -> ListTools end-to-end).
2. pgrep finds the child process by argv.
3. POST /servers/launcher-test/restart yields a different PID.
4. POST /servers/launcher-test/disable reaps the child within 8s.
5. POST /servers/launcher-test/enable respawns it with a fresh
PID.
6. /servers/launcher-test/logs?tail=200 contains the launcher
banner or fixture stdout.
* cleanup() pkill's any leaked launcher-server processes so a
failure mid-test doesn't taint the next run.
.gitignore: ignore the built fixture binary
(/test/launcher-server/launcher-server). Source stays tracked.
Refs spec: specs/046-local-launcher-for-http-sse/plan.md (Phase 2 e2e
follow-up that was listed as outstanding in the PR test plan).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>1 parent a11d002 commit 31e4887
21 files changed
Lines changed: 2323 additions & 14 deletions
File tree
- docs
- internal
- config
- upstream
- core
- launcher
- scripts
- specs/046-local-launcher-for-http-sse
- test
- launcher-server
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| 8 | + | |
8 | 9 | | |
9 | 10 | | |
10 | 11 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
352 | 352 | | |
353 | 353 | | |
354 | 354 | | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
355 | 363 | | |
356 | 364 | | |
357 | 365 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
147 | 147 | | |
148 | 148 | | |
149 | 149 | | |
150 | | - | |
151 | | - | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
152 | 153 | | |
153 | 154 | | |
154 | 155 | | |
| |||
210 | 211 | | |
211 | 212 | | |
212 | 213 | | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
213 | 262 | | |
214 | 263 | | |
215 | 264 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
224 | 224 | | |
225 | 225 | | |
226 | 226 | | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
227 | 234 | | |
228 | 235 | | |
229 | 236 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
528 | 528 | | |
529 | 529 | | |
530 | 530 | | |
531 | | - | |
532 | | - | |
533 | | - | |
534 | | - | |
535 | | - | |
536 | | - | |
537 | | - | |
538 | | - | |
539 | | - | |
540 | | - | |
541 | | - | |
| 531 | + | |
| 532 | + | |
| 533 | + | |
| 534 | + | |
| 535 | + | |
| 536 | + | |
| 537 | + | |
| 538 | + | |
| 539 | + | |
| 540 | + | |
| 541 | + | |
| 542 | + | |
542 | 543 | | |
543 | 544 | | |
544 | 545 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
19 | 19 | | |
20 | 20 | | |
21 | 21 | | |
| 22 | + | |
22 | 23 | | |
23 | 24 | | |
24 | 25 | | |
| |||
98 | 99 | | |
99 | 100 | | |
100 | 101 | | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
101 | 110 | | |
102 | 111 | | |
103 | 112 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3 | 3 | | |
4 | 4 | | |
5 | 5 | | |
| 6 | + | |
6 | 7 | | |
7 | 8 | | |
8 | 9 | | |
| |||
118 | 119 | | |
119 | 120 | | |
120 | 121 | | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
121 | 137 | | |
122 | 138 | | |
123 | 139 | | |
| |||
183 | 199 | | |
184 | 200 | | |
185 | 201 | | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
186 | 232 | | |
187 | 233 | | |
188 | 234 | | |
| |||
0 commit comments