diff --git a/frontend/__tests__/components/features/settings/agent-profiles/agent-profile-form-state.test.ts b/frontend/__tests__/components/features/settings/agent-profiles/agent-profile-form-state.test.ts new file mode 100644 index 0000000000..6b27ebf32e --- /dev/null +++ b/frontend/__tests__/components/features/settings/agent-profiles/agent-profile-form-state.test.ts @@ -0,0 +1,64 @@ +import { describe, expect, it } from "vitest"; +import { + AgentProfileFormDeps, + buildAgentProfileSaveInput, + newAgentProfileFormState, +} from "#/components/features/settings/agent-profiles/agent-profile-form-state"; + +const deps: AgentProfileFormDeps = { + acpProviders: [], + defaultLlmProfileRef: "Default", + schema: null, +}; + +describe("buildAgentProfileSaveInput MCP server references", () => { + it("sends no MCP servers for an org profile while org MCP listings are off", () => { + const orgDeps = { ...deps, profileScope: "org" as const }; + const state = { + ...newAgentProfileFormState(orgDeps), + mcpMode: "custom" as const, + mcpSelected: ["datadog-shared"], + }; + + const input = buildAgentProfileSaveInput(state, orgDeps); + + expect(input.mcp_server_refs).toEqual([]); + }); + + it("sends the chosen organization-managed servers for an org profile when on", () => { + const orgDeps = { + ...deps, + profileScope: "org" as const, + orgMcpServersEnabled: true, + }; + const state = { + ...newAgentProfileFormState(orgDeps), + mcpMode: "custom" as const, + mcpSelected: ["datadog-shared"], + }; + + const input = buildAgentProfileSaveInput(state, orgDeps); + + expect(input.mcp_server_refs).toEqual(["datadog-shared"]); + // Nothing chosen stays an explicit "none", never "all". + expect( + buildAgentProfileSaveInput({ ...state, mcpSelected: [] }, orgDeps) + .mcp_server_refs, + ).toEqual([]); + }); + + it("keeps the personal semantics: null for all servers, the list when chosen", () => { + const personalDeps = { ...deps, orgMcpServersEnabled: true }; + const standard = newAgentProfileFormState(personalDeps); + + expect( + buildAgentProfileSaveInput(standard, personalDeps).mcp_server_refs, + ).toBeNull(); + expect( + buildAgentProfileSaveInput( + { ...standard, mcpMode: "custom", mcpSelected: ["a", "bifrost"] }, + personalDeps, + ).mcp_server_refs, + ).toEqual(["a", "bifrost"]); + }); +}); diff --git a/frontend/__tests__/components/features/settings/agent-profiles/agent-profile-form.test.tsx b/frontend/__tests__/components/features/settings/agent-profiles/agent-profile-form.test.tsx new file mode 100644 index 0000000000..8a4e2f573d --- /dev/null +++ b/frontend/__tests__/components/features/settings/agent-profiles/agent-profile-form.test.tsx @@ -0,0 +1,182 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { AgentProfileForm } from "#/components/features/settings/agent-profiles/agent-profile-form"; +import { + AgentProfileFormDeps, + AgentProfileFormState, + newAgentProfileFormState, +} from "#/components/features/settings/agent-profiles/agent-profile-form-state"; +import type { ProfileScopeItem } from "#/components/features/settings/agent-profiles/profile-scope-list"; +import type { AcpCredentialForm } from "#/hooks/use-acp-credential-form"; + +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ + t: (key: string, options?: Record) => + options?.names ? `${key}:${options.names}` : key, + }), +})); + +const deps: AgentProfileFormDeps = { + acpProviders: [], + defaultLlmProfileRef: "Default", + schema: null, +}; + +const credentialForm: AcpCredentialForm = { + fields: [], + values: {}, + setValue: () => {}, + secretExists: () => false, + hasValueFor: () => false, + conflicts: [], + isDirty: false, + save: async () => true, + reset: () => {}, + isSaving: false, +}; + +const ORG_MCP_ITEMS: ProfileScopeItem[] = [ + { + name: "datadog-shared", + description: "https://dd.example/mcp", + scope: "organization", + }, +]; + +function renderForm({ + state, + scope, + mcpServers, + orgMcpServersEnabled, + onChange = vi.fn(), +}: { + state: AgentProfileFormState; + scope: "personal" | "org"; + mcpServers: ProfileScopeItem[]; + orgMcpServersEnabled?: boolean; + onChange?: (patch: Partial) => void; +}) { + render( + , + ); + return onChange; +} + +describe("AgentProfileForm MCP servers", () => { + it("still leaves the MCP section out of an org profile while the flag is off", () => { + renderForm({ + state: newAgentProfileFormState({ ...deps, profileScope: "org" }), + scope: "org", + mcpServers: ORG_MCP_ITEMS, + }); + + expect( + screen.queryByTestId("agent-profile-mcp-list"), + ).not.toBeInTheDocument(); + expect( + screen.queryByTestId("agent-profile-mcp-mode"), + ).not.toBeInTheDocument(); + expect( + screen.queryByTestId("agent-profile-mcp-hint"), + ).not.toBeInTheDocument(); + }); + + it("lets an org profile toggle the organization-managed servers, with no 'all' mode", () => { + const onChange = renderForm({ + state: { + ...newAgentProfileFormState({ ...deps, profileScope: "org" }), + mcpMode: "custom", + mcpSelected: [], + }, + scope: "org", + mcpServers: ORG_MCP_ITEMS, + orgMcpServersEnabled: true, + }); + + expect( + screen.queryByTestId("agent-profile-mcp-mode"), + ).not.toBeInTheDocument(); + expect(screen.getByTestId("agent-profile-mcp-hint")).toHaveTextContent( + "SETTINGS$AGENT_PROFILE_ORG_MCP_HINT", + ); + const toggle = screen.getByTestId("agent-profile-mcp-datadog-shared"); + expect(toggle).not.toBeChecked(); + expect(toggle).not.toBeDisabled(); + + fireEvent.click(toggle); + + expect(onChange).toHaveBeenCalledWith({ mcpSelected: ["datadog-shared"] }); + }); + + it("tells an org admin when there is nothing to pick yet", () => { + renderForm({ + state: { + ...newAgentProfileFormState({ ...deps, profileScope: "org" }), + mcpMode: "custom", + }, + scope: "org", + mcpServers: [], + orgMcpServersEnabled: true, + }); + + expect(screen.getByTestId("agent-profile-mcp-none")).toHaveTextContent( + "SETTINGS$AGENT_PROFILE_ORG_MCP_NONE", + ); + }); + + it("flags an org profile's reference to a listing that no longer exists", () => { + renderForm({ + state: { + ...newAgentProfileFormState({ ...deps, profileScope: "org" }), + mcpMode: "custom", + mcpSelected: ["gone"], + }, + scope: "org", + mcpServers: ORG_MCP_ITEMS, + orgMcpServersEnabled: true, + }); + + expect(screen.getByTestId("agent-profile-mcp-dangling")).toHaveTextContent( + "SETTINGS$AGENT_PROFILE_MCP_DANGLING:gone", + ); + }); + + it("badges the organization's listings in a personal profile's picker", () => { + renderForm({ + state: { + ...newAgentProfileFormState(deps), + mcpMode: "custom", + mcpSelected: ["mine"], + }, + scope: "personal", + mcpServers: [{ name: "mine", description: null }, ...ORG_MCP_ITEMS], + orgMcpServersEnabled: true, + }); + + expect(screen.getByTestId("agent-profile-mcp-mode")).toBeInTheDocument(); + expect( + screen.getByTestId("agent-profile-mcp-datadog-shared-org-badge"), + ).toBeInTheDocument(); + expect( + screen.queryByTestId("agent-profile-mcp-mine-org-badge"), + ).not.toBeInTheDocument(); + expect(screen.getByTestId("agent-profile-mcp-hint")).toHaveTextContent( + "SETTINGS$AGENT_PROFILE_MCP_CHOOSE_HINT", + ); + }); +}); diff --git a/frontend/src/components/features/settings/agent-profiles/agent-profile-form-state.ts b/frontend/src/components/features/settings/agent-profiles/agent-profile-form-state.ts index b67c6c8671..fadd0b2d1e 100644 --- a/frontend/src/components/features/settings/agent-profiles/agent-profile-form-state.ts +++ b/frontend/src/components/features/settings/agent-profiles/agent-profile-form-state.ts @@ -97,6 +97,12 @@ export interface AgentProfileFormDeps { * its secrets explicitly (OHE-3088). Defaults to ``personal``. */ profileScope?: AgentProfileScope; + /** + * ``feature_flags.enable_org_mcp_servers`` (OHE-3091): an org profile may + * then name the organization-managed MCP server listings it uses. Off, an + * org save keeps sending ``mcp_server_refs: []``. + */ + orgMcpServersEnabled?: boolean; } export function newAgentProfileFormState( @@ -261,9 +267,16 @@ export function buildAgentProfileSaveInput( stored?: AgentProfile | null, ): AgentProfileSaveInput { const isOrg = deps.profileScope === "org"; - const mcpServerRefs = isOrg - ? [] - : scopeRefs(state.mcpMode, state.mcpSelected); + // An org profile names the organization-managed servers it uses, or none; + // never "all", which would mean every launching member's own servers. + let mcpServerRefs: string[] | null; + if (!isOrg) { + mcpServerRefs = scopeRefs(state.mcpMode, state.mcpSelected); + } else if (deps.orgMcpServersEnabled) { + mcpServerRefs = state.mcpSelected; + } else { + mcpServerRefs = []; + } const secretRefs = isOrg ? state.secretsSelected : scopeRefs(state.secretsMode, state.secretsSelected); diff --git a/frontend/src/components/features/settings/agent-profiles/agent-profile-form.tsx b/frontend/src/components/features/settings/agent-profiles/agent-profile-form.tsx index 45562aaa9b..f1b7f5c012 100644 --- a/frontend/src/components/features/settings/agent-profiles/agent-profile-form.tsx +++ b/frontend/src/components/features/settings/agent-profiles/agent-profile-form.tsx @@ -56,7 +56,11 @@ interface AgentProfileFormProps { toolsLoading?: boolean; /** True when the tool catalog could not be loaded. */ toolsLoadFailed?: boolean; - /** The member's configured MCP servers (user-scoped). */ + /** + * The MCP servers the profile may pick: the member's own plus the + * organization's listings for a personal profile, the organization-managed + * listings for an org profile (OHE-3091). + */ mcpServers: ProfileScopeItem[]; /** The member's saved secrets (user-scoped). */ secrets: ProfileScopeItem[]; @@ -67,11 +71,14 @@ interface AgentProfileFormProps { /** Credentials for the selected ACP preset; saved as global secrets. */ credentialForm: AcpCredentialForm; /** - * The collection being edited. An org profile is OpenHands-only, has no - * MCP servers and always names its secrets, so the agent-kind selector, the - * MCP section and the secrets mode switch are left out (OHE-3088). + * The collection being edited. An org profile is OpenHands-only and always + * names its secrets, so the agent-kind selector and the secrets mode switch + * are left out (OHE-3088); its MCP section lists the organization-managed + * servers only once ``orgMcpServersEnabled`` is on. */ profileScope?: AgentProfileScope; + /** ``feature_flags.enable_org_mcp_servers``: shows the org profile's MCP section. */ + orgMcpServersEnabled?: boolean; } function toggleName(list: string[], name: string, checked: boolean): string[] { @@ -104,6 +111,7 @@ export function AgentProfileForm({ skillsLoadFailed = false, credentialForm, profileScope = "personal", + orgMcpServersEnabled = false, }: AgentProfileFormProps) { const { t } = useTranslation(); const isAcp = state.agentKind === "acp"; @@ -132,13 +140,19 @@ export function AgentProfileForm({ ]; const knownMcp = new Set(mcpServers.map(({ name }) => name)); const danglingMcpRefs = - state.mcpMode === "custom" + isOrg || state.mcpMode === "custom" ? state.mcpSelected.filter((name) => !knownMcp.has(name)) : []; const mcpCatalog: ProfileScopeItem[] = [ ...mcpServers, ...danglingMcpRefs.map((name) => ({ name, description: null })), ]; + let mcpHintKey = I18nKey.SETTINGS$AGENT_PROFILE_MCP_ALL_HINT; + if (isOrg) { + mcpHintKey = I18nKey.SETTINGS$AGENT_PROFILE_ORG_MCP_HINT; + } else if (state.mcpMode === "custom") { + mcpHintKey = I18nKey.SETTINGS$AGENT_PROFILE_MCP_CHOOSE_HINT; + } const knownSecrets = new Set(secrets.map(({ name }) => name)); // An org profile always names its secrets; a personal one only when narrowed. const hasSecretList = isOrg || state.secretsMode === "custom"; @@ -549,51 +563,57 @@ export function AgentProfileForm({ )} - {/* MCP servers (both kinds): all configured, or an explicit subset. - Not for org profiles: MCP servers are per member, so an org profile + {/* MCP servers: a personal profile uses all configured servers (the + member's own and the organization's listings) or an explicit subset; + an org profile names the organization-managed listings it uses, and + only once org MCP listings are on (OHE-3091) -- before that it launches with none. */} - {!isOrg && ( + {(!isOrg || orgMcpServersEnabled) && (
{t(I18nKey.SETTINGS$AGENT_PROFILE_MCP)} - { - if (!key) return; - const mcpMode = key as ProfileScopeMode; - const patch: Partial = { mcpMode }; - // Seed a first switch to "choose" with every configured server, - // so narrowing starts from what the agent had. - if (mcpMode === "custom" && state.mcpSelected.length === 0) { - patch.mcpSelected = mcpServers.map(({ name }) => name); - } - onChange(patch); - }} - /> + {!isOrg ? ( + { + if (!key) return; + const mcpMode = key as ProfileScopeMode; + const patch: Partial = { mcpMode }; + // Seed a first switch to "choose" with every configured server, + // so narrowing starts from what the agent had. + if (mcpMode === "custom" && state.mcpSelected.length === 0) { + patch.mcpSelected = mcpServers.map(({ name }) => name); + } + onChange(patch); + }} + /> + ) : null} {mcpCatalog.length > 0 ? ( name) } - isDisabled={isDisabled || state.mcpMode === "standard"} + isDisabled={ + isDisabled || (!isOrg && state.mcpMode === "standard") + } onToggle={(name, checked) => onChange({ mcpSelected: toggleName(state.mcpSelected, name, checked), @@ -601,8 +621,15 @@ export function AgentProfileForm({ } /> ) : ( - - {t(I18nKey.SETTINGS$AGENT_PROFILE_MCP_NONE)} + + {t( + isOrg + ? I18nKey.SETTINGS$AGENT_PROFILE_ORG_MCP_NONE + : I18nKey.SETTINGS$AGENT_PROFILE_MCP_NONE, + )} )} {danglingMcpRefs.length > 0 ? ( @@ -615,12 +642,11 @@ export function AgentProfileForm({ })} ) : null} - - {t( - state.mcpMode === "custom" - ? I18nKey.SETTINGS$AGENT_PROFILE_MCP_CHOOSE_HINT - : I18nKey.SETTINGS$AGENT_PROFILE_MCP_ALL_HINT, - )} + + {t(mcpHintKey)}
)} diff --git a/frontend/src/components/features/settings/agent-profiles/agent-profiles-manager.tsx b/frontend/src/components/features/settings/agent-profiles/agent-profiles-manager.tsx index add14d6722..72f7a90283 100644 --- a/frontend/src/components/features/settings/agent-profiles/agent-profiles-manager.tsx +++ b/frontend/src/components/features/settings/agent-profiles/agent-profiles-manager.tsx @@ -23,6 +23,9 @@ interface AgentProfilesManagerProps { isAddOrgDisabled?: boolean; /** True when the viewer may add, edit and delete the organization's profiles. */ canManageOrg?: boolean; + /** ``feature_flags.enable_org_mcp_servers``: org profiles may name + * organization-managed MCP servers, so the admin copy says so (OHE-3091). */ + orgMcpServersEnabled?: boolean; } /** @@ -38,6 +41,7 @@ export function AgentProfilesManager({ isAddDisabled = false, isAddOrgDisabled = isAddDisabled, canManageOrg = false, + orgMcpServersEnabled = false, }: AgentProfilesManagerProps) { const { t } = useTranslation(); const { data, isLoading, error } = useAgentProfiles(); @@ -56,6 +60,14 @@ export function AgentProfilesManager({ ); const activeId = data?.active_agent_profile_id ?? null; + let orgSectionDescriptionKey = + I18nKey.SETTINGS$ORG_AGENT_PROFILES_DESCRIPTION; + if (canManageOrg) { + orgSectionDescriptionKey = orgMcpServersEnabled + ? I18nKey.SETTINGS$ORG_AGENT_PROFILES_ADMIN_DESCRIPTION_WITH_MCP + : I18nKey.SETTINGS$ORG_AGENT_PROFILES_ADMIN_DESCRIPTION; + } + const handleActivate = async (profile: AgentProfileSummary) => { if (!profile.id) return; await mutateWithToast(activateProfile, profile.id, { @@ -117,11 +129,7 @@ export function AgentProfilesManager({ {t(I18nKey.SETTINGS$ORG_AGENT_PROFILES_TITLE)}

- {t( - canManageOrg - ? I18nKey.SETTINGS$ORG_AGENT_PROFILES_ADMIN_DESCRIPTION - : I18nKey.SETTINGS$ORG_AGENT_PROFILES_DESCRIPTION, - )} + {t(orgSectionDescriptionKey)}

{canManageOrg ? ( diff --git a/frontend/src/components/features/settings/agent-profiles/agent-profiles-view.tsx b/frontend/src/components/features/settings/agent-profiles/agent-profiles-view.tsx index 3df7cfaa60..28239b69a7 100644 --- a/frontend/src/components/features/settings/agent-profiles/agent-profiles-view.tsx +++ b/frontend/src/components/features/settings/agent-profiles/agent-profiles-view.tsx @@ -18,6 +18,7 @@ import { useAgentProfiles } from "#/hooks/query/use-agent-profiles"; import { useAgentSettingsSchema } from "#/hooks/query/use-agent-settings-schema"; import { useConfig } from "#/hooks/query/use-config"; import { useOrgSecrets, useSearchSecrets } from "#/hooks/query/use-get-secrets"; +import { useOrgMcpServers } from "#/hooks/query/use-org-mcp-servers"; import { useMe } from "#/hooks/query/use-me"; import { usePermission } from "#/hooks/organizations/use-permissions"; import { useSkills } from "#/hooks/query/use-skills"; @@ -36,6 +37,7 @@ import { } from "#/utils/custom-toast-handlers"; import { formControlBackNavButtonClassName } from "#/utils/form-control-classes"; import { parseMcpConfig } from "#/utils/mcp-config"; +import { toOrgMcpServerView } from "#/utils/org-mcp-servers"; import { retrieveAxiosErrorMessage } from "#/utils/retrieve-axios-error-message"; import { AgentProfileForm } from "./agent-profile-form"; import { @@ -78,19 +80,24 @@ function configuredMcpServers(raw: unknown): ProfileScopeItem[] { } /** - * The org editor's view of a profile's scopes: no MCP servers, and an explicit - * secrets list -- every org-shared secret when the stored profile had no list - * (OHE-3088). The save body enforces the same shape. + * The org editor's view of a profile's scopes: an explicit MCP list (the + * stored one once org MCP listings are on, OHE-3091; otherwise none) and an + * explicit secrets list -- every org-shared secret when the stored profile had + * no list (OHE-3088). The save body enforces the same shape. */ function withOrgScopes( state: AgentProfileFormState, stored: AgentProfile | null, orgSecretNames: string[], + orgMcpServersEnabled: boolean, ): AgentProfileFormState { return { ...state, mcpMode: "custom", - mcpSelected: [], + mcpSelected: + orgMcpServersEnabled && Array.isArray(stored?.mcp_server_refs) + ? state.mcpSelected + : [], secretsMode: "custom", secretsSelected: Array.isArray(stored?.secret_refs) ? state.secretsSelected @@ -180,6 +187,15 @@ export function AgentProfilesView() { const acpProviders = config?.acp_providers ?? EMPTY_ACP_PROVIDERS; const isAcpEnabled = !!config?.feature_flags?.enable_acp; + // Organization MCP server listings (OHE-3091): a personal profile may pick + // them next to the member's own servers, an org profile the organization- + // managed ones. The query stays idle while the flag is off. + const orgMcpEnabled = !!config?.feature_flags?.enable_org_mcp_servers; + const orgMcpServersQuery = useOrgMcpServers(); + const orgMcpListings = useMemo( + () => orgMcpServersQuery.data.map(toOrgMcpServerView), + [orgMcpServersQuery.data], + ); // Org admins manage the organization's shared profiles here when the // org-profiles flag is on; everyone else only sets one as their default. const canManageOrg = @@ -207,6 +223,7 @@ export function AgentProfilesView() { schema: schema ?? null, defaultLlmProfileRef, profileScope: editingScope, + orgMcpServersEnabled: orgMcpEnabled, }), [ acpProviders, @@ -214,13 +231,45 @@ export function AgentProfilesView() { schema, defaultLlmProfileRef, editingScope, + orgMcpEnabled, ], ); const mcpConfigRaw = settings?.agent_settings?.mcp_config; - const mcpServers = useMemo( + const memberMcpServers = useMemo( () => configuredMcpServers(mcpConfigRaw), [mcpConfigRaw], ); + // The personal picker lists the organization's listings after the member's + // own. A member's server of the same name wins at launch, so it shows once. + const mcpServers = useMemo(() => { + if (!orgMcpEnabled) return memberMcpServers; + const own = new Set(memberMcpServers.map(({ name }) => name)); + return [ + ...memberMcpServers, + ...orgMcpListings + .filter((listing) => !own.has(listing.name)) + .map((listing) => ({ + name: listing.name, + description: listing.url, + scope: "organization" as const, + })), + ]; + }, [memberMcpServers, orgMcpListings, orgMcpEnabled]); + // An org profile may only name organization-managed listings: a + // user-managed one resolves against a member's personal secret. + const orgMcpItems = useMemo( + () => + orgMcpEnabled + ? orgMcpListings + .filter((listing) => listing.managedBy === "org") + .map((listing) => ({ + name: listing.name, + description: listing.url, + scope: "organization" as const, + })) + : [], + [orgMcpListings, orgMcpEnabled], + ); // The search list carries personal and organization-shared secrets under // the names the runtime exposes, so ``secret_refs`` can reference either. const secretItems = useMemo( @@ -374,7 +423,9 @@ export function AgentProfilesView() { if (scope === "org" ? !isOrgEditorReady : !isEditorReady) return; const scopedDeps = { ...deps, profileScope: scope }; let state = newAgentProfileFormState(scopedDeps); - if (scope === "org") state = withOrgScopes(state, null, orgSecretNames); + if (scope === "org") { + state = withOrgScopes(state, null, orgSecretNames, orgMcpEnabled); + } setEditingScope(scope); setEditingProfile(null); setProfileName(""); @@ -382,7 +433,7 @@ export function AgentProfilesView() { setInitialInput(null); setViewMode("create"); }, - [deps, isEditorReady, isOrgEditorReady, orgSecretNames], + [deps, isEditorReady, isOrgEditorReady, orgSecretNames, orgMcpEnabled], ); const handleEditProfile = useCallback( @@ -404,7 +455,7 @@ export function AgentProfilesView() { ); let state = storedState; if (scope === "org") { - state = withOrgScopes(state, profile, orgSecretNames); + state = withOrgScopes(state, profile, orgSecretNames, orgMcpEnabled); } if (profile.agent_kind === "openhands") { // A profile can reference an LLM profile that has since been @@ -448,6 +499,7 @@ export function AgentProfilesView() { llmProfiles, defaultLlmProfileRef, orgSecretNames, + orgMcpEnabled, t, ], ); @@ -530,6 +582,7 @@ export function AgentProfilesView() { isAddDisabled={!isEditorReady} isAddOrgDisabled={!isOrgEditorReady} canManageOrg={canManageOrg} + orgMcpServersEnabled={orgMcpEnabled} /> ); } @@ -577,7 +630,11 @@ export function AgentProfilesView() { data-testid="org-agent-profile-editor-hint" className="text-sm leading-5 text-muted" > - {t(I18nKey.SETTINGS$ORG_AGENT_PROFILE_EDITOR_HINT)} + {t( + orgMcpEnabled + ? I18nKey.SETTINGS$ORG_AGENT_PROFILE_EDITOR_HINT_WITH_MCP + : I18nKey.SETTINGS$ORG_AGENT_PROFILE_EDITOR_HINT, + )}

) : null} {isLegacyOrgProfile ? ( @@ -585,7 +642,11 @@ export function AgentProfilesView() { data-testid="org-agent-profile-legacy-warning" className="text-sm leading-5 text-yellow-400" > - {t(I18nKey.SETTINGS$ORG_AGENT_PROFILE_LEGACY_WARNING)} + {t( + orgMcpEnabled + ? I18nKey.SETTINGS$ORG_AGENT_PROFILE_LEGACY_WARNING_WITH_MCP + : I18nKey.SETTINGS$ORG_AGENT_PROFILE_LEGACY_WARNING, + )}

) : null} @@ -622,12 +683,13 @@ export function AgentProfilesView() { tools={toolItems} toolsLoading={isToolCatalogPending} toolsLoadFailed={isToolCatalogError} - mcpServers={mcpServers} + mcpServers={isOrgEditor ? orgMcpItems : mcpServers} secrets={isOrgEditor ? orgSecretItems : secretItems} skills={skillItems} skillsLoadFailed={skillsLoadFailed || isMarketplaceSkillsError} credentialForm={credentialForm} profileScope={editingScope} + orgMcpServersEnabled={orgMcpEnabled} />
diff --git a/frontend/src/i18n/declaration.ts b/frontend/src/i18n/declaration.ts index f514342859..b9bb3770d0 100644 --- a/frontend/src/i18n/declaration.ts +++ b/frontend/src/i18n/declaration.ts @@ -2301,6 +2301,11 @@ export enum I18nKey { SETTINGS$ORG_AGENT_PROFILES_ADMIN_DESCRIPTION = "SETTINGS$ORG_AGENT_PROFILES_ADMIN_DESCRIPTION", SETTINGS$ORG_AGENT_PROFILE_EDITOR_HINT = "SETTINGS$ORG_AGENT_PROFILE_EDITOR_HINT", SETTINGS$ORG_AGENT_PROFILE_LEGACY_WARNING = "SETTINGS$ORG_AGENT_PROFILE_LEGACY_WARNING", + SETTINGS$AGENT_PROFILE_ORG_MCP_HINT = "SETTINGS$AGENT_PROFILE_ORG_MCP_HINT", + SETTINGS$AGENT_PROFILE_ORG_MCP_NONE = "SETTINGS$AGENT_PROFILE_ORG_MCP_NONE", + SETTINGS$ORG_AGENT_PROFILES_ADMIN_DESCRIPTION_WITH_MCP = "SETTINGS$ORG_AGENT_PROFILES_ADMIN_DESCRIPTION_WITH_MCP", + SETTINGS$ORG_AGENT_PROFILE_EDITOR_HINT_WITH_MCP = "SETTINGS$ORG_AGENT_PROFILE_EDITOR_HINT_WITH_MCP", + SETTINGS$ORG_AGENT_PROFILE_LEGACY_WARNING_WITH_MCP = "SETTINGS$ORG_AGENT_PROFILE_LEGACY_WARNING_WITH_MCP", SETTINGS$AGENT_PROFILE_ORG_SECRETS_HINT = "SETTINGS$AGENT_PROFILE_ORG_SECRETS_HINT", SETTINGS$AGENT_PROFILE_ORG_SECRETS_NONE = "SETTINGS$AGENT_PROFILE_ORG_SECRETS_NONE", SETTINGS$AGENT_PROFILE_ORG_SKILLS_HINT = "SETTINGS$AGENT_PROFILE_ORG_SKILLS_HINT", diff --git a/frontend/src/i18n/translation.json b/frontend/src/i18n/translation.json index c6ff185590..71178e5e10 100644 --- a/frontend/src/i18n/translation.json +++ b/frontend/src/i18n/translation.json @@ -39116,6 +39116,91 @@ "uk": "Цей профіль досі дозволяє учасникам використовувати власні секрети та сервери MCP. Збереження тут робить його лише організаційним: учасники, які його запускають, отримують лише вибрані нижче спільні секрети організації та жодних серверів MCP.", "ca": "Aquest perfil encara permet que els membres facin servir els seus propis secrets i servidors MCP. Desar-lo aquí el fa exclusiu de l'organització: els membres que l'iniciïn només rebran els secrets compartits de l'organització triats a sota i cap servidor MCP." }, + "SETTINGS$AGENT_PROFILE_ORG_MCP_HINT": { + "en": "Switch on the organization-managed MCP servers this profile may use. Members launching it get only those; their own servers stay out.", + "ja": "このプロファイルが使用できる組織管理の MCP サーバーをオンにします。起動するメンバーはそれらのみを利用でき、各自のサーバーは含まれません。", + "zh-CN": "开启此配置文件可以使用的组织管理 MCP 服务器。启动它的成员只会获得这些服务器,不包含他们自己的服务器。", + "zh-TW": "開啟此設定檔可以使用的組織管理 MCP 伺服器。啟動它的成員只會取得這些伺服器,不包含他們自己的伺服器。", + "ko-KR": "이 프로필이 사용할 수 있는 조직 관리 MCP 서버를 켜세요. 이 프로필로 시작하는 구성원은 해당 서버만 사용하며 본인 서버는 포함되지 않습니다.", + "no": "Slå på de organisasjonsstyrte MCP-serverne denne profilen kan bruke. Medlemmer som starter den får bare disse; deres egne servere holdes utenfor.", + "it": "Attiva i server MCP gestiti dall'organizzazione che questo profilo può usare. I membri che lo avviano ottengono solo quelli; i loro server restano esclusi.", + "pt": "Ativa os servidores MCP geridos pela organização que este perfil pode usar. Os membros que o iniciam recebem apenas esses; os seus próprios servidores ficam de fora.", + "es": "Activa los servidores MCP gestionados por la organización que este perfil puede usar. Los miembros que lo inicien solo obtienen esos; sus propios servidores quedan fuera.", + "ar": "فعّل خوادم MCP التي تديرها المؤسسة والتي يمكن لهذا الملف استخدامها. يحصل الأعضاء الذين يشغّلونه على هذه الخوادم فقط؛ وتبقى خوادمهم الخاصة خارجه.", + "fr": "Activez les serveurs MCP gérés par l'organisation que ce profil peut utiliser. Les membres qui le lancent n'obtiennent que ceux-là ; leurs propres serveurs restent exclus.", + "tr": "Bu profilin kullanabileceği kuruluş tarafından yönetilen MCP sunucularını açın. Profili başlatan üyeler yalnızca bunları alır; kendi sunucuları dışarıda kalır.", + "de": "Schalten Sie die von der Organisation verwalteten MCP-Server ein, die dieses Profil nutzen darf. Mitglieder, die es starten, erhalten nur diese; ihre eigenen Server bleiben außen vor.", + "uk": "Увімкніть керовані організацією MCP-сервери, які може використовувати цей профіль. Учасники, що запускають його, отримують лише їх; власні сервери не долучаються.", + "ca": "Activa els servidors MCP gestionats per l'organització que aquest perfil pot utilitzar. Els membres que l'iniciïn només obtindran aquests; els seus propis servidors en queden fora." + }, + "SETTINGS$AGENT_PROFILE_ORG_MCP_NONE": { + "en": "Your organization has no organization-managed MCP servers yet. Add them under Settings > MCP.", + "ja": "組織にはまだ組織管理の MCP サーバーがありません。「設定 > MCP」で追加してください。", + "zh-CN": "你的组织还没有组织管理的 MCP 服务器。请在“设置 > MCP”中添加。", + "zh-TW": "你的組織還沒有組織管理的 MCP 伺服器。請在「設定 > MCP」中新增。", + "ko-KR": "조직에 아직 조직 관리 MCP 서버가 없습니다. 설정 > MCP에서 추가하세요.", + "no": "Organisasjonen har ingen organisasjonsstyrte MCP-servere ennå. Legg dem til under Innstillinger > MCP.", + "it": "L'organizzazione non ha ancora server MCP gestiti dall'organizzazione. Aggiungili in Impostazioni > MCP.", + "pt": "A organização ainda não tem servidores MCP geridos pela organização. Adiciona-os em Definições > MCP.", + "es": "La organización todavía no tiene servidores MCP gestionados por la organización. Agrégalos en Configuración > MCP.", + "ar": "لا تملك مؤسستك خوادم MCP تديرها المؤسسة بعد. أضفها ضمن الإعدادات > MCP.", + "fr": "Votre organisation n'a pas encore de serveur MCP géré par l'organisation. Ajoutez-les dans Paramètres > MCP.", + "tr": "Kuruluşunuzun henüz kuruluş tarafından yönetilen MCP sunucusu yok. Ayarlar > MCP altından ekleyin.", + "de": "Ihre Organisation hat noch keine von der Organisation verwalteten MCP-Server. Fügen Sie sie unter Einstellungen > MCP hinzu.", + "uk": "У вашої організації ще немає керованих організацією MCP-серверів. Додайте їх у Налаштування > MCP.", + "ca": "L'organització encara no té servidors MCP gestionats per l'organització. Afegeix-los a Configuració > MCP." + }, + "SETTINGS$ORG_AGENT_PROFILES_ADMIN_DESCRIPTION_WITH_MCP": { + "en": "Shared with everyone in your organization. They run on organization settings only: organization-shared secrets, organization LLM profiles, organization or instance skills, and the organization-managed MCP servers you choose.", + "ja": "組織の全員と共有されます。組織の設定のみで動作します: 組織共有シークレット、組織の LLM プロファイル、組織またはインスタンスのスキル、そして選択した組織管理の MCP サーバー。", + "zh-CN": "与组织中的所有人共享。仅使用组织设置运行:组织共享密钥、组织 LLM 配置文件、组织或实例技能,以及你选择的组织管理 MCP 服务器。", + "zh-TW": "與組織中的所有人共用。僅使用組織設定執行:組織共用密鑰、組織 LLM 設定檔、組織或執行個體技能,以及你選擇的組織管理 MCP 伺服器。", + "ko-KR": "조직의 모든 구성원과 공유됩니다. 조직 설정만으로 실행됩니다: 조직 공유 비밀 값, 조직 LLM 프로필, 조직 또는 인스턴스 스킬, 그리고 선택한 조직 관리 MCP 서버.", + "no": "Delt med alle i organisasjonen. De kjører kun på organisasjonsinnstillinger: organisasjonsdelte hemmeligheter, organisasjonens LLM-profiler, organisasjons- eller instansferdigheter og de organisasjonsstyrte MCP-serverne du velger.", + "it": "Condivisi con tutti nell'organizzazione. Usano solo impostazioni dell'organizzazione: segreti condivisi, profili LLM dell'organizzazione, skill dell'organizzazione o dell'istanza e i server MCP gestiti dall'organizzazione che scegli.", + "pt": "Partilhados com todos na organização. Funcionam apenas com definições da organização: segredos partilhados, perfis LLM da organização, skills da organização ou da instância e os servidores MCP geridos pela organização que escolheres.", + "es": "Compartidos con todos en la organización. Funcionan solo con configuración de la organización: secretos compartidos, perfiles LLM de la organización, skills de la organización o de la instancia y los servidores MCP gestionados por la organización que elijas.", + "ar": "مشتركة مع الجميع في مؤسستك. تعمل بإعدادات المؤسسة فقط: الأسرار المشتركة للمؤسسة، وملفات LLM الخاصة بالمؤسسة، ومهارات المؤسسة أو المثيل، وخوادم MCP التي تديرها المؤسسة والتي تختارها.", + "fr": "Partagés avec toute votre organisation. Ils s'exécutent uniquement avec les paramètres de l'organisation : secrets partagés, profils LLM de l'organisation, compétences de l'organisation ou de l'instance, et les serveurs MCP gérés par l'organisation que vous choisissez.", + "tr": "Kuruluşunuzdaki herkesle paylaşılır. Yalnızca kuruluş ayarlarıyla çalışır: kuruluşça paylaşılan gizli anahtarlar, kuruluş LLM profilleri, kuruluş veya örnek becerileri ve seçtiğiniz kuruluş tarafından yönetilen MCP sunucuları.", + "de": "Mit allen in Ihrer Organisation geteilt. Sie laufen ausschließlich mit Organisationseinstellungen: organisationsweit geteilte Geheimnisse, LLM-Profile der Organisation, Organisations- oder Instanz-Skills und die von der Organisation verwalteten MCP-Server, die Sie auswählen.", + "uk": "Спільні для всіх в організації. Працюють лише з налаштуваннями організації: спільні секрети, LLM-профілі організації, навички організації чи інстансу та обрані вами керовані організацією MCP-сервери.", + "ca": "Compartits amb tothom a l'organització. Funcionen només amb configuració de l'organització: secrets compartits, perfils LLM de l'organització, habilitats de l'organització o de la instància i els servidors MCP gestionats per l'organització que triïs." + }, + "SETTINGS$ORG_AGENT_PROFILE_EDITOR_HINT_WITH_MCP": { + "en": "An organization profile uses organization settings only, never a member's personal ones: organization-shared secrets, organization LLM profiles, organization or instance skills, and the organization-managed MCP servers chosen below. Any member can start conversations and automations with it.", + "ja": "組織プロファイルは組織の設定のみを使用し、メンバー個人の設定は使用しません: 組織共有シークレット、組織の LLM プロファイル、組織またはインスタンスのスキル、そして以下で選択した組織管理の MCP サーバー。どのメンバーもこれで会話や自動化を開始できます。", + "zh-CN": "组织配置文件仅使用组织设置,绝不使用成员的个人设置:组织共享密钥、组织 LLM 配置文件、组织或实例技能,以及下方选择的组织管理 MCP 服务器。任何成员都可以用它启动对话和自动化。", + "zh-TW": "組織設定檔僅使用組織設定,絕不使用成員的個人設定:組織共用密鑰、組織 LLM 設定檔、組織或執行個體技能,以及下方選擇的組織管理 MCP 伺服器。任何成員都可以用它啟動對話和自動化。", + "ko-KR": "조직 프로필은 구성원의 개인 설정이 아닌 조직 설정만 사용합니다: 조직 공유 비밀 값, 조직 LLM 프로필, 조직 또는 인스턴스 스킬, 그리고 아래에서 선택한 조직 관리 MCP 서버. 모든 구성원이 이 프로필로 대화와 자동화를 시작할 수 있습니다.", + "no": "En organisasjonsprofil bruker kun organisasjonsinnstillinger, aldri et medlems personlige: organisasjonsdelte hemmeligheter, organisasjonens LLM-profiler, organisasjons- eller instansferdigheter og de organisasjonsstyrte MCP-serverne valgt nedenfor. Alle medlemmer kan starte samtaler og automatiseringer med den.", + "it": "Un profilo dell'organizzazione usa solo impostazioni dell'organizzazione, mai quelle personali di un membro: segreti condivisi, profili LLM dell'organizzazione, skill dell'organizzazione o dell'istanza e i server MCP gestiti dall'organizzazione scelti qui sotto. Qualsiasi membro può avviare conversazioni e automazioni con esso.", + "pt": "Um perfil da organização usa apenas definições da organização, nunca as pessoais de um membro: segredos partilhados, perfis LLM da organização, skills da organização ou da instância e os servidores MCP geridos pela organização escolhidos abaixo. Qualquer membro pode iniciar conversas e automações com ele.", + "es": "Un perfil de la organización usa solo configuración de la organización, nunca la personal de un miembro: secretos compartidos, perfiles LLM de la organización, skills de la organización o de la instancia y los servidores MCP gestionados por la organización elegidos abajo. Cualquier miembro puede iniciar conversaciones y automatizaciones con él.", + "ar": "يستخدم ملف المؤسسة إعدادات المؤسسة فقط، وليس الإعدادات الشخصية لأي عضو: الأسرار المشتركة للمؤسسة، وملفات LLM الخاصة بالمؤسسة، ومهارات المؤسسة أو المثيل، وخوادم MCP التي تديرها المؤسسة والمختارة أدناه. يمكن لأي عضو بدء محادثات وأتمتة به.", + "fr": "Un profil d'organisation n'utilise que les paramètres de l'organisation, jamais ceux d'un membre : secrets partagés, profils LLM de l'organisation, compétences de l'organisation ou de l'instance, et les serveurs MCP gérés par l'organisation choisis ci-dessous. Tout membre peut lancer des conversations et des automatisations avec lui.", + "tr": "Bir kuruluş profili yalnızca kuruluş ayarlarını kullanır, asla bir üyenin kişisel ayarlarını değil: kuruluşça paylaşılan gizli anahtarlar, kuruluş LLM profilleri, kuruluş veya örnek becerileri ve aşağıda seçilen kuruluş tarafından yönetilen MCP sunucuları. Her üye onunla konuşma ve otomasyon başlatabilir.", + "de": "Ein Organisationsprofil verwendet ausschließlich Organisationseinstellungen, nie die persönlichen eines Mitglieds: organisationsweit geteilte Geheimnisse, LLM-Profile der Organisation, Organisations- oder Instanz-Skills und die unten gewählten, von der Organisation verwalteten MCP-Server. Jedes Mitglied kann damit Unterhaltungen und Automatisierungen starten.", + "uk": "Профіль організації використовує лише налаштування організації, а не особисті налаштування учасника: спільні секрети, LLM-профілі організації, навички організації чи інстансу та обрані нижче керовані організацією MCP-сервери. Будь-який учасник може запускати з ним розмови та автоматизації.", + "ca": "Un perfil d'organització només utilitza la configuració de l'organització, mai la personal d'un membre: secrets compartits, perfils LLM de l'organització, habilitats de l'organització o de la instància i els servidors MCP gestionats per l'organització triats a sota. Qualsevol membre pot iniciar-hi converses i automatitzacions." + }, + "SETTINGS$ORG_AGENT_PROFILE_LEGACY_WARNING_WITH_MCP": { + "en": "This profile still lets members use their own secrets and MCP servers. Saving it here makes it organization-only: members launching it get only the organization-shared secrets and organization-managed MCP servers chosen below.", + "ja": "このプロファイルは現在もメンバー自身のシークレットと MCP サーバーの使用を許可しています。ここで保存すると組織専用になります: 起動するメンバーは以下で選択した組織共有シークレットと組織管理の MCP サーバーのみを利用できます。", + "zh-CN": "此配置文件仍允许成员使用自己的密钥和 MCP 服务器。在此保存后将变为仅限组织:启动它的成员只会获得下方选择的组织共享密钥和组织管理 MCP 服务器。", + "zh-TW": "此設定檔仍允許成員使用自己的密鑰和 MCP 伺服器。在此儲存後將變為僅限組織:啟動它的成員只會取得下方選擇的組織共用密鑰和組織管理 MCP 伺服器。", + "ko-KR": "이 프로필은 여전히 구성원이 본인의 비밀 값과 MCP 서버를 사용하도록 허용합니다. 여기서 저장하면 조직 전용이 됩니다: 이 프로필로 시작하는 구성원은 아래에서 선택한 조직 공유 비밀 값과 조직 관리 MCP 서버만 사용합니다.", + "no": "Denne profilen lar fortsatt medlemmer bruke sine egne hemmeligheter og MCP-servere. Lagres den her, blir den kun for organisasjonen: medlemmer som starter den får bare de organisasjonsdelte hemmelighetene og organisasjonsstyrte MCP-serverne valgt nedenfor.", + "it": "Questo profilo consente ancora ai membri di usare i propri segreti e server MCP. Salvandolo qui diventa solo dell'organizzazione: i membri che lo avviano ottengono solo i segreti condivisi e i server MCP gestiti dall'organizzazione scelti qui sotto.", + "pt": "Este perfil ainda permite que os membros usem os seus próprios segredos e servidores MCP. Guardá-lo aqui torna-o exclusivo da organização: os membros que o iniciam recebem apenas os segredos partilhados e os servidores MCP geridos pela organização escolhidos abaixo.", + "es": "Este perfil todavía permite que los miembros usen sus propios secretos y servidores MCP. Guardarlo aquí lo convierte en exclusivo de la organización: los miembros que lo inicien solo obtienen los secretos compartidos y los servidores MCP gestionados por la organización elegidos abajo.", + "ar": "لا يزال هذا الملف يسمح للأعضاء باستخدام أسرارهم وخوادم MCP الخاصة بهم. حفظه هنا يجعله خاصًا بالمؤسسة فقط: يحصل الأعضاء الذين يشغّلونه على الأسرار المشتركة للمؤسسة وخوادم MCP التي تديرها المؤسسة المختارة أدناه فقط.", + "fr": "Ce profil laisse encore les membres utiliser leurs propres secrets et serveurs MCP. L'enregistrer ici le rend réservé à l'organisation : les membres qui le lancent n'obtiennent que les secrets partagés et les serveurs MCP gérés par l'organisation choisis ci-dessous.", + "tr": "Bu profil üyelerin hâlâ kendi gizli anahtarlarını ve MCP sunucularını kullanmasına izin veriyor. Burada kaydetmek onu yalnızca kuruluşa özel yapar: profili başlatan üyeler yalnızca aşağıda seçilen kuruluşça paylaşılan gizli anahtarları ve kuruluş tarafından yönetilen MCP sunucularını alır.", + "de": "Dieses Profil erlaubt Mitgliedern noch die eigenen Geheimnisse und MCP-Server. Wird es hier gespeichert, gilt es nur noch für die Organisation: Mitglieder, die es starten, erhalten nur die unten gewählten organisationsweit geteilten Geheimnisse und von der Organisation verwalteten MCP-Server.", + "uk": "Цей профіль досі дозволяє учасникам використовувати власні секрети та MCP-сервери. Збереження тут робить його лише організаційним: учасники, що запускають його, отримують лише обрані нижче спільні секрети організації та керовані організацією MCP-сервери.", + "ca": "Aquest perfil encara permet que els membres facin servir els seus propis secrets i servidors MCP. Desar-lo aquí el fa exclusiu de l'organització: els membres que l'iniciïn només obtindran els secrets compartits i els servidors MCP gestionats per l'organització triats a sota." + }, "SETTINGS$AGENT_PROFILE_ORG_SECRETS_HINT": { "en": "Organization-shared secrets this agent may use. Members' personal secrets are never passed to an organization profile.", "ja": "このエージェントが使用できる組織共有のシークレット。メンバーの個人シークレットが組織プロファイルに渡されることはありません。",