Skip to content

feat(web): surface connect backup path in wizard and Connect modal (Spec 078 US2)#799

Merged
Dumbris merged 2 commits into
mainfrom
feat/078-backup-visibility
Jul 2, 2026
Merged

feat(web): surface connect backup path in wizard and Connect modal (Spec 078 US2)#799
Dumbris merged 2 commits into
mainfrom
feat/078-backup-visibility

Conversation

@Dumbris

@Dumbris Dumbris commented Jul 2, 2026

Copy link
Copy Markdown
Member

Motivation

Part of Spec 078 (Connect Trust Preview) — this PR implements the backup-visibility slice (User Story 2: "Always-created backup is visible and understood", FR-005/FR-006). The backend already creates a timestamped backup before modifying a client config and returns it as ConnectResult.backup_path; the Web UI silently dropped it. This slice surfaces it.

This is a deliberate FIRST SLICE. It does not include:

  • US1 pre-connect change preview (FR-001–FR-004)
  • US3 one-click undo (FR-007–FR-009)
  • US4 plain-language trust copy / macOS prompt explanation (FR-010+)
  • Any backend changes — backup_path was already in the API contract (frontend/src/types/api.ts needed no change)

Implementation

  • ConnectModal.vue: after a successful connect or disconnect, the result area shows "A backup of your previous config was saved to <path>" with a one-click Copy path button (reuses the existing navigator.clipboard pattern). A success with no backup_path renders an explicit "No prior config file existed, so no backup was needed." instead of nothing.
  • OnboardingWizard.vue: the client row renders the same backup line (path + Copy path, or the no-prior-file message) after a connect performed in the wizard session — including per-client results from Connect All and connects where no config file existed. State tracked per-client in connectBackups; new backupPath/copiedBackup props + copy-backup emit on the functional ClientRow.
  • Test hooks: data-test="connect-backup-path" / "connect-no-backup" / "connect-copy-backup" (modal); "client-backup-<id>" / "client-copy-backup-<id>" (wizard).
  • Unit tests: frontend/tests/unit/connect-modal.spec.ts, frontend/tests/unit/onboarding-wizard-backup-path.spec.ts.

Codex review

Reviewed by codex; findings applied in 543cb98c:

  1. Wizard rows missed the backup line when connect happened with no prior config (no-config connect path wasn't bridged into connectBackups) — fixed.
  2. Connect All only recorded the last client's backup — now records per-client results.

No findings rebutted.

Verification

All steps run in the worktree at HEAD 543cb98c (on top of 3b339516):

  1. Build: make build exit 0 — frontend built and embedded, go build OK (v0.47.0-rc.1).
  2. Live API against the built binary with an isolated HOME/config (port 18093, /readyz READY): POST /api/v1/connect/claude-code with a pre-existing ~/.claude.json → success with a real timestamped backup_path; connect with no prior file → success without backup_path (drives the explicit no-backup message).
  3. Frontend unit tests for both surfaces pass (modal + wizard, incl. Connect All and no-backup cases).

Part of specs/078-connect-trust-preview

🤖 Generated with Claude Code

Dumbris and others added 2 commits July 2, 2026 14:03
…ct modal

First slice of specs/078-connect-trust-preview (backup visibility, spec
US2 / FR-005+FR-006): the backend has always written a timestamped backup
before modifying a client config and returned it as ConnectResult.backup_path
(internal/connect/backup.go, connect.go), and the CLI prints it — but the
Web UI dropped it. Now:

- ConnectModal.vue: after a successful connect OR disconnect, the result
  area shows "A backup of your previous config was saved to <path>" with a
  one-click "Copy path" button (same navigator.clipboard pattern as the
  existing copy-tccutil affordance). A success with no backup_path renders
  the explicit "No prior config file existed, so no backup was needed."
  message instead of a blank path (FR-006 no-prior-file case, e.g. bridge
  clients whose config is created by connect).
- OnboardingWizard.vue: ClientRow now renders the same backup line (path +
  Copy path, or the no-prior-file message) inside the client's row after a
  connect performed in the wizard session.

TDD: added 3 failing ConnectModal cases (backup path + copy, no-prior-file,
disconnect backup) to tests/unit/connect-modal.spec.ts and a new
tests/unit/onboarding-wizard-backup-path.spec.ts (2 cases) before
implementing; full frontend suite passes (32 files, 235 tests). Built via
make build so the Go binary embeds the updated web/frontend/dist.

Assumptions (zero-interruption policy):
- The TS contract already carries backup_path?: string on ConnectResult
  (frontend/src/types/api.ts) — no contract change needed.
- An empty-string backup_path on success is treated the same as absent
  (backend omits it when no prior file existed; never render a blank path).
- Preview/undo/TCC copy (spec US1/US3/US4) intentionally NOT built here.
- No tasks.md exists under specs/078-connect-trust-preview/ — nothing to
  check off; spec.md left untouched.
- npm-ci metadata churn in frontend/package-lock.json was reverted, not
  committed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ent Connect All backups

Codex review of the Spec 078 US2 slice found two majors and a minor; all
three verified against the code and fixed:

1. OnboardingWizard ClientRow rendered 'Not installed' for EVERY
   exists=false client, including bridge clients (Claude Desktop), which
   the backend explicitly marks connectable without a config file
   (internal/connect/connect.go:48; connect creates the file, returning an
   empty backup_path — connect_test.go:253). That made the wizard's
   "no prior file to back up" branch (FR-006 / US2 independent test)
   unreachable. ClientRow now gates on !exists && !bridge, matching
   ConnectModal's connectableClients predicate; non-bridge missing clients
   still show 'Not installed'.

2. ConnectModal connectAll() let each connect() overwrite
   resultBackupPath, so a bulk run over N modified configs displayed only
   the Nth backup (SC-005 requires 100%). connectAll now accumulates a
   per-client bulkBackups list ({name, path|no-prior-file} per successful
   connect, each with its own Copy affordance) and suppresses the single-
   result line for bulk runs. The loop also iterates a snapshot of
   connectableClients — connect() refetches the listing mid-loop, which
   mutated the computed while it was being iterated. Single connects
   (connectSingle) and disconnects clear the bulk list.

3. Minor: the wizard's open watcher reset connectMessage but not
   connectBackups/copiedBackupClient, so backup rows from a previous
   wizard session replayed on reopen. Both are now session-scoped.

TDD: 4 new failing cases first — wizard bridge-no-config connect,
wizard non-bridge stays 'Not installed', wizard reopen clears backup rows,
ConnectModal Connect All shows all three per-client outcomes (two paths +
one no-prior-file) with per-row copy. Full frontend suite: 32 files,
239 tests pass; vue-tsc clean; make build rebuilds the embedded dist;
golangci-lint v2 + go test -race ./internal/connect/... clean (no Go
changes in this commit).

npm-ci metadata churn in frontend/package-lock.json reverted again, not
committed (same as the base commit).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying mcpproxy-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: 543cb98
Status: ✅  Deploy successful!
Preview URL: https://e471c054.mcpproxy-docs.pages.dev
Branch Preview URL: https://feat-078-backup-visibility.mcpproxy-docs.pages.dev

View logs

@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown

📦 Build Artifacts

Workflow Run: View Run
Branch: feat/078-backup-visibility

Available Artifacts

  • archive-darwin-amd64 (28 MB)
  • archive-darwin-arm64 (25 MB)
  • archive-linux-amd64 (16 MB)
  • archive-linux-arm64 (14 MB)
  • archive-windows-amd64 (28 MB)
  • archive-windows-arm64 (25 MB)
  • frontend-dist-pr (0 MB)
  • installer-dmg-darwin-amd64 (21 MB)
  • installer-dmg-darwin-arm64 (19 MB)

How to Download

Option 1: GitHub Web UI (easiest)

  1. Go to the workflow run page linked above
  2. Scroll to the bottom "Artifacts" section
  3. Click on the artifact you want to download

Option 2: GitHub CLI

gh run download 28586067827 --repo smart-mcp-proxy/mcpproxy-go

Note: Artifacts expire in 14 days.

@Dumbris Dumbris merged commit b6fcb29 into main Jul 2, 2026
36 checks passed
@Dumbris Dumbris deleted the feat/078-backup-visibility branch July 2, 2026 15:37
Dumbris added a commit that referenced this pull request Jul 2, 2026
…t modal

Second slice of specs/078-connect-trust-preview US1 (FR-001,003,004): the
Connect action in BOTH the standalone Connect modal and the onboarding
wizard's ClientRow now fetches the preview first and shows an inline panel
before any file is written:

- target config path and, in a monospace additive ("+ will be added")
  block, the exact entry that will be written (pretty JSON, or TOML for
  Codex) with the API key masked;
- plain-language trust copy ("Only this entry is added — everything else in
  the file stays untouched. A timestamped backup is created first.");
- a masked-key line when contains_api_key so the user knows a credential is
  written;
- an overwrite warning when an entry with the same name already exists, in
  which case confirming implies force=true;
- graceful copy for the bridge/no-prior-file (access_state=absent) and
  unparseable (malformed) cases.

Confirm proceeds with the connect (reusing the existing #799 post-connect
backup-path line, untouched); Cancel dismisses the panel and writes nothing.
A non-denial preview fetch error is surfaced inline; in the modal a denied
read still resolves the Spec 075 access banner via checkAccess. Wizard
connect-step telemetry (markConnectCompleted/Skipped) is unchanged.

- types/api.ts: ConnectPreview; services/api.ts: getConnectPreview
- ConnectModal.vue + OnboardingWizard.vue ClientRow: preview panel + flow
- The existing #799 connect specs are updated to drive click → preview →
  confirm (Connect no longer writes on the first click); new
  connect-preview.spec.ts covers preview render, cancel-writes-nothing,
  confirm-proceeds, masked key, and overwrite→force for both surfaces.

Full frontend suite: 33 files, 244 tests pass; vue-tsc clean; make build
embeds the updated dist. npm-ci lockfile churn reverted (as in #799).

Related #793

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dumbris added a commit that referenced this pull request Jul 2, 2026
… the API key into client configs (Spec 078 US1) (#802)

* feat(connect): preview endpoint sharing buildServerEntry with the write path

First slice of specs/078-connect-trust-preview US1 (see spec FR-001..004,
012, 013). Adds GET /api/v1/connect/{client}/preview returning the exact
change a subsequent connect would make — target config_path, server_key,
entry name, and the exact entry contents — WITHOUT modifying the file or
creating a backup.

Preview==write guarantee: buildServerEntry is now the single construction
path for every client, including Codex/TOML (connectTOML previously built
its {url} entry inline). Preview derives its entry from the same function
the write uses, so what is previewed equals what is written (FR-002); a
table-driven test connects each supported client and asserts the written
file entry deep-equals the shared constructor output across JSON and TOML.

API-key honesty (FR-004): the embedded apikey is masked in the payload
(entry, entry_text) while contains_api_key flags that a credential is
written; the real key never appears in the preview (verified by test that
marshals the whole payload and asserts the secret is absent).

Spec 075 access states (FR-012): the on-demand read used to classify
create-vs-overwrite degrades to access_state=absent|malformed and a denial
returns the same typed AccessError (403 + remediation) as connect/disconnect.

- internal/connect/preview.go: Service.Preview, maskURLAPIKey, entry render
- internal/connect/clients.go: buildServerEntry gains the codex case
- internal/connect/connect.go: connectTOML uses buildServerEntry
- internal/httpapi: handler + route + tests (masked, no side effects, 403)
- oas: regenerated for the new endpoint (make swagger)

Tests: preview-equals-write (JSON+TOML), masking, entry_exists (create vs
overwrite), no-side-effects (no write, no backup, mtime unchanged), absent/
malformed/denied degradation. go test -race ./internal/connect/... and
./internal/httpapi/... pass; golangci-lint v2 clean.

Related #793

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(web): preview the exact change before writing in wizard + Connect modal

Second slice of specs/078-connect-trust-preview US1 (FR-001,003,004): the
Connect action in BOTH the standalone Connect modal and the onboarding
wizard's ClientRow now fetches the preview first and shows an inline panel
before any file is written:

- target config path and, in a monospace additive ("+ will be added")
  block, the exact entry that will be written (pretty JSON, or TOML for
  Codex) with the API key masked;
- plain-language trust copy ("Only this entry is added — everything else in
  the file stays untouched. A timestamped backup is created first.");
- a masked-key line when contains_api_key so the user knows a credential is
  written;
- an overwrite warning when an entry with the same name already exists, in
  which case confirming implies force=true;
- graceful copy for the bridge/no-prior-file (access_state=absent) and
  unparseable (malformed) cases.

Confirm proceeds with the connect (reusing the existing #799 post-connect
backup-path line, untouched); Cancel dismisses the panel and writes nothing.
A non-denial preview fetch error is surfaced inline; in the modal a denied
read still resolves the Spec 075 access banner via checkAccess. Wizard
connect-step telemetry (markConnectCompleted/Skipped) is unchanged.

- types/api.ts: ConnectPreview; services/api.ts: getConnectPreview
- ConnectModal.vue + OnboardingWizard.vue ClientRow: preview panel + flow
- The existing #799 connect specs are updated to drive click → preview →
  confirm (Connect no longer writes on the first click); new
  connect-preview.spec.ts covers preview render, cancel-writes-nothing,
  confirm-proceeds, masked key, and overwrite→force for both surfaces.

Full frontend suite: 33 files, 244 tests pass; vue-tsc clean; make build
embeds the updated dist. npm-ci lockfile churn reverted (as in #799).

Related #793

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(connect): stop leaking the API key into client configs; auth-aware carrier

Security fix folded into Spec 078. Connect (and the new preview) previously
appended ?apikey=<REST-admin key> to EVERY client entry via mcpURL(), but
/mcp does not require auth by default (config.RequireMCPAuth defaults false;
the admin context is granted unauthenticated). So connect leaked the
REST-admin API key in plaintext into other apps' config files where it
served no purpose.

New behavior, threaded from config.RequireMCPAuth into connect.Service the
same way listenAddr/apiKey are (server wiring + both CLI sites), via
WithRequireMCPAuth:

- require_mcp_auth=false (default): the entry carries NO credential — a clean
  http://host:port/mcp URL, no query, no headers. Existing configs keep
  working because /mcp accepts unauthenticated requests when protection is off.
- require_mcp_auth=true: the credential is written via the carrier each
  client's real config schema supports (verified against vendor docs):
    * X-API-Key header — claude-code, vscode, cursor, windsurf, gemini,
      opencode (all support a "headers" object on their remote entry);
    * mcp-remote --header "X-API-Key:<key>" bridge arg — claude-desktop
      (no space after the colon: Claude Desktop/Cursor don't escape spaces in
      args and mangle the header — geelen/mcp-remote README; our keys are
      space-free);
    * ?apikey= query fallback — codex only, whose TOML HTTP transport takes
      header values indirectly via env-var names (http_headers /
      bearer_token_env_var) and cannot express a literal header value.
  Server-side ExtractToken accepts X-API-Key, Authorization: Bearer, and
  ?apikey= (in that order), so header and query authenticate identically.

buildServerEntry now takes serverEntryParams{baseURL, credential}; mcpURL()
is replaced by the credential-free baseURL() anchor. Entry MATCHING
(findEntry*/findEquivalentJSONServerName/entryPointsToBridge) already keys on
baseURL with an optional ?query, so it recognizes BOTH legacy ?apikey=
entries and new clean entries — reconnect adopts/updates in place (no
duplicate) and disconnect still finds legacy entries; a force-reconnect over a
legacy entry upgrades it and drops the leaked key.

Preview reflects reality: contains_api_key is true only when require_mcp_auth
is on; the credential is masked in whichever carrier it uses (header value,
bridge arg, or query) and the real key never appears in the preview payload.

Tests: matrix over require_mcp_auth on/off × all 8 clients for both write and
preview (preview==write shape, auth-off writes contain NO apikey/headers at
all); per-client carrier assertions; legacy ?apikey= migration (recognized,
upgraded in place, no duplicate, disconnect still works). go test -race
./internal/connect/... ./internal/httpapi/... pass; server/config/management
pass; golangci-lint v2 clean.

Related #793

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(web): carrier-agnostic credential copy in the connect preview

Follows the connect security fix: the preview panel's masked-credential line
now reads "This entry includes your API key (shown masked)…" instead of "The
URL includes…", since with require_mcp_auth on the credential is carried in an
X-API-Key header (or the mcp-remote --header bridge arg) for most clients and
only falls back to the ?apikey= URL query for Codex. The line already renders
only when contains_api_key is true, which the backend now sets only when
require_mcp_auth is on — so the default (keyless) connect shows no credential
line at all.

Adds a frontend test for the default keyless case: contains_api_key=false ⇒
no credential line, and the previewed entry shows neither an apikey query nor
a mask. Full frontend suite: 33 files, 245 tests pass; vue-tsc clean.

Related #793

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(connect): live-config carrier + honest preview for edge cases

Codex review of the Spec 078 US1 connect-preview branch surfaced one blocker
and three preview/write-divergence gaps; this addresses them:

- blocker: the HTTP connect service snapshotted require_mcp_auth (and
  listen/api_key) once at startup and was never rebuilt, while the /mcp auth
  middleware honors require_mcp_auth live per-request. Toggling auth off at
  runtime (wizard toggle or hot-reload) left the service embedding the real
  API key into client configs — the exact leak this branch closes. Add a
  live config-provider seam (WithConfigProvider) wired to runtime.Config() so
  preview and write always match the currently-enforced auth state.

- OpenCode preview divergence: previewEntryExists checked only the exact
  server name, but OpenCode's write path adopts/normalizes an equivalent entry
  (incl. the legacy ?apikey= shape) under a different key. Preview now mirrors
  that via findEquivalentJSONServerName so entry_exists reflects the overwrite.

- preview endpoint ignored server_name and always previewed "mcpproxy" while
  POST connect accepts a custom name. Honor an optional ?server_name= query so
  a caller previewing then connecting under a custom name does not diverge.

- malformed config: the write parses the same bytes and would fail, but the
  preview copy promised it "still writes only the entry" and left Connect
  enabled. Make the copy honest and disable Connect for malformed configs.

Tests: live-toggle credential (auth off->on without rebuild), OpenCode
equivalent-entry preview, server_name honoring, and malformed confirm-disabled.
go test -race ./internal/connect/... ./internal/httpapi/... green; frontend
246 tests + vue-tsc clean; golangci-lint v2 ./... 0 issues; swagger regenerated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(web): make the first connect click explicitly commitment-free

The preview only appeared AFTER clicking "Connect" — the exact button
hesitant users avoid, fearing the click immediately modifies their
config. Rename the row action to "Review & connect" and say up front
in the modal subtitle and wizard intro that nothing is written until
the confirm step, so the safety of the first click is visible before
it happens (Spec 078 US1 trust copy).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(web): align setup.md and test prose with the Review & connect flow

Codex review follow-up: docs/setup.md described the Claude Desktop
bridge registration as one-click "Connect"; it is now review-then-
confirm. Test comments updated to match the renamed row action.
Spec 046/075 texts intentionally left untouched — they are point-in-
time records (046 FR-005 already required the diff preview this
branch implements).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Dumbris added a commit that referenced this pull request Jul 2, 2026
…ne, connect-trust/upgrade-nudge progress (#803)

check-github now passes with 0 errors: scanner-simplification epic
complete (#786/#792/#793/#794 incl. deep-scan trust fixes + docs
sweep); connect-trust US1 preview (#802) + backup visibility (#799)
done; upgrade-nudge status/log slice (#798) split out as done with
the banner+config remainder tracked separately; telemetry machine_id
client (#796) and hygiene check-github (#800) done. Remaining
warnings are the known windows-tray no-PR-evidence items.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants