Skip to content

Commit f119f48

Browse files
feat(registries): Web UI add-registry affordance + provenance surfacing + one-time third-party warning (MCP-867) (#575)
Frontend slice of MCP-856 / MCP-866. Surfaces the user-added-registries backend in the Repositories page: - Add Registry affordance (modal): https URL + optional protocol (defaults to modelcontextprotocol/registry) + optional name; POSTs /api/v1/registries via the new api.addRegistrySource(), mapping the stable error codes (invalid_registry_url / registries_locked / registry_shadows_builtin / duplicate_registry) to actionable messages. - Provenance surfacing: each registry is flagged "Official · trusted" or "Third-party · unverified" from its provenance/trusted fields; custom sources carry an always-quarantined note. - One-time third-party warning gating the first custom add; the acknowledgement is remembered in localStorage. Tests: api.addRegistrySource client contract + Repositories warning-gating, ack persistence, provenance badges, and error mapping. Docs updated in docs/registries.md. Co-authored-by: Paperclip <noreply@paperclip.ing>
1 parent fa8efc5 commit f119f48

6 files changed

Lines changed: 666 additions & 5 deletions

File tree

docs/registries.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,14 @@ Equivalent surfaces:
6767

6868
- **REST:** `POST /api/v1/registries` with `{ "url": "https://…", "protocol": "…", "id": "…", "name": "…" }`.
6969
- **CLI:** `mcpproxy registry add-source <https-url>`.
70+
- **Web UI:** the **Repositories** page has an **Add Registry** button (URL + optional
71+
protocol/name). Each registry in the selector is flagged **Official · trusted** or
72+
**Third-party · unverified** from its `provenance`, and the first custom add shows a
73+
one-time third-party-registry warning (the acknowledgement is remembered locally).
7074

7175
Errors share a stable code across surfaces: `invalid_registry_url` (400),
7276
`registries_locked` (403), `registry_shadows_builtin` / `duplicate_registry` (409).
77+
The Web UI maps each code to an actionable message.
7378

7479
### Enterprise: `registries_locked` (stub)
7580

frontend/src/services/api.ts

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { APIResponse, Server, Tool, ToolApproval, SearchResult, StatusUpdate, SecretRef, MigrationAnalysis, ConfigSecretsResponse, GetToolCallsResponse, GetToolCallDetailResponse, GetServerToolCallsResponse, GetConfigResponse, ValidateConfigResponse, ConfigApplyResult, ServerTokenMetrics, GetRegistriesResponse, SearchRegistryServersResponse, GetSessionsResponse, GetSessionDetailResponse, InfoResponse, ActivityListResponse, ActivityDetailResponse, ActivitySummaryResponse, ImportResponse, AgentTokenInfo, CreateAgentTokenRequest, CreateAgentTokenResponse, RoutingInfo, ConnectStatusResponse, ConnectResult, OnboardingStateResponse, OnboardingMarkRequest, DiagnosticFixResponse, GlobalToolsResponse, UsageAggregateResponse, UsageWindow, UsageSort, UsageStatus } from '@/types'
1+
import type { APIResponse, Server, Tool, ToolApproval, SearchResult, StatusUpdate, SecretRef, MigrationAnalysis, ConfigSecretsResponse, GetToolCallsResponse, GetToolCallDetailResponse, GetServerToolCallsResponse, GetConfigResponse, ValidateConfigResponse, ConfigApplyResult, ServerTokenMetrics, GetRegistriesResponse, SearchRegistryServersResponse, RegistrySummary, GetSessionsResponse, GetSessionDetailResponse, InfoResponse, ActivityListResponse, ActivityDetailResponse, ActivitySummaryResponse, ImportResponse, AgentTokenInfo, CreateAgentTokenRequest, CreateAgentTokenResponse, RoutingInfo, ConnectStatusResponse, ConnectResult, OnboardingStateResponse, OnboardingMarkRequest, DiagnosticFixResponse, GlobalToolsResponse, UsageAggregateResponse, UsageWindow, UsageSort, UsageStatus } from '@/types'
22

33
// Event types for API service
44
export interface APIAuthEvent {
@@ -33,6 +33,17 @@ export interface AddFromRegistryResult {
3333
missingInputs?: string[]
3434
}
3535

36+
// MCP-866 / MCP-867: result of adding a *registry source* (POST /registries).
37+
// Carries the stable error `code` (invalid_registry_url | registries_locked |
38+
// registry_shadows_builtin | duplicate_registry) so the UI can render an
39+
// actionable message instead of a generic string.
40+
export interface AddRegistrySourceResult {
41+
success: boolean
42+
registry?: RegistrySummary
43+
error?: string
44+
code?: string
45+
}
46+
3647
class APIService {
3748
private baseUrl = ''
3849
private apiKey = ''
@@ -667,6 +678,58 @@ class APIService {
667678
return this.request<SearchRegistryServersResponse>(url)
668679
}
669680

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
685+
// addServerFromRegistry so the UI can branch on the stable `code`
686+
// (invalid_registry_url | registries_locked | registry_shadows_builtin |
687+
// duplicate_registry).
688+
async addRegistrySource(
689+
url: string,
690+
opts?: { protocol?: string; id?: string; name?: string }
691+
): Promise<AddRegistrySourceResult> {
692+
const body: Record<string, unknown> = { url }
693+
if (opts?.protocol) body.protocol = opts.protocol
694+
if (opts?.id) body.id = opts.id
695+
if (opts?.name) body.name = opts.name
696+
697+
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
698+
if (this.apiKey) headers['X-API-Key'] = this.apiKey
699+
700+
try {
701+
const response = await fetch(`${this.baseUrl}/api/v1/registries`, {
702+
method: 'POST',
703+
headers,
704+
body: JSON.stringify(body)
705+
})
706+
707+
const payload: any = await response.json().catch(() => ({}))
708+
709+
if (!response.ok) {
710+
if (response.status === 401 || response.status === 403) {
711+
// registries_locked is a 403 but is a policy decision, not an auth
712+
// failure — only emit the auth-error path for a missing/invalid key.
713+
if (payload?.code !== 'registries_locked') {
714+
this.emitAuthError(payload?.error || `HTTP ${response.status}`, response.status)
715+
}
716+
}
717+
return {
718+
success: false,
719+
error: payload?.error || `HTTP ${response.status}: ${response.statusText}`,
720+
code: payload?.code
721+
}
722+
}
723+
724+
return { success: true, registry: payload?.data?.registry }
725+
} catch (error) {
726+
return {
727+
success: false,
728+
error: error instanceof Error ? error.message : 'Unknown error'
729+
}
730+
}
731+
}
732+
670733
// Spec 070 (CN-001): add a server to upstream by *reference* — the server
671734
// re-derives and validates the config from the registry entry. The client no
672735
// longer splits install_cmd / chooses protocol (that client-side parsing was

frontend/src/types/api.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,28 @@ export interface Registry {
562562
tags?: string[]
563563
protocol?: string
564564
count?: number | string
565+
// MCP-866: trust tag — "official/trusted" for built-in defaults,
566+
// "custom/unverified" for user-added sources. Derived server-side from
567+
// membership in the default set, never from self-assertion in config.
568+
provenance?: string
569+
// Convenience boolean mirror of provenance === "official/trusted".
570+
trusted?: boolean
571+
}
572+
573+
// MCP-866 trust-tag constants (mirror config.RegistryProvenance*).
574+
export const REGISTRY_PROVENANCE_OFFICIAL = 'official/trusted'
575+
export const REGISTRY_PROVENANCE_CUSTOM = 'custom/unverified'
576+
577+
// RegistrySummary is the slim projection returned by POST /api/v1/registries
578+
// (add-source). Mirrors contracts.RegistrySummary.
579+
export interface RegistrySummary {
580+
id: string
581+
name: string
582+
url?: string
583+
servers_url?: string
584+
protocol?: string
585+
provenance?: string
586+
trusted?: boolean
565587
}
566588

567589
export interface NPMPackageInfo {

0 commit comments

Comments
 (0)