PR #3293
Sections
Review

feat: add workspace MCP configuration

main ← feature/plan-workspace-mcp-c-dcc 165 files +8420 −1180 PR #3293 ↗

Workspace MCP moves from profile JSON to a workspace catalog with scoped selections, effective resolution, and idle-only session updates.

Why this change

MCP servers live in profile JSON and apply to every workspace. Teams cannot share a workspace catalog, pick servers per repository, task, or session, or update a running session safely.

What it does

Architecture, end to end

Workspace owns the catalog. Selections from four scopes merge in the resolver. The lifecycle applies changes only when idle.

flowchart LR
  UI[Workspace MCP Settings] --> Catalog[CatalogService]
  Marketplace[MCP Registry] --> Catalog
  Legacy[Legacy Profile JSON] --> Importer[LegacyImporter]
  Importer --> Catalog
  Catalog --> Selections[SelectionService]
  Selections --> Resolver[Resolver]
  Resolver --> Lifecycle[Lifecycle Manager]
  Lifecycle --> Agent[Agent Session]
  Resolver -- secrets --> Secrets[Secret Resolver]
  Resolver -- policy --> Policy[Transport Policy]

Key code changes

Drag to pan. Use the + and − buttons to zoom. Click a node to open the full code. The arrows show how the parts interact.

drag to pan · +/− to zoom · click a node for details

Backend
Frontend
type MCPServerDefinition struct
Click for details →

The catalog stores workspace-owned definitions with execution mode, transport, and secret bindings.

Definition
type MCPServerDefinition struct {
  ID                    string                 `json:"id"`
  WorkspaceID           string                 `json:"workspace_id"`
  RuntimeName           string                 `json:"runtime_name"`
  NormalizedRuntimeName string                 `json:"normalized_runtime_name"`
  DisplayName           string                 `json:"display_name"`
  Enabled               bool                   `json:"enabled"`
  ExecutionMode         ExecutionMode          `json:"execution_mode"`
  Transport             ServerType             `json:"transport"`
  Configuration         MCPServerConfiguration `json:"configuration"`
  SecretBindings        []MCPSecretBinding     `json:"secret_bindings,omitempty"`
  Source                DefinitionSource       `json:"source"`
  Revision              int64                  `json:"revision"`
  CreatedAt             time.Time              `json:"created_at"`
  UpdatedAt             time.Time              `json:"updated_at"`
}
Execution modes
const (
  ExecutionModeRemote             ExecutionMode = "remote"
  ExecutionModeManagedPackage     ExecutionMode = "managed_package"
  ExecutionModeExistingExecutable ExecutionMode = "existing_executable"
)
func (s *CatalogService) Create(ctx context.Context, input CreateDefinitionInput) (*MCPServerDefinition, error)
Click for details →

The service validates names, transport, and configuration, and guards deletes with selection impact.

Create
func (s *CatalogService) Create(ctx context.Context, input CreateDefinitionInput) (*MCPServerDefinition, error) {
  if err := s.authorize(ctx, input.WorkspaceID); err != nil {
    return nil, err
  }
  definition, err := newDefinition(input)
  if err != nil {
    return nil, err
  }
  if err := s.ensureRuntimeNameAvailable(ctx, definition.WorkspaceID, definition.NormalizedRuntimeName, ""); err != nil {
    return nil, err
  }
  if err := s.repo.CreateMCPServerDefinition(ctx, definition); err != nil {
    return nil, err
  }
  return cloneDefinition(definition), nil
}
Delete guard
func (s *CatalogService) Delete(ctx context.Context, workspaceID, id string, expectedRevision int64, confirm bool) error {
  current, err := s.Get(ctx, workspaceID, id)
  if err != nil {
    return err
  }
  if current.Revision != expectedRevision {
    return revisionConflict(current)
  }
  if s.selectionRepo != nil {
    impact, _ := s.selectionRepo.SelectionImpact(ctx, workspaceID, id)
    if impact.Total() > 0 && !confirm {
      return &MCPSelectionImpactError{Impact: impact}
    }
  }
  if !confirm {
    return ErrMCPDeleteConfirmation
  }
  return s.repo.DeleteMCPServerDefinition(ctx, workspaceID, id, expectedRevision)
}
func (s *SelectionService) Replace(ctx context.Context, scope SelectionScope, workspaceID, ownerID string, definitionIDs []string) error
Click for details →

Selections store only definition IDs per scope and track desired versus applied revision for sessions.

Scopes
const (
  SelectionScopeProfile     SelectionScope = "profile"
  SelectionScopeRepository  SelectionScope = "repository"
  SelectionScopeTask        SelectionScope = "task"
  SelectionScopeTaskSession SelectionScope = "task_session"
)
Session state
type SessionMCPSelectionState struct {
  DesiredRevision     int64                `json:"desired_revision"`
  AppliedRevision     int64                `json:"applied_revision"`
  ApplyState          SessionMCPApplyState `json:"apply_state"`
  FailureCode         string               `json:"failure_code,omitempty"`
  FailureSummary      string               `json:"failure_summary,omitempty"`
  AttachmentAttemptID string               `json:"attachment_attempt_id,omitempty"`
}
Replace
func (s *SelectionService) Replace(ctx context.Context, scope SelectionScope, workspaceID, ownerID string, definitionIDs []string) error {
  if err := s.validateContext(ctx, scope, workspaceID, ownerID); err != nil {
    return err
  }
  ids := uniqueSelectionIDs(definitionIDs)
  for _, definitionID := range ids {
    definition, err := s.catalog.GetMCPServerDefinition(ctx, workspaceID, definitionID)
    if errors.Is(err, ErrMCPServerDefinitionNotFound) || definition == nil {
      return fmt.Errorf("%w: %s", ErrMCPSelectionWorkspaceMismatch, definitionID)
    }
    if !definition.Enabled {
      return fmt.Errorf("%w: %s", ErrMCPDefinitionDisabled, definitionID)
    }
  }
  if scope == SelectionScopeTaskSession && s.stateRepo != nil {
    return s.replaceSessionSelections(ctx, workspaceID, ownerID, ids)
  }
  return s.repo.ReplaceMCPSelections(ctx, scope, workspaceID, ownerID, ids)
}
func (r *Resolver) Resolve(ctx context.Context, resolutionContext ResolutionContext, policy Policy) (*EffectiveMCPResolution, error)
Click for details →

The resolver merges all scopes, resolves secrets at the last moment, and filters by transport policy.

Resolve
func (r *Resolver) Resolve(ctx context.Context, resolutionContext ResolutionContext, policy Policy) (*EffectiveMCPResolution, error) {
  if strings.TrimSpace(resolutionContext.WorkspaceID) == "" {
    return nil, fmt.Errorf("%w: workspace id is required", ErrMCPInvalidSelection)
  }
  origins, legacyServers, err := r.loadOrigins(ctx, resolutionContext)
  if err != nil {
    return nil, err
  }
  r.addLegacyFallback(ctx, resolutionContext, origins, legacyServers)
  ids := make([]string, 0, len(origins))
  for id := range origins {
    ids = append(ids, id)
  }
  sort.Strings(ids)
  result := &EffectiveMCPResolution{}
  names := make(map[string]string, len(ids))
  for _, id := range ids {
    resolved, decision, err := r.resolveDefinition(ctx, resolutionContext, policy, id, origins[id], legacyServers, names)
    if decision != nil {
      result.Decisions = append(result.Decisions, *decision)
      continue
    }
    result.Servers = append(result.Servers, *resolved)
  }
  return result, nil
}
Secret delivery
func (r *Resolver) deliveryServer(ctx context.Context, workspaceID string, definition *MCPServerDefinition) (ServerDef, error) {
  server := ServerDef{
    Type: definition.Transport, Command: definition.Configuration.Command,
    Args: append([]string(nil), definition.Configuration.Args...),
    Env: cloneStringMap(definition.Configuration.Env),
    URL: definition.Configuration.URL, Headers: cloneStringMap(definition.Configuration.Headers),
  }
  for _, binding := range definition.SecretBindings {
    value, err := r.secretResolve(ctx, binding.SecretID, workspaceID)
    if err != nil {
      return ServerDef{}, fmt.Errorf("%w: %w", ErrMCPSecretResolution, err)
    }
    if definition.Transport == ServerTypeStdio {
      server.Env[binding.InputName] = value
    } else {
      server.Headers[binding.InputName] = value
    }
  }
  return server, nil
}
func (m *Manager) applyPendingSessionMCP(ctx context.Context, sessionID string) error
Click for details →

The manager applies new selections only when the session is idle and holds the prompt lock across the provider call.

Apply when idle
func (m *Manager) applyPendingSessionMCP(ctx context.Context, sessionID string) error {
  state, err := m.mcpStateRepo.GetMCPSelectionState(ctx, sessionID)
  if errors.Is(err, mcpconfig.ErrMCPSelectionStateNotFound) {
    return nil
  }
  if state.DesiredRevision <= state.AppliedRevision {
    return nil
  }
  execution, ok := m.executionStore.GetBySessionID(sessionID)
  if !ok || execution == nil {
    return m.saveDeferredSessionMCP(ctx, sessionID)
  }
  execution.promptLifecycleMu.Lock()
  defer execution.promptLifecycleMu.Unlock()
  state, err = m.mcpStateRepo.GetMCPSelectionState(ctx, sessionID)
  if state.DesiredRevision <= state.AppliedRevision {
    return nil
  }
  if execution.Status != v1.AgentStatusReady {
    return m.savePendingSessionMCP(ctx, sessionID)
  }
  if execution.IsPassthrough || execution.agentctl == nil || execution.ACPSessionID == "" {
    return m.saveDeferredSessionMCP(ctx, sessionID)
  }
  if mcpErr := m.applySessionMCPWithAgent(ctx, execution); mcpErr != nil {
    return m.saveFailedSessionMCP(ctx, sessionID, state.DesiredRevision, mcpErr)
  }
  return m.saveAppliedSessionMCP(ctx, sessionID, state.DesiredRevision, execution.agentctl.GetLastAttachmentAttemptID())
}
Resume then load
func (m *Manager) applySessionMCPWithAgent(ctx context.Context, execution *AgentExecution) error {
  agentConfig, err := m.getAgentConfigForExecution(execution)
  if err != nil {
    return err
  }
  servers, err := m.resolveMcpServers(ctx, execution, agentConfig)
  if err != nil {
    return err
  }
  client := execution.agentctl
  if client.SupportsSessionResume() {
    err = client.ResumeSession(ctx, execution.ACPSessionID, execution.WorkspacePath, servers)
    if err == nil {
      return nil
    }
    if !client.SupportsSessionLoad() {
      return fmt.Errorf("%s: %w", sessionMCPFailureResume, err)
    }
  }
  if client.SupportsSessionLoad() {
    return client.LoadSession(ctx, execution.ACPSessionID, servers)
  }
  return errors.New("agent does not support session resume or load")
}
export function MCPSettings({ workspaceId }: MCPSettingsProps)
Click for details →

The settings page shows configured servers, marketplace, and guarded delete with selection impact.

Settings page
export function MCPSettings({ workspaceId }: MCPSettingsProps) {
  const data = useMCPWorkspaceSettings(workspaceId);
  const [view, setView] = useState<"configured" | "marketplace">("configured");
  const [editor, setEditor] = useState<MCPServerDefinition | null | undefined>(undefined);
  const [deleteTarget, setDeleteTarget] = useState<MCPServerDefinition | null>(null);
  const saveDefinition = async (payload: MCPDefinitionInput, server?: MCPServerDefinition) => {
    if (server) await data.update(server.id, { ...payload, expected_revision: server.revision });
    else await data.create(payload);
    setEditor(undefined);
  };
  if (editor !== undefined) return <MCPDefinitionForm workspaceId={workspaceId} server={editor} onSave={saveDefinition} onClose={() => setEditor(undefined)} />;
  return (
    <div data-testid="workspace-mcp-settings">
      <SettingsPageHeader title={t("settings:workspaceMcpServers")} />
      <MCPViewTabs view={view} onChange={setView} />
      {view === "configured" ? <ConfiguredMCPServers servers={data.servers} /> : <MCPMarketplaceView data={data} />}
    </div>
  );
}
Server card
function ServerCard({ server, onEdit, onToggle, onDelete }: { server: MCPServerDefinition }) {
  return (
    <Card data-testid={`mcp-server-card-${server.id}`}>
      <CardHeader>
        <CardTitle>{server.display_name}</CardTitle>
        <Badge variant={server.enabled ? "default" : "outline"}>
          {server.enabled ? t("settings:mcpEnabled") : t("settings:mcpDisabled")}
        </Badge>
        <p>{server.runtime_name}</p>
      </CardHeader>
      <CardContent>
        <SelectionImpact impact={server.selection_impact} />
        <Button onClick={onEdit}>{t("common:edit")}</Button>
        <Button onClick={onToggle}>{server.enabled ? t("settings:mcpDisable") : t("settings:mcpEnable")}</Button>
        <Button onClick={onDelete} aria-label={t("settings:deleteMcpServer")}><IconTrash /></Button>
      </CardContent>
    </Card>
  );
}
Read the changes as a list

Workspace MCP catalog types

apps/backend/internal/agent/mcpconfig/catalog_types.go

The catalog stores workspace-owned definitions with execution mode, transport, and secret bindings.

Definition
type MCPServerDefinition struct {
  ID                    string                 `json:"id"`
  WorkspaceID           string                 `json:"workspace_id"`
  RuntimeName           string                 `json:"runtime_name"`
  NormalizedRuntimeName string                 `json:"normalized_runtime_name"`
  DisplayName           string                 `json:"display_name"`
  Enabled               bool                   `json:"enabled"`
  ExecutionMode         ExecutionMode          `json:"execution_mode"`
  Transport             ServerType             `json:"transport"`
  Configuration         MCPServerConfiguration `json:"configuration"`
  SecretBindings        []MCPSecretBinding     `json:"secret_bindings,omitempty"`
  Source                DefinitionSource       `json:"source"`
  Revision              int64                  `json:"revision"`
  CreatedAt             time.Time              `json:"created_at"`
  UpdatedAt             time.Time              `json:"updated_at"`
}
Execution modes
const (
  ExecutionModeRemote             ExecutionMode = "remote"
  ExecutionModeManagedPackage     ExecutionMode = "managed_package"
  ExecutionModeExistingExecutable ExecutionMode = "existing_executable"
)

Catalog service with validation

apps/backend/internal/agent/mcpconfig/catalog_service.go

The service validates names, transport, and configuration, and guards deletes with selection impact.

Create
func (s *CatalogService) Create(ctx context.Context, input CreateDefinitionInput) (*MCPServerDefinition, error) {
  if err := s.authorize(ctx, input.WorkspaceID); err != nil {
    return nil, err
  }
  definition, err := newDefinition(input)
  if err != nil {
    return nil, err
  }
  if err := s.ensureRuntimeNameAvailable(ctx, definition.WorkspaceID, definition.NormalizedRuntimeName, ""); err != nil {
    return nil, err
  }
  if err := s.repo.CreateMCPServerDefinition(ctx, definition); err != nil {
    return nil, err
  }
  return cloneDefinition(definition), nil
}
Delete guard
func (s *CatalogService) Delete(ctx context.Context, workspaceID, id string, expectedRevision int64, confirm bool) error {
  current, err := s.Get(ctx, workspaceID, id)
  if err != nil {
    return err
  }
  if current.Revision != expectedRevision {
    return revisionConflict(current)
  }
  if s.selectionRepo != nil {
    impact, _ := s.selectionRepo.SelectionImpact(ctx, workspaceID, id)
    if impact.Total() > 0 && !confirm {
      return &MCPSelectionImpactError{Impact: impact}
    }
  }
  if !confirm {
    return ErrMCPDeleteConfirmation
  }
  return s.repo.DeleteMCPServerDefinition(ctx, workspaceID, id, expectedRevision)
}

Scoped selections with session state

apps/backend/internal/agent/mcpconfig/selections.go

Selections store only definition IDs per scope and track desired versus applied revision for sessions.

Scopes
const (
  SelectionScopeProfile     SelectionScope = "profile"
  SelectionScopeRepository  SelectionScope = "repository"
  SelectionScopeTask        SelectionScope = "task"
  SelectionScopeTaskSession SelectionScope = "task_session"
)
Session state
type SessionMCPSelectionState struct {
  DesiredRevision     int64                `json:"desired_revision"`
  AppliedRevision     int64                `json:"applied_revision"`
  ApplyState          SessionMCPApplyState `json:"apply_state"`
  FailureCode         string               `json:"failure_code,omitempty"`
  FailureSummary      string               `json:"failure_summary,omitempty"`
  AttachmentAttemptID string               `json:"attachment_attempt_id,omitempty"`
}
Replace
func (s *SelectionService) Replace(ctx context.Context, scope SelectionScope, workspaceID, ownerID string, definitionIDs []string) error {
  if err := s.validateContext(ctx, scope, workspaceID, ownerID); err != nil {
    return err
  }
  ids := uniqueSelectionIDs(definitionIDs)
  for _, definitionID := range ids {
    definition, err := s.catalog.GetMCPServerDefinition(ctx, workspaceID, definitionID)
    if errors.Is(err, ErrMCPServerDefinitionNotFound) || definition == nil {
      return fmt.Errorf("%w: %s", ErrMCPSelectionWorkspaceMismatch, definitionID)
    }
    if !definition.Enabled {
      return fmt.Errorf("%w: %s", ErrMCPDefinitionDisabled, definitionID)
    }
  }
  if scope == SelectionScopeTaskSession && s.stateRepo != nil {
    return s.replaceSessionSelections(ctx, workspaceID, ownerID, ids)
  }
  return s.repo.ReplaceMCPSelections(ctx, scope, workspaceID, ownerID, ids)
}

Effective resolver

apps/backend/internal/agent/mcpconfig/resolve_effective.go

The resolver merges all scopes, resolves secrets at the last moment, and filters by transport policy.

Resolve
func (r *Resolver) Resolve(ctx context.Context, resolutionContext ResolutionContext, policy Policy) (*EffectiveMCPResolution, error) {
  if strings.TrimSpace(resolutionContext.WorkspaceID) == "" {
    return nil, fmt.Errorf("%w: workspace id is required", ErrMCPInvalidSelection)
  }
  origins, legacyServers, err := r.loadOrigins(ctx, resolutionContext)
  if err != nil {
    return nil, err
  }
  r.addLegacyFallback(ctx, resolutionContext, origins, legacyServers)
  ids := make([]string, 0, len(origins))
  for id := range origins {
    ids = append(ids, id)
  }
  sort.Strings(ids)
  result := &EffectiveMCPResolution{}
  names := make(map[string]string, len(ids))
  for _, id := range ids {
    resolved, decision, err := r.resolveDefinition(ctx, resolutionContext, policy, id, origins[id], legacyServers, names)
    if decision != nil {
      result.Decisions = append(result.Decisions, *decision)
      continue
    }
    result.Servers = append(result.Servers, *resolved)
  }
  return result, nil
}
Secret delivery
func (r *Resolver) deliveryServer(ctx context.Context, workspaceID string, definition *MCPServerDefinition) (ServerDef, error) {
  server := ServerDef{
    Type: definition.Transport, Command: definition.Configuration.Command,
    Args: append([]string(nil), definition.Configuration.Args...),
    Env: cloneStringMap(definition.Configuration.Env),
    URL: definition.Configuration.URL, Headers: cloneStringMap(definition.Configuration.Headers),
  }
  for _, binding := range definition.SecretBindings {
    value, err := r.secretResolve(ctx, binding.SecretID, workspaceID)
    if err != nil {
      return ServerDef{}, fmt.Errorf("%w: %w", ErrMCPSecretResolution, err)
    }
    if definition.Transport == ServerTypeStdio {
      server.Env[binding.InputName] = value
    } else {
      server.Headers[binding.InputName] = value
    }
  }
  return server, nil
}

Idle-only session reconfiguration

apps/backend/internal/agent/runtime/lifecycle/manager_mcp_reconfiguration.go

The manager applies new selections only when the session is idle and holds the prompt lock across the provider call.

Apply when idle
func (m *Manager) applyPendingSessionMCP(ctx context.Context, sessionID string) error {
  state, err := m.mcpStateRepo.GetMCPSelectionState(ctx, sessionID)
  if errors.Is(err, mcpconfig.ErrMCPSelectionStateNotFound) {
    return nil
  }
  if state.DesiredRevision <= state.AppliedRevision {
    return nil
  }
  execution, ok := m.executionStore.GetBySessionID(sessionID)
  if !ok || execution == nil {
    return m.saveDeferredSessionMCP(ctx, sessionID)
  }
  execution.promptLifecycleMu.Lock()
  defer execution.promptLifecycleMu.Unlock()
  state, err = m.mcpStateRepo.GetMCPSelectionState(ctx, sessionID)
  if state.DesiredRevision <= state.AppliedRevision {
    return nil
  }
  if execution.Status != v1.AgentStatusReady {
    return m.savePendingSessionMCP(ctx, sessionID)
  }
  if execution.IsPassthrough || execution.agentctl == nil || execution.ACPSessionID == "" {
    return m.saveDeferredSessionMCP(ctx, sessionID)
  }
  if mcpErr := m.applySessionMCPWithAgent(ctx, execution); mcpErr != nil {
    return m.saveFailedSessionMCP(ctx, sessionID, state.DesiredRevision, mcpErr)
  }
  return m.saveAppliedSessionMCP(ctx, sessionID, state.DesiredRevision, execution.agentctl.GetLastAttachmentAttemptID())
}
Resume then load
func (m *Manager) applySessionMCPWithAgent(ctx context.Context, execution *AgentExecution) error {
  agentConfig, err := m.getAgentConfigForExecution(execution)
  if err != nil {
    return err
  }
  servers, err := m.resolveMcpServers(ctx, execution, agentConfig)
  if err != nil {
    return err
  }
  client := execution.agentctl
  if client.SupportsSessionResume() {
    err = client.ResumeSession(ctx, execution.ACPSessionID, execution.WorkspacePath, servers)
    if err == nil {
      return nil
    }
    if !client.SupportsSessionLoad() {
      return fmt.Errorf("%s: %w", sessionMCPFailureResume, err)
    }
  }
  if client.SupportsSessionLoad() {
    return client.LoadSession(ctx, execution.ACPSessionID, servers)
  }
  return errors.New("agent does not support session resume or load")
}

Workspace MCP settings UI

apps/web/components/settings/workspaces/mcp-settings.tsx

The settings page shows configured servers, marketplace, and guarded delete with selection impact.

Settings page
export function MCPSettings({ workspaceId }: MCPSettingsProps) {
  const data = useMCPWorkspaceSettings(workspaceId);
  const [view, setView] = useState<"configured" | "marketplace">("configured");
  const [editor, setEditor] = useState<MCPServerDefinition | null | undefined>(undefined);
  const [deleteTarget, setDeleteTarget] = useState<MCPServerDefinition | null>(null);
  const saveDefinition = async (payload: MCPDefinitionInput, server?: MCPServerDefinition) => {
    if (server) await data.update(server.id, { ...payload, expected_revision: server.revision });
    else await data.create(payload);
    setEditor(undefined);
  };
  if (editor !== undefined) return <MCPDefinitionForm workspaceId={workspaceId} server={editor} onSave={saveDefinition} onClose={() => setEditor(undefined)} />;
  return (
    <div data-testid="workspace-mcp-settings">
      <SettingsPageHeader title={t("settings:workspaceMcpServers")} />
      <MCPViewTabs view={view} onChange={setView} />
      {view === "configured" ? <ConfiguredMCPServers servers={data.servers} /> : <MCPMarketplaceView data={data} />}
    </div>
  );
}
Server card
function ServerCard({ server, onEdit, onToggle, onDelete }: { server: MCPServerDefinition }) {
  return (
    <Card data-testid={`mcp-server-card-${server.id}`}>
      <CardHeader>
        <CardTitle>{server.display_name}</CardTitle>
        <Badge variant={server.enabled ? "default" : "outline"}>
          {server.enabled ? t("settings:mcpEnabled") : t("settings:mcpDisabled")}
        </Badge>
        <p>{server.runtime_name}</p>
      </CardHeader>
      <CardContent>
        <SelectionImpact impact={server.selection_impact} />
        <Button onClick={onEdit}>{t("common:edit")}</Button>
        <Button onClick={onToggle}>{server.enabled ? t("settings:mcpDisable") : t("settings:mcpEnable")}</Button>
        <Button onClick={onDelete} aria-label={t("settings:deleteMcpServer")}><IconTrash /></Button>
      </CardContent>
    </Card>
  );
}

Data and storage

Catalog and selections are workspace-owned. Secrets stay out of the catalog and resolve only at delivery.

FieldTypeNotes
mcp_server_definitionstableworkspace_id, runtime_name, execution_mode, transport, configuration, revision
mcp_selectionstablescope, workspace_id, owner_id, definition_id (profile, repository, task, task_session)
mcp_session_statetablesession_id, desired_revision, applied_revision, apply_state, failure_code
mcp_registry_cachetablecached marketplace entries with stale and degraded flags
legacy_import_statetableworkspace_id, profile_id, status pending or complete
secret_bindingsjsoninput_name to secret_id, value resolved only in Resolver

Risk

7 / 10 High
1 low5 medium10 high

Why this score

  • Large blast radius: catalog, selections, resolver, lifecycle, and UI change together.
  • Data migration: legacy profile JSON imports with deterministic IDs and secret rebind.
  • Runtime safety: idle-only apply with compare-and-swap prevents stale writes.

Trade-offs and review notes

Where to look first

  1. Check catalog validation for runtime name, transport, and execution mode in catalog_service.go.
  2. Verify selection scope checks and session state compare-and-swap in selections.go.
  3. Confirm resolver merges all scopes without precedence and handles name collisions in resolve_effective.go.
  4. Review idle guard and resume then load fallback in manager_mcp_reconfiguration.go.
  5. Check workspace MCP settings tabs, delete impact, and marketplace install in mcp-settings.tsx.