Session exclusion control
apps/web/components/task/chat/mcp-explorer/mcp-session-exclusions.tsx ↗The row action switches between exclusion and restoration, and the text states that the choice applies only to this session. A disabled definition gets a separate explanation.
Exclude or restore row action
export function McpSessionExclusionAction({
excluded,
definition,
disabled,
onChange,
}: {
excluded: boolean;
definition: MCPServerDefinition;
disabled?: boolean;
onChange: () => void;
}) {
const { t } = useTranslation();
return (
<div className="mt-3 flex min-w-0 items-center justify-between gap-3 max-md:flex-col max-md:items-stretch">
<div className="min-w-0 text-pretty text-xs text-muted-foreground">
<p>{t(excluded ? "settings:mcpSessionExcluded" : "settings:mcpSessionInherited")}</p>
{!definition.enabled && (
<p className="mt-1">{t("settings:mcpRestoreDisabledDescription")}</p>
)}
</div>
<Button
type="button"
variant="outline"
disabled={disabled}
className="h-auto min-h-7 shrink-0 cursor-pointer whitespace-normal py-1 max-md:min-h-11 max-md:w-full [@media(pointer:coarse)]:min-h-11"
onClick={onChange}
>
{t(excluded ? "settings:mcpRestoreInheritance" : "settings:mcpExcludeForSession")}
</Button>
</div>
);
}
Session-only request and response fields
apps/backend/internal/agent/settings/handlers/mcp_selection_handlers.go ↗The handler accepts excluded IDs only with a revision condition. The session response always returns an exclusion array, including an empty array when no row exists.
Request condition
func selectionCondition(body mcpSelectionRequest, scope mcpconfig.SelectionScope) (mcpconfig.SelectionCondition, error) {
condition := mcpconfig.SelectionCondition{}
if body.ExpectedExcludedDefinitionIDs != nil {
return condition, mcpconfig.ErrMCPInvalidSelection
}
if body.ExcludedDefinitionIDs != nil {
var ids []string
if err := json.Unmarshal(body.ExcludedDefinitionIDs, &ids); err != nil || ids == nil {
return condition, mcpconfig.ErrMCPInvalidSelection
}
condition.ExcludedDefinitionIDs = &ids
}
return condition, condition.Validate(scope)
}
Response field
if scope == mcpconfig.SelectionScopeTaskSession {
revision := int64(0)
if snapshot.State != nil {
revision = snapshot.State.DesiredRevision
}
response.SelectionRevision = &revision
ids := snapshot.ExcludedDefinitionIDs
if ids == nil {
ids = []string{}
}
response.ExcludedDefinitionIDs = &ids
}
c.JSON(http.StatusOK, response)
}
Exclusion admission and revision check
apps/backend/internal/agent/mcpconfig/selection_write.go ↗The service resolves the task, profile, and repository context for the target session before any write. It rejects an exclusion condition when the context is missing or belongs to another workspace or session.
Prepare the exclusion context
func (s *SelectionService) prepareExclusionCondition(ctx context.Context, origin SelectionOrigin, condition *SelectionCondition) error {
if condition.ExcludedDefinitionIDs == nil {
return nil
}
if s.resolutionContext == nil {
return ErrMCPInvalidSelection
}
rc, err := s.resolutionContext(ctx, origin.WorkspaceID, origin.OwnerID)
if err != nil || rc.WorkspaceID != origin.WorkspaceID || rc.SessionID != origin.OwnerID {
return ErrMCPInvalidSelection
}
condition.ExclusionContext = &rc
return nil
}
Atomic exclusion persistence
apps/backend/internal/agent/settings/store/mcp_session_exclusions.go ↗The store replaces the session exclusion set inside the existing conditional selection transaction. It accepts a target that was already excluded or is currently inherited, and it locks definitions in the same order as catalog deletion.
Read and replace exclusions
func readMCPSessionExclusionsTx(ctx context.Context, tx *sqlx.Tx, workspaceID, sessionID string) ([]string, error) {
ids := make([]string, 0)
err := tx.SelectContext(ctx, &ids, tx.Rebind(`SELECT mcp_server_id FROM task_session_mcp_exclusions WHERE workspace_id = ? AND task_session_id = ? ORDER BY mcp_server_id`), workspaceID, sessionID)
return ids, err
}
// Positive and excluded definitions share one sorted lock order with catalog deletion.
func lockMCPSelectionDefinitions(ctx context.Context, tx *sqlx.Tx, workspaceID string, ids []string, excluded *[]string) error {
all := append([]string{}, ids...)
if excluded != nil {
all = append(all, (*excluded)...)
}
for _, id := range normalizedMCPSelectionIDs(all) {
if err := lockMCPDefinition(ctx, tx, workspaceID, id); err != nil {
if errors.Is(err, mcpconfig.ErrMCPServerDefinitionNotFound) {
return mcpconfig.ErrMCPSelectionWorkspaceMismatch
}
return err
}
}
return nil
}
func replaceMCPSessionExclusionsTx(ctx context.Context, tx *sqlx.Tx, origin mcpconfig.SelectionOrigin, previous []string, condition mcpconfig.SelectionCondition) error {
if condition.ExcludedDefinitionIDs == nil {
return nil
}
rc := condition.ExclusionContext
if rc == nil || rc.WorkspaceID != origin.WorkspaceID || rc.SessionID != origin.OwnerID {
return mcpconfig.ErrMCPInvalidSelection
}
ids := normalizedMCPSelectionIDs(*condition.ExcludedDefinitionIDs)
inherited, err := readMCPInheritedIDsTx(ctx, tx, *rc)
if err != nil {
return err
}
for _, id := range ids {
if !slices.Contains(previous, id) && !slices.Contains(inherited, id) {
return mcpconfig.ErrMCPInvalidSelection
}
}
if _, err := tx.ExecContext(ctx, tx.Rebind(`DELETE FROM task_session_mcp_exclusions WHERE workspace_id = ? AND task_session_id = ?`), origin.WorkspaceID, origin.OwnerID); err != nil {
return err
}
for _, id := range ids {
if _, err := tx.ExecContext(ctx, tx.Rebind(`INSERT INTO task_session_mcp_exclusions (workspace_id, task_session_id, mcp_server_id) VALUES (?, ?, ?)`), origin.WorkspaceID, origin.OwnerID, id); err != nil {
return err
}
}
return nil
}
Resolver subtracts excluded IDs
apps/backend/internal/agent/mcpconfig/resolve_effective.go ↗For each selected definition, the resolver checks the exclusion set before it resolves a secret or prepares a server. An excluded definition produces a bounded session_excluded decision and keeps its visible origins.
Skip excluded definitions
names := make(map[string]string, len(ids))
for _, id := range ids {
if slices.Contains(inputs.ExcludedDefinitionIDs, id) {
if definition := inputs.Definitions[id]; definition != nil {
result.Decisions = append(result.Decisions, definitionDecision(definition, "session_excluded", "MCP definition is excluded from this session", origins[id]))
}
continue
}
resolved, decision, err := r.resolveDefinition(ctx, resolutionContext, policy, id, origins[id], legacyServers, names, inputs.Definitions[id])
if err != nil {
return nil, err