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>
);
}