Skip to content

Commit 67f1379

Browse files
authored
feat(frontend): manage custom registries + drop alarming warnings (MCP-1073) (#595)
Web UI registry management for the v0.37.0 blocker: - Remove the one-time third-party warning gate, the "always quarantined / can never skip security review" copy, and the "third-party · unverified" badge. Adding a custom source now goes straight through. Provenance is surfaced as a neutral Official / Custom badge (MCP-1072 made it informational only). - Add a Registries management section: each configured source renders as a card. Official cards are read-only with a "Built-in" tag. Custom cards carry a kebab (⋮) menu with Edit and Delete. - Edit reuses the Add-Registry dialog pre-filled (id shown read-only), submitting to PUT /api/v1/registries/{id}. - Delete opens a destructive confirmation modal, then DELETE /api/v1/registries/{id}. Servers already added stay; only the source is removed. - api.ts: add editRegistrySource (PUT) + removeRegistrySource (DELETE) clients mirroring the addRegistrySource structured-error contract. Soften the multiselect suffix ("— unverified" → "— custom") and update docs/registries.md to describe the new neutral, edit/delete Web UI surface. Verified: 114 vitest pass (incl. new api-client + manage-registries specs); make build (frontend embed) green; Playwright CRUD sweep against a fresh instance (add → edit-persisted → delete, official read-only, no warning). Related MCP-1073
1 parent 2b6da58 commit 67f1379

6 files changed

Lines changed: 669 additions & 153 deletions

File tree

docs/registries.md

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,8 @@ Provenance is **informational only** (it no longer changes quarantine behavior):
5252
recorded on its config as `source_registry_id` / `source_registry_provenance`
5353
and surfaced in the approval/quarantine view.
5454
- The `list_registries` output (MCP, REST, CLI) includes `provenance` and a
55-
`trusted` boolean (derived `official == trusted`) so a UI can show a one-time
56-
third-party-registry warning.
55+
`trusted` boolean (derived `official == trusted`) so a UI can show a neutral
56+
**Official / Custom** badge.
5757
- **Migration:** earlier builds persisted the two-word tags `official/trusted` /
5858
`custom/unverified`; these are normalized to `official` / `custom` on read, so
5959
an existing `config.db` / `mcp_config.json` keeps working unchanged.
@@ -79,9 +79,12 @@ Equivalent surfaces:
7979
- **REST:** `POST /api/v1/registries` with `{ "url": "https://…", "protocol": "…", "id": "…", "name": "…" }`.
8080
- **CLI:** `mcpproxy registry add-source <https-url>`.
8181
- **Web UI:** the **Repositories** page has an **Add Registry** button (URL + optional
82-
protocol/name). Each registry in the selector is flagged **Official · trusted** or
83-
**Third-party · unverified** from its `provenance`, and the first custom add shows a
84-
one-time third-party-registry warning (the acknowledgement is remembered locally).
82+
protocol/name) and a **Registries** section listing every configured source as a
83+
card with a neutral **Official / Custom** badge (official cards also carry a
84+
**Built-in** tag). There is no warning gate — adding a custom source goes straight
85+
through. Custom cards expose a **kebab (⋮) menu** with **Edit** (reuses the
86+
add dialog, pre-filled, id read-only) and **Delete** (destructive confirmation);
87+
official cards are read-only.
8588
- **macOS tray:** the **Registries** sidebar tab lists every configured registry
8689
with its provenance/trust badge, offers an **Add Registry** affordance,
8790
and shows a one-time third-party warning before the first custom add.
@@ -109,6 +112,7 @@ Equivalent surfaces:
109112

110113
- **REST:** `DELETE /api/v1/registries/{id}``{ "registry": { … } }` echoing the removed entry.
111114
- **CLI:** `mcpproxy registry remove <id>`.
115+
- **Web UI:** the custom registry card's kebab (⋮) → **Delete**, behind a destructive confirmation modal.
112116

113117
Errors share a stable code across surfaces: `registry_not_found` (404),
114118
`registry_shadows_builtin` (409, built-in cannot be removed),
@@ -134,6 +138,7 @@ Equivalent surfaces:
134138

135139
- **REST:** `PUT /api/v1/registries/{id}` with `{ "name": "…", "url": "https://…", "servers_url": "https://…" }` (all optional) → `{ "registry": { … } }` echoing the updated entry.
136140
- **CLI:** `mcpproxy registry edit <id> [--name … --url … --servers-url …]`.
141+
- **Web UI:** the custom registry card's kebab (⋮) → **Edit**, which reuses the add dialog pre-filled with the current name/URL (id shown read-only).
137142

138143
Errors share a stable code across surfaces: `registry_not_found` (404),
139144
`registry_shadows_builtin` (409, built-in cannot be edited),

frontend/src/services/api.ts

Lines changed: 90 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -678,10 +678,10 @@ class APIService {
678678
return this.request<SearchRegistryServersResponse>(url)
679679
}
680680

681-
// MCP-866 / MCP-867: add a user-supplied registry source. The server always
682-
// tags an added source custom/unverified (provenance is NOT part of the
683-
// request), so every server later discovered through it lands quarantined and
684-
// can never skip quarantine. We mirror the structured-error pattern of
681+
// MCP-866 / MCP-867: add a user-supplied registry source. The server tags an
682+
// added source as custom provenance (provenance is NOT part of the request) —
683+
// informational only (MCP-1072); servers added from it follow the global
684+
// quarantine default like any other. We mirror the structured-error pattern of
685685
// addServerFromRegistry so the UI can branch on the stable `code`
686686
// (invalid_registry_url | registries_locked | registry_shadows_builtin |
687687
// duplicate_registry).
@@ -730,6 +730,92 @@ class APIService {
730730
}
731731
}
732732

733+
// MCP-1073: edit a user-added custom registry source via
734+
// PUT /api/v1/registries/{id}. Only the fields supplied are sent; empty fields
735+
// are left unchanged server-side. Built-in registries are read-only and the
736+
// backend refuses them with registry_shadows_builtin. Mirrors
737+
// addRegistrySource's structured-error contract (registry_not_found |
738+
// registry_shadows_builtin | invalid_registry_url | registries_locked).
739+
async editRegistrySource(
740+
id: string,
741+
opts: { name?: string; url?: string; serversUrl?: string }
742+
): Promise<AddRegistrySourceResult> {
743+
const body: Record<string, unknown> = {}
744+
if (opts.name) body.name = opts.name
745+
if (opts.url) body.url = opts.url
746+
if (opts.serversUrl) body.servers_url = opts.serversUrl
747+
748+
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
749+
if (this.apiKey) headers['X-API-Key'] = this.apiKey
750+
751+
try {
752+
const response = await fetch(`${this.baseUrl}/api/v1/registries/${encodeURIComponent(id)}`, {
753+
method: 'PUT',
754+
headers,
755+
body: JSON.stringify(body)
756+
})
757+
758+
const payload: any = await response.json().catch(() => ({}))
759+
760+
if (!response.ok) {
761+
if (response.status === 401 || response.status === 403) {
762+
if (payload?.code !== 'registries_locked') {
763+
this.emitAuthError(payload?.error || `HTTP ${response.status}`, response.status)
764+
}
765+
}
766+
return {
767+
success: false,
768+
error: payload?.error || `HTTP ${response.status}: ${response.statusText}`,
769+
code: payload?.code
770+
}
771+
}
772+
773+
return { success: true, registry: payload?.data?.registry }
774+
} catch (error) {
775+
return {
776+
success: false,
777+
error: error instanceof Error ? error.message : 'Unknown error'
778+
}
779+
}
780+
}
781+
782+
// MCP-1073: remove a user-added custom registry source via
783+
// DELETE /api/v1/registries/{id}. Servers already added from it stay; only the
784+
// source is removed. Built-in registries are read-only (registry_shadows_builtin).
785+
async removeRegistrySource(id: string): Promise<AddRegistrySourceResult> {
786+
const headers: Record<string, string> = {}
787+
if (this.apiKey) headers['X-API-Key'] = this.apiKey
788+
789+
try {
790+
const response = await fetch(`${this.baseUrl}/api/v1/registries/${encodeURIComponent(id)}`, {
791+
method: 'DELETE',
792+
headers
793+
})
794+
795+
const payload: any = await response.json().catch(() => ({}))
796+
797+
if (!response.ok) {
798+
if (response.status === 401 || response.status === 403) {
799+
if (payload?.code !== 'registries_locked') {
800+
this.emitAuthError(payload?.error || `HTTP ${response.status}`, response.status)
801+
}
802+
}
803+
return {
804+
success: false,
805+
error: payload?.error || `HTTP ${response.status}: ${response.statusText}`,
806+
code: payload?.code
807+
}
808+
}
809+
810+
return { success: true, registry: payload?.data?.registry }
811+
} catch (error) {
812+
return {
813+
success: false,
814+
error: error instanceof Error ? error.message : 'Unknown error'
815+
}
816+
}
817+
}
818+
733819
// Spec 070 (CN-001): add a server to upstream by *reference* — the server
734820
// re-derives and validates the config from the registry entry. The client no
735821
// longer splits install_cmd / chooses protocol (that client-side parsing was

0 commit comments

Comments
 (0)