Skip to content

Commit ad31fde

Browse files
committed
Merge feat/per-tool-enable-disable into halo-main
Fast-forward-compatible merge that brings halo-main up to the PR branch tip. Cherry-picks already on halo-main (f7694c8, f548de1, c3e99ac, ba2dcad) are byte-identical to their PR-branch originals (6032ed8, d584b16, b5f0a0c, 7bc7c73), so the 3-way merge resolves cleanly and the resulting tree matches the PR tip. Brings in: - Spec 047 (CPU hotpath fix + coalescer + SSE payload embed) - Spec 048 (tray refetch elimination) - Spec 046 (local launcher for HTTP/SSE upstreams + onboarding wizard v2) - Spec 044 phase H diagnostics counters - Spec 042 telemetry tier 2 finalization - The 3 PR-smart-mcp-proxy#463 commits that depend on the Spec 047 coalescer: - perf(runtime): build servers.changed payload lazily in coalescer drainer - fix(runtime): honour app-shutdown cancellation in coalescer drainer - fix(sse): plumb SecurityScan via management.ListServers - All other upstream-merged fixes between 2b9b5f9 and 7bc7c73 Authorized force-push equivalent: the user explicitly requested halo-main be set to the PR branch tip; force-push was blocked by the git MCP and the sandbox lacks raw-git auth, so we take the merge-commit path which achieves tree-equality without rewriting halo-main history. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> # Conflicts: # .gitignore # oas/docs.go
2 parents ba2dcad + 7bc7c73 commit ad31fde

192 files changed

Lines changed: 11870 additions & 668 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*.so
66
*.dylib
77
/mcpproxy
8+
/test/launcher-server/launcher-server
89
__debug_bin*
910

1011
# Playwright MCP artifacts
@@ -139,4 +140,4 @@ native/macos/MCPProxy/.build/
139140
# Wrangler session cache
140141
.wrangler/
141142

142-
.run
143+
.run

CLAUDE.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,53 @@ go test -race ./internal/... -v # Race detection
157157

158158
**E2E Prerequisites**: Node.js, npm, jq, built mcpproxy binary.
159159

160+
### Verifying Web UI changes (Playwright + rich HTML report)
161+
162+
When you modify the Web UI (any Vue file under `frontend/src/`), verify it end-to-end with a Playwright sweep that captures screenshots and packages them into a self-contained HTML report. This is the same workflow used to verify Spec 046 v2 — see `specs/046-local-first-onboarding/verification/` for a worked example.
163+
164+
The pattern, in order:
165+
166+
1. **Stand up a fresh mcpproxy.** Use a throwaway data-dir so persisted state doesn't bleed between runs:
167+
```bash
168+
pkill -f 'mcpproxy serve.*<port>' 2>/dev/null; sleep 1
169+
rm -rf /tmp/mcpproxy-uitest/{config.db,index.bleve,logs} 2>/dev/null
170+
cat > /tmp/mcpproxy-uitest/mcp_config.json <<'EOF'
171+
{ "listen": "127.0.0.1:18081", "data_dir": "/tmp/mcpproxy-uitest", "api_key": "uitest", "enable_web_ui": true, "enable_socket": false, "telemetry": {"enabled": false}, "mcpServers": [] }
172+
EOF
173+
./mcpproxy serve --config=/tmp/mcpproxy-uitest/mcp_config.json --listen=127.0.0.1:18081 --log-level=info > /tmp/mcpproxy-uitest/server.log 2>&1 &
174+
until curl -sf -H "X-API-Key: uitest" http://127.0.0.1:18081/api/v1/status >/dev/null; do sleep 1; done
175+
```
176+
2. **Reuse the existing Playwright install.** `e2e/playwright/node_modules` already has Playwright + Chromium 1217. Symlink it into your scratch dir:
177+
```bash
178+
mkdir -p /tmp/uitest && cd /tmp/uitest
179+
ln -sfn /Users/user/repos/mcpproxy-go/e2e/playwright/node_modules ./node_modules
180+
```
181+
3. **Pin the Chromium binary in `playwright.config.ts`** so Playwright doesn't try to download a different version:
182+
```ts
183+
import { defineConfig } from '@playwright/test';
184+
export default defineConfig({
185+
testDir: '.', timeout: 30000, fullyParallel: false, workers: 1, retries: 0,
186+
use: {
187+
headless: true,
188+
viewport: { width: 1440, height: 900 },
189+
launchOptions: {
190+
executablePath: '/Users/user/Library/Caches/ms-playwright/chromium-1217/chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing',
191+
},
192+
},
193+
});
194+
```
195+
4. **Write the spec.** Use `data-test` attributes already on the components (the project convention). For new components, add them. Drive scenarios with `page.locator('[data-test="..."]')`. Always use `page.waitForLoadState('domcontentloaded')` — `networkidle` hangs because of the SSE channel. Snapshot each state with `page.screenshot({ path: ... })`. Number screenshots in execution order so the report renders left-to-right.
196+
5. **Run.** `./node_modules/.bin/playwright test --reporter=list`. Iterate until green.
197+
6. **Build the rich HTML report.** A short Python script that base64-embeds each PNG and wraps it in a styled `<details>` per scenario produces a single self-contained HTML file the user can open offline. Pattern: top summary card with pass/fail counts, then one collapsible per scenario with `Expected` / `Observed` / inline screenshot. The reference implementation is `/tmp/wizard-v2-verify/build-report.py` from the v2 work — clone it and update the `SCENARIOS` list. Output goes to `specs/<feature>/verification/report.html`.
198+
7. **Drop screenshots + report alongside the spec.** Always commit them with the spec changes — they're part of the trace.
199+
8. **Surface the report.** End your reply with `open <path-to-report.html>` so the user can review without re-running the suite.
200+
201+
Key gotchas:
202+
- The wizard's `<dialog>` element renders as `[open]` only when the Vue store sets it. To assert open/closed state robustly, query the dialog property in `page.evaluate()`, not aria-hidden or styling.
203+
- `pkill -f 'mcpproxy ... <port>'` matches the merge-detection regex in user hooks. Ignore the `Merge-detection trigger` notifications when no actual merge happened.
204+
- The default config from a stub file does NOT trigger `applyFirstRunDockerIsolation` — that only runs when the config file is absent at boot. To test the "Docker auto-enabled" path, either let mcpproxy create the config or pre-set `docker_isolation.enabled: true` in your stub.
205+
- For browser-driven verification of subtle states (badge counts, empty/loaded transitions), prefer the Playwright spec over ad-hoc screenshots from the chrome-in-chrome MCP — the spec is reproducible and a CI agent can re-run it.
206+
160207
### Linting
161208
```bash
162209
./scripts/run-linter.sh # Requires golangci-lint v1.59.1+
@@ -719,6 +766,10 @@ See `docs/prerelease-builds.md` for download instructions.
719766
- No new persistent storage. Diagnostic state lives on in-memory stateview snapshot. Fix-attempt audit rows reuse existing activity log (`ActivityBucket` in BBolt). Telemetry counters are in-memory only (consistent with spec 042). (044-diagnostics-taxonomy)
720767
- Markdown (agent instruction files, wiki articles); optionally shell or AppleScript helpers for bootstrap idempotency + Paperclip AI (paperclipai/paperclip, MIT) running locally on loopback :3100; Synapbus on kubic; Anthropic API via Paperclip's Claude Code subprocess adapter (045-paperclip-cockpit)
721768
- Paperclip's embedded Postgres (existing, port 54329); Synapbus DB (existing); no new storage in mcpproxy-go (045-paperclip-cockpit)
769+
- Go 1.24 (toolchain go1.24.10); Swift 5.9 (macOS 13+); TypeScript 5.9 / Vue 3.5 (frontend) + `go.etcd.io/bbolt` (existing), `go.uber.org/zap` (existing), `github.com/mark3labs/mcp-go` (existing). No new deps. (047-cpu-hotpath-fix)
770+
- BBolt (`~/.mcpproxy/config.db`) — read-only on the hot path; no schema change. (047-cpu-hotpath-fix)
771+
- Swift 5.9 (macOS 13+); Go 1.24 only for the verification harness, no Go changes in scope. + SwiftUI/AppKit (existing), Combine (existing for the periodic timer pattern). No new deps. (048-tray-refetch-elimination)
772+
- None. Pure in-memory state. (048-tray-refetch-elimination)
722773
723774
## Recent Changes
724775
- 001-update-version-display: Added Go 1.24 (toolchain go1.24.10)

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,13 @@ sudo dnf config-manager --add-repo https://rpm.mcpproxy.app/mcpproxy.repo
7676
sudo dnf install -y mcpproxy
7777
```
7878

79+
Arch Linux (AUR): [`mcpproxy-bin`](https://aur.archlinux.org/packages/mcpproxy-bin)
80+
```bash
81+
yay -S mcpproxy-bin
82+
# or
83+
git clone https://aur.archlinux.org/mcpproxy-bin.git && cd mcpproxy-bin && makepkg -si
84+
```
85+
7986
Both packages ship a hardened `systemd` unit and start the service automatically. Repository signing key fingerprint: `3B6F A1AD 5D53 59DA 51F1 8DDC E1B5 9B9B A1CB 8A3B`.
8087

8188
For one-off `.deb` / `.rpm` downloads (air-gapped installs), grab them from the [latest release](https://github.com/smart-mcp-proxy/mcpproxy-go/releases/latest).

cmd/mcpproxy/connect_cmd.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ func GetConnectCommand() *cobra.Command {
2727
AI coding clients. This modifies the client's config file to add an HTTP/SSE
2828
entry pointing to the running MCPProxy instance.
2929
30-
Supported clients: claude-code, cursor, windsurf, vscode, codex, gemini
30+
Supported clients: claude-code, cursor, windsurf, vscode, codex, gemini, opencode
3131
3232
A backup of the original config file is created before any modification.
3333
@@ -36,6 +36,7 @@ Examples:
3636
mcpproxy connect claude-code # Register in Claude Code
3737
mcpproxy connect cursor --force # Overwrite existing entry
3838
mcpproxy connect codex --name my-proxy # Custom server name
39+
mcpproxy connect opencode # Register in OpenCode
3940
mcpproxy connect --all # Register in all supported clients`,
4041
Args: cobra.MaximumNArgs(1),
4142
RunE: runConnect,

cmd/mcpproxy/connect_cmd_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package main
2+
3+
import (
4+
"testing"
5+
6+
"github.com/smart-mcp-proxy/mcpproxy-go/internal/connect"
7+
)
8+
9+
func TestConnectClientRegistry_IncludesOpenCode(t *testing.T) {
10+
clients := connect.GetAllClients()
11+
found := false
12+
for _, c := range clients {
13+
if c.ID == "opencode" {
14+
found = true
15+
break
16+
}
17+
}
18+
if !found {
19+
t.Fatal("expected opencode in client registry")
20+
}
21+
}

contrib/linux-repos/apt-publish.sh

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ echo "[1/5] syncing bucket s3://${APT_BUCKET} -> ${workdir}"
5252
if [[ "${DRY_RUN}" == "true" ]]; then
5353
echo " dry-run: skipping sync-down"
5454
else
55-
aws s3 sync "s3://${APT_BUCKET}/" "${workdir}/" --no-progress
55+
aws s3 sync "s3://${APT_BUCKET}/" "${workdir}/" --quiet
5656
fi
5757

5858
# Set up standard directory structure even if empty
@@ -148,15 +148,15 @@ else
148148
# fetch Release + Packages.gz together, so that a user running `apt update`
149149
# never sees a Release referencing a stale Packages.gz.
150150
aws s3 sync "${workdir}/dists/" "s3://${APT_BUCKET}/dists/" \
151-
--cache-control "public, max-age=60, must-revalidate" --no-progress --delete
151+
--cache-control "public, max-age=60, must-revalidate" --quiet --delete
152152
# Pool artifacts are immutable
153153
aws s3 sync "${workdir}/pool/" "s3://${APT_BUCKET}/pool/" \
154-
--cache-control "public, max-age=31536000, immutable" --no-progress --delete
154+
--cache-control "public, max-age=31536000, immutable" --quiet --delete
155155
# Public key
156156
aws s3 cp "${workdir}/mcpproxy.gpg" "s3://${APT_BUCKET}/mcpproxy.gpg" \
157-
--cache-control "public, max-age=86400" --no-progress
157+
--cache-control "public, max-age=86400" --quiet
158158
aws s3 cp "${workdir}/mcpproxy.gpg.asc" "s3://${APT_BUCKET}/mcpproxy.gpg.asc" \
159-
--cache-control "public, max-age=86400" --no-progress
159+
--cache-control "public, max-age=86400" --quiet
160160
fi
161161

162162
echo "apt-publish: OK"

contrib/linux-repos/rpm-publish.sh

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ echo "[1/7] syncing bucket s3://${RPM_BUCKET} -> ${workdir}"
4242
if [[ "${DRY_RUN}" == "true" ]]; then
4343
echo " dry-run: skipping sync-down"
4444
else
45-
aws s3 sync "s3://${RPM_BUCKET}/" "${workdir}/" --no-progress
45+
aws s3 sync "s3://${RPM_BUCKET}/" "${workdir}/" --quiet
4646
fi
4747
mkdir -p "${workdir}/x86_64" "${workdir}/aarch64"
4848

@@ -157,12 +157,12 @@ else
157157
# Per-arch content: metadata short cache, rpms immutable
158158
for arch in x86_64 aarch64; do
159159
aws s3 sync "${workdir}/${arch}/repodata/" "s3://${RPM_BUCKET}/${arch}/repodata/" \
160-
--cache-control "public, max-age=60, must-revalidate" --no-progress --delete
160+
--cache-control "public, max-age=60, must-revalidate" --quiet --delete
161161
# rpms — upload individually with immutable cache (sync --cache-control is per-run)
162162
for f in "${workdir}/${arch}/"*.rpm; do
163163
[[ -e "${f}" ]] || continue
164164
aws s3 cp "${f}" "s3://${RPM_BUCKET}/${arch}/$(basename "${f}")" \
165-
--cache-control "public, max-age=31536000, immutable" --no-progress
165+
--cache-control "public, max-age=31536000, immutable" --quiet
166166
done
167167
# Delete rpms that no longer exist locally (after pruning)
168168
aws s3 ls "s3://${RPM_BUCKET}/${arch}/" --no-paginate \
@@ -172,16 +172,16 @@ else
172172
[[ -z "${remote}" ]] && continue
173173
if [[ ! -f "${workdir}/${arch}/${remote}" ]]; then
174174
echo " deleting stale ${arch}/${remote}"
175-
aws s3 rm "s3://${RPM_BUCKET}/${arch}/${remote}" --no-progress
175+
aws s3 rm "s3://${RPM_BUCKET}/${arch}/${remote}" --quiet
176176
fi
177177
done
178178
done
179179
aws s3 cp "${workdir}/mcpproxy.repo" "s3://${RPM_BUCKET}/mcpproxy.repo" \
180-
--cache-control "public, max-age=3600" --no-progress
180+
--cache-control "public, max-age=3600" --quiet
181181
aws s3 cp "${workdir}/mcpproxy.gpg" "s3://${RPM_BUCKET}/mcpproxy.gpg" \
182-
--cache-control "public, max-age=86400" --no-progress
182+
--cache-control "public, max-age=86400" --quiet
183183
aws s3 cp "${workdir}/mcpproxy.gpg.asc" "s3://${RPM_BUCKET}/mcpproxy.gpg.asc" \
184-
--cache-control "public, max-age=86400" --no-progress
184+
--cache-control "public, max-age=86400" --quiet
185185
fi
186186

187187
echo "rpm-publish: OK"

docs/cli-management-commands.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,14 @@ mcpproxy upstream restart --all
352352

353353
**Note:** Restart does not require confirmation as it's non-destructive.
354354

355+
**Locally-launched HTTP/SSE upstreams:** when a server is configured with both
356+
`command` and an HTTP/SSE `url` (see [docs/configuration.md](configuration.md#locally-launched-http--sse-servers)),
357+
`restart` stops the spawned child (`SIGTERM` → grace → `SIGKILL`) before
358+
re-running Connect. The grace timeout is fixed at 5s today; the next start
359+
won't begin until the previous child is fully reaped, so you can rely on the
360+
port being free after the command returns. Stop ordering is: close MCP client
361+
→ stop launched child → release per-server state.
362+
355363
---
356364

357365
### `mcpproxy doctor`

docs/configuration.md

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,8 +147,9 @@ MCPProxy looks for configuration in these locations (in order):
147147
| `args` | array | No | Command arguments |
148148
| `url` | string | Yes* | Server URL (required for `http`/`sse`/`streamable-http` protocols) |
149149
| `headers` | object | No | HTTP headers for HTTP-based protocols |
150-
| `working_dir` | string | No | Working directory for stdio servers (default: current directory) |
151-
| `env` | object | No | Environment variables for stdio servers |
150+
| `working_dir` | string | No | Working directory for stdio servers, or for the locally-launched child of an HTTP/SSE server (default: current directory) |
151+
| `env` | object | No | Environment variables for stdio servers, or for the locally-launched child of an HTTP/SSE server |
152+
| `launcher_wait_timeout` | duration | No | When `command` is set together with an HTTP/SSE `url`, how long mcpproxy waits for that URL to become reachable after spawning the child (e.g. `"15s"`, default `"30s"`) |
152153
| `oauth` | object | No | OAuth configuration (see [OAuth Configuration](#oauth-configuration)) |
153154
| `isolation` | object | No | Per-server Docker isolation settings (see [Docker Isolation](#docker-isolation)) |
154155
| `enabled` | boolean | No | Enable/disable server (default: `true`) |
@@ -210,6 +211,54 @@ MCPProxy looks for configuration in these locations (in order):
210211
}
211212
```
212213

214+
### Locally-launched HTTP / SSE servers
215+
216+
By default `command` is only used for `stdio` servers. When you set `command`
217+
together with an HTTP/SSE `url` and an explicit `protocol` of `http`, `sse`,
218+
or `streamable-http`, mcpproxy will:
219+
220+
1. Spawn the command (with `args`, `env`, `working_dir`, and Docker isolation
221+
exactly like a stdio server).
222+
2. Wait up to `launcher_wait_timeout` (default 30s) for `url` to accept a TCP
223+
connection.
224+
3. Connect via the configured HTTP/SSE transport.
225+
4. Own the child's lifecycle — the process is stopped (`SIGTERM`, then
226+
`SIGKILL` after a grace period) on disconnect, restart, server-disable, or
227+
mcpproxy shutdown. Unexpected exits trigger an automatic disconnect, which
228+
the existing reconnect path picks up.
229+
230+
```json
231+
{
232+
"name": "local-http-mcp",
233+
"protocol": "http",
234+
"url": "http://127.0.0.1:9999/mcp",
235+
"command": "node",
236+
"args": ["./examples/echo-http-server.js", "--port", "9999"],
237+
"working_dir": "/path/to/repo",
238+
"launcher_wait_timeout": "15s",
239+
"enabled": true
240+
}
241+
```
242+
243+
`stdout` and `stderr` of the child are routed to the per-server log, so
244+
`mcpproxy upstream logs <name>` continues to work the same way it does for
245+
stdio servers.
246+
247+
#### Behaviour matrix when both `command` and `url` are set
248+
249+
| `protocol` | `command` | `url` | Behaviour |
250+
|---|---|---|---|
251+
| `stdio` (explicit) | set | any | Stdio transport, child via stdin/stdout — `url` ignored. |
252+
| `http` / `sse` / `streamable-http` (explicit) | set | set | **Locally-launched HTTP/SSE** — spawn child, wait for URL, connect via network. |
253+
| `http` / `sse` / `streamable-http` (explicit) | unset | set | Connect to remote URL — no spawn. |
254+
| `auto` or unset | set | any | Stdio (`command` wins over `url` for back-compat — set `protocol` explicitly to opt into the launcher). |
255+
| `auto` or unset | unset | set | HTTP/SSE remote — no spawn. |
256+
257+
The "command wins" rule under `auto` is intentional: it preserves backwards
258+
compatibility with configurations written before the launcher feature
259+
existed. To launch a local HTTP/SSE server you **must** set `protocol`
260+
explicitly to one of `http`, `sse`, or `streamable-http`.
261+
213262
### OAuth Configuration
214263

215264
```json

docs/getting-started/installation.md

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ title: Installation
44
sidebar_label: Installation
55
sidebar_position: 1
66
description: Install MCPProxy on macOS, Windows, or Linux
7-
keywords: [install, setup, homebrew, dmg, windows, linux, deb, rpm, apt, dnf]
7+
keywords: [install, setup, homebrew, dmg, windows, linux, deb, rpm, apt, dnf, arch, aur]
88
---
99

1010
# Installation
@@ -73,6 +73,29 @@ sudo dnf install -y mcpproxy
7373

7474
Supported architectures: `x86_64`, `aarch64`.
7575

76+
### Arch Linux — AUR (community-maintained)
77+
78+
MCPProxy is available on the [Arch User Repository](https://aur.archlinux.org/) as [`mcpproxy-bin`](https://aur.archlinux.org/packages/mcpproxy-bin), which installs the official upstream binary plus a hardened systemd unit.
79+
80+
```bash
81+
# With an AUR helper (recommended)
82+
yay -S mcpproxy-bin
83+
84+
# Or manually with makepkg
85+
git clone https://aur.archlinux.org/mcpproxy-bin.git
86+
cd mcpproxy-bin
87+
makepkg -si
88+
```
89+
90+
The package installs `mcpproxy` to `/usr/bin` and ships a systemd user unit at `/usr/lib/systemd/user/mcpproxy.service`.
91+
92+
Enable it with:
93+
:::note Update cadence
94+
95+
Unlike the apt/dnf repositories above, AUR is community-driven: new versions land via the `mcpproxy-bin` PKGBUILD being bumped, not via a project-controlled mirror. The package is currently kept current by automation in the maintainer's [updater repo](https://github.com/JasonLandbridge/Arch-Linux-AUR-Packages-Updater), so bumps usually appear within a day of a GitHub release. If `yay` reports an old version after a recent release, you can flag the package "out-of-date" on AUR or fall back to the [Tarball install](#tarball-any-distro) below.
96+
97+
:::
98+
7699
### Debian / Ubuntu — direct `.deb` download (fallback)
77100

78101
If the apt repository isn't reachable (air-gapped installs, behind corporate proxies blocking `mcpproxy.app`, etc.), download the `.deb` from the [releases page](https://github.com/smart-mcp-proxy/mcpproxy-go/releases) and install it locally.

0 commit comments

Comments
 (0)