|
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' |
2 | 2 |
|
3 | 3 | // Event types for API service |
4 | 4 | export interface APIAuthEvent { |
@@ -33,6 +33,17 @@ export interface AddFromRegistryResult { |
33 | 33 | missingInputs?: string[] |
34 | 34 | } |
35 | 35 |
|
| 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 | + |
36 | 47 | class APIService { |
37 | 48 | private baseUrl = '' |
38 | 49 | private apiKey = '' |
@@ -667,6 +678,58 @@ class APIService { |
667 | 678 | return this.request<SearchRegistryServersResponse>(url) |
668 | 679 | } |
669 | 680 |
|
| 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 | + |
670 | 733 | // Spec 070 (CN-001): add a server to upstream by *reference* — the server |
671 | 734 | // re-derives and validates the config from the registry entry. The client no |
672 | 735 | // longer splits install_cmd / chooses protocol (that client-side parsing was |
|
0 commit comments