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": "このエージェントが使用できる組織共有のシークレット。メンバーの個人シークレットが組織プロファイルに渡されることはありません。",