-
- {baselineJson || 'Empty'}
+
+
+ {baselineJson ||
+ intl.formatMessage({
+ defaultMessage: 'Empty',
+ description: 'Fallback for empty server JSON in compare view',
+ })}
+
@@ -213,19 +218,7 @@ export const MCPServerVersionCompare = ({
-
+
{diff.map((part, index) => (
void;
onDeleteBinding?: (binding: MCPAccessBinding) => void;
onEditMetadata?: (version: MCPServerVersion) => void;
- onSetLatest?: (version: string | null) => void;
- setLatestLoading?: boolean;
- setLatestError?: Error | null;
- onClearLatestError?: () => void;
resolvedLatestVersion?: string;
- onUpdateDescription?: (description: string | null) => Promise;
}) => {
const { theme } = useDesignSystemTheme();
const intl = useIntl();
- const [statusModalVisible, setStatusModalVisible] = useState(false);
- const [displayNameModalVisible, setDisplayNameModalVisible] = useState(false);
+ const [editVersionModalVisible, setEditVersionModalVisible] = useState(false);
+ const [editVersionDisplayName, setEditVersionDisplayName] = useState('');
+ const [editVersionStatus, setEditVersionStatus] = useState('draft');
+ const [editVersionAliases, setEditVersionAliases] = useState([]);
+ const [editVersionToolsText, setEditVersionToolsText] = useState('');
const [deleteModalVisible, setDeleteModalVisible] = useState(false);
- const [descriptionModalVisible, setDescriptionModalVisible] = useState(false);
- const [descriptionDraft, setDescriptionDraft] = useState('');
- const [descriptionSaving, setDescriptionSaving] = useState(false);
- const [descriptionError, setDescriptionError] = useState(null);
+ const [toolsValidationError, setToolsValidationError] = useState(null);
- const updateStatusMutation = useUpdateMCPServerVersionStatus(server.name);
- const updateDisplayNameMutation = useUpdateMCPServerVersionDisplayName(server.name);
+ const updateVersionMutation = useUpdateMCPServerVersion(server.name);
const deleteVersionMutation = useDeleteMCPServerVersion(server.name);
if (!version) {
@@ -112,21 +102,6 @@ export const MCPServerVersionDetail = ({
const versionDisplayName = version.display_name || version.server_json?.title;
const showVersionDisplayName = versionDisplayName && versionDisplayName !== displayName;
- const isPinned = server.latest_version === version.version;
- const isResolvedLatest = resolvedLatestVersion === version.version;
- const isDraftVersion = version.status === 'draft';
- const setLatestDisabled = !isPinned && !isResolvedLatest && isDraftVersion;
-
- const setLatestLabel = isPinned ? (
-
- ) : isResolvedLatest && !server.latest_version ? (
-
- ) : (
-
- );
-
- const setLatestOnClick = isPinned ? () => onSetLatest?.(null) : () => onSetLatest?.(version.version);
-
return (
@@ -139,83 +114,35 @@ export const MCPServerVersionDetail = ({
/>
{showVersionDisplayName && (
-
-
- {versionDisplayName}
-
- }
- onClick={() => setDisplayNameModalVisible(true)}
- />
-
+
+ {versionDisplayName}
+
+ )}
+ {version.server_json?.description && (
+
+ {version.server_json.description}
+
)}
- {(() => {
- const description = server.description;
- return description ? (
-
- {description}
- }
- onClick={() => {
- setDescriptionDraft(description);
- setDescriptionModalVisible(true);
- }}
- />
-
- ) : (
-
- );
- })()}
- {onSetLatest &&
- (() => {
- const button = (
-
- );
- return setLatestDisabled ? (
-
- {button}
-
- ) : (
- button
- );
- })()}
+ }
+ onClick={() => {
+ setEditVersionDisplayName(version.display_name || version.server_json?.title || '');
+ setEditVersionStatus(version.status);
+ const currentAliases = (aliasesByVersion[version.version] ?? []).filter((a) => a !== 'latest');
+ setEditVersionAliases(currentAliases);
+ setEditVersionToolsText(version.tools?.length ? JSON.stringify(version.tools, null, 2) : '');
+ setEditVersionModalVisible(true);
+ }}
+ >
+
+
}
@@ -228,18 +155,6 @@ export const MCPServerVersionDetail = ({
- {setLatestError && (
- <>
-
-
- >
- )}
@@ -290,12 +205,6 @@ export const MCPServerVersionDetail = ({
{version.status}
-
}
- onClick={() => setStatusModalVisible(true)}
- />
{version.server_json?.websiteUrl && (
@@ -340,17 +249,21 @@ export const MCPServerVersionDetail = ({
- {Object.keys(version.tags).length > 0
- ? Object.entries(version.tags).map(([key, value]) => (
+ {Object.keys(version.tags ?? {}).length > 0
+ ? Object.entries(version.tags ?? {}).map(([key, value]) => (
))
: !onEditMetadata && —}
{onEditMetadata &&
- (Object.keys(version.tags).length > 0 ? (
+ (Object.keys(version.tags ?? {}).length > 0 ? (
}
+ aria-label={intl.formatMessage({
+ defaultMessage: 'Edit metadata',
+ description: 'Aria label for edit metadata button',
+ })}
onClick={() => onEditMetadata(version)}
/>
) : (
@@ -367,52 +280,282 @@ export const MCPServerVersionDetail = ({
- {version.server_json &&
}
+
+ {version.server_json && (
+
+
+
+ }
+ defaultCollapsed
+ componentId="mlflow.mcp_registry.detail.configuration_section"
+ >
+
+ {JSON.stringify(version.server_json, null, 2)}
+
+
+ )}
+ {version.tools && version.tools.length > 0 && (
+
+ {chunks},
+ }}
+ />
+
+ }
+ defaultCollapsed
+ componentId="mlflow.mcp_registry.detail.tools_section"
+ >
+
+
+
+
+ {version.tools.map((tool) => (
+
+
+ {tool.name}
+
+ {tool.description && (
+
+ {tool.description}
+
+ )}
+
+ ))}
+
+
+ )}
-
-
+
+
+
+ }
+ defaultCollapsed
+ componentId="mlflow.mcp_registry.detail.bindings_section"
+ >
+
+
+
-
{
- updateStatusMutation.mutate(
- { version: version.version, status: newStatus },
- { onSuccess: () => setStatusModalVisible(false) },
- );
- }}
- onCancel={() => {
- updateStatusMutation.reset();
- setStatusModalVisible(false);
- }}
- />
+
+ }
+ visible={editVersionModalVisible}
+ destroyOnClose
+ confirmLoading={updateVersionMutation.isLoading}
+ okText={}
+ onOk={() => {
+ const payload: Parameters[0] = { version: version.version };
+
+ if (editVersionDisplayName !== (version.display_name || version.server_json?.title || '')) {
+ payload.displayName = editVersionDisplayName;
+ }
+ if (editVersionStatus !== version.status) {
+ payload.status = editVersionStatus;
+ }
+
+ setToolsValidationError(null);
+ const currentToolsJson = version.tools?.length ? JSON.stringify(version.tools, null, 2) : '';
+ if (editVersionToolsText !== currentToolsJson) {
+ if (editVersionToolsText.trim()) {
+ const toolsResult = validateToolsJson(editVersionToolsText);
+ if (!toolsResult.valid) {
+ setToolsValidationError(toolsResult.error ?? 'Invalid tools JSON');
+ return;
+ }
+ payload.tools = toolsResult.parsed ?? null;
+ } else {
+ payload.tools = [];
+ }
+ }
+
+ const existingAliases = (aliasesByVersion[version.version] ?? []).filter((a) => a !== LATEST_ALIAS);
+ const addedAliases = editVersionAliases.filter((a) => !existingAliases.includes(a));
+ const deletedAliases = existingAliases.filter((a) => !editVersionAliases.includes(a));
+ if (addedAliases.length > 0 || deletedAliases.length > 0) {
+ payload.aliases = { add: addedAliases, remove: deletedAliases };
+ }
- {
- updateDisplayNameMutation.mutate(
- { version: version.version, displayName },
- { onSuccess: () => setDisplayNameModalVisible(false) },
- );
+ updateVersionMutation.mutate(payload, {
+ onSuccess: () => setEditVersionModalVisible(false),
+ });
}}
onCancel={() => {
- updateDisplayNameMutation.reset();
- setDisplayNameModalVisible(false);
+ updateVersionMutation.reset();
+ setToolsValidationError(null);
+ setEditVersionModalVisible(false);
}}
- />
+ >
+ {updateVersionMutation.error && (
+ updateVersionMutation.reset()}
+ message={(updateVersionMutation.error as Error).message}
+ css={{ marginBottom: theme.spacing.sm }}
+ />
+ )}
+
+
+
+
+
+ setEditVersionDisplayName(e.target.value)}
+ placeholder={intl.formatMessage({
+ defaultMessage: 'Enter display name',
+ description: 'Placeholder for version display name input',
+ })}
+ />
+
+
+
+
+
+ setEditVersionStatus(target.value as MCPStatus)}
+ >
+ {(['draft', 'active', 'deprecated'] as MCPStatus[]).map((s) => (
+
+ {s.charAt(0).toUpperCase() + s.slice(1)}
+
+ ))}
+
+
+
+
+
+
+
a.alias).filter((a) => !RESERVED_ALIASES.includes(a))}
+ setDraftAliases={setEditVersionAliases}
+ version={version.version}
+ aliasToVersionMap={Object.fromEntries((server.aliases ?? []).map((a) => [a.alias, a.version]))}
+ pinnedAliases={resolvedLatestVersion === version.version ? [LATEST_ALIAS] : undefined}
+ pinnedAliasColor={aliasColors?.[LATEST_ALIAS]}
+ />
+
+
+
+
+
+ {toolsValidationError && (
+
setToolsValidationError(null)}
+ message={toolsValidationError}
+ css={{ marginBottom: theme.spacing.xs }}
+ />
+ )}
+ {
+ setEditVersionToolsText(e.target.value);
+ setToolsValidationError(null);
+ }}
+ autoSize={{ minRows: 4, maxRows: 12 }}
+ css={{ fontFamily: 'monospace' }}
+ placeholder={intl.formatMessage({
+ defaultMessage: 'Enter tools JSON array',
+ description: 'Placeholder for version tools input',
+ })}
+ />
+
+
+
-
- }
- visible={descriptionModalVisible}
- destroyOnClose
- confirmLoading={descriptionSaving}
- okText={
-
- }
- cancelText={
-
- }
- onOk={async () => {
- if (onUpdateDescription) {
- setDescriptionSaving(true);
- setDescriptionError(null);
- try {
- await onUpdateDescription(descriptionDraft || null);
- setDescriptionModalVisible(false);
- } catch (e) {
- setDescriptionError(e as Error);
- } finally {
- setDescriptionSaving(false);
- }
- }
- }}
- onCancel={() => {
- setDescriptionError(null);
- setDescriptionModalVisible(false);
- }}
- >
- {descriptionError && (
- setDescriptionError(null)}
- message={descriptionError.message}
- css={{ marginBottom: theme.spacing.sm }}
- />
- )}
- setDescriptionDraft(e.target.value)}
- autoSize={{ minRows: 3, maxRows: 10 }}
- placeholder={intl.formatMessage({
- defaultMessage: 'Enter a description',
- description: 'Placeholder for MCP server version description textarea',
- })}
- />
-
);
};
diff --git a/mlflow/server/js/src/mcp-registry/components/MCPServerVersionDiffSelectorButton.test.tsx b/mlflow/server/js/src/mcp-registry/components/MCPServerVersionDiffSelectorButton.test.tsx
new file mode 100644
index 0000000000000..b05a36f6484e7
--- /dev/null
+++ b/mlflow/server/js/src/mcp-registry/components/MCPServerVersionDiffSelectorButton.test.tsx
@@ -0,0 +1,53 @@
+import { describe, it, expect, jest } from '@jest/globals';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { IntlProvider } from 'react-intl';
+import { DesignSystemProvider } from '@databricks/design-system';
+import { MCPServerVersionDiffSelectorButton } from './MCPServerVersionDiffSelectorButton';
+
+const renderButton = (props: Partial> = {}) =>
+ render(
+
+
+
+
+ ,
+ );
+
+describe('MCPServerVersionDiffSelectorButton', () => {
+ it('renders two radio buttons', () => {
+ renderButton();
+ const radios = screen.getAllByRole('radio');
+ expect(radios).toHaveLength(2);
+ });
+
+ it('marks baseline as checked when selected', () => {
+ renderButton({ isSelectedBaseline: true });
+ const radios = screen.getAllByRole('radio');
+ expect(radios[0]).toHaveAttribute('aria-checked', 'true');
+ expect(radios[1]).toHaveAttribute('aria-checked', 'false');
+ });
+
+ it('marks compared as checked when selected', () => {
+ renderButton({ isSelectedCompared: true });
+ const radios = screen.getAllByRole('radio');
+ expect(radios[0]).toHaveAttribute('aria-checked', 'false');
+ expect(radios[1]).toHaveAttribute('aria-checked', 'true');
+ });
+
+ it('calls onSelectBaseline when baseline button is clicked', async () => {
+ const onSelectBaseline = jest.fn();
+ renderButton({ onSelectBaseline });
+ const radios = screen.getAllByRole('radio');
+ await userEvent.click(radios[0]);
+ expect(onSelectBaseline).toHaveBeenCalledTimes(1);
+ });
+
+ it('calls onSelectCompared when compared button is clicked', async () => {
+ const onSelectCompared = jest.fn();
+ renderButton({ onSelectCompared });
+ const radios = screen.getAllByRole('radio');
+ await userEvent.click(radios[1]);
+ expect(onSelectCompared).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/mlflow/server/js/src/mcp-registry/components/MCPServerVersionDiffSelectorButton.tsx b/mlflow/server/js/src/mcp-registry/components/MCPServerVersionDiffSelectorButton.tsx
index f92a695cd7d8f..5905c4fb3873f 100644
--- a/mlflow/server/js/src/mcp-registry/components/MCPServerVersionDiffSelectorButton.tsx
+++ b/mlflow/server/js/src/mcp-registry/components/MCPServerVersionDiffSelectorButton.tsx
@@ -18,7 +18,14 @@ export const MCPServerVersionDiffSelectorButton = ({
-
+
{
- const { theme } = useDesignSystemTheme();
- const [expanded, setExpanded] = useState(false);
- const formattedJson = useMemo(() => JSON.stringify(serverJson, null, 2), [serverJson]);
-
- return (
- <>
-
-
- {expanded && (
- <>
-
-
- {formattedJson}
-
- >
- )}
- >
- );
-};
diff --git a/mlflow/server/js/src/mcp-registry/components/UpdateDescriptionModal.test.tsx b/mlflow/server/js/src/mcp-registry/components/UpdateDescriptionModal.test.tsx
new file mode 100644
index 0000000000000..14b69359678d8
--- /dev/null
+++ b/mlflow/server/js/src/mcp-registry/components/UpdateDescriptionModal.test.tsx
@@ -0,0 +1,90 @@
+import { describe, it, expect, jest } from '@jest/globals';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { IntlProvider } from 'react-intl';
+import { DesignSystemProvider } from '@databricks/design-system';
+import { UpdateDescriptionModal } from './UpdateDescriptionModal';
+
+const renderModal = (props: Partial> = {}) =>
+ render(
+
+
+
+
+ ,
+ );
+
+describe('UpdateDescriptionModal', () => {
+ it('renders with current description', () => {
+ renderModal();
+ expect(screen.getByDisplayValue('Original description')).toBeInTheDocument();
+ });
+
+ it('does not render when not visible', () => {
+ renderModal({ visible: false });
+ expect(screen.queryByDisplayValue('Original description')).not.toBeInTheDocument();
+ });
+
+ it('calls onUpdate with null for empty description', async () => {
+ const onUpdate = jest.fn();
+ renderModal({ onUpdate, currentDescription: '' });
+ await userEvent.click(screen.getByText('Save'));
+ expect(onUpdate).toHaveBeenCalledWith(null);
+ });
+
+ it('calls onUpdate with text for non-empty description', async () => {
+ const onUpdate = jest.fn();
+ renderModal({ onUpdate });
+ const textarea = screen.getByDisplayValue('Original description');
+ await userEvent.clear(textarea);
+ await userEvent.type(textarea, 'New description');
+ await userEvent.click(screen.getByText('Save'));
+ expect(onUpdate).toHaveBeenCalledWith('New description');
+ });
+
+ it('calls onCancel when cancel is clicked', async () => {
+ const onCancel = jest.fn();
+ renderModal({ onCancel });
+ await userEvent.click(screen.getByText('Cancel'));
+ expect(onCancel).toHaveBeenCalledTimes(1);
+ });
+
+ it('shows error alert when error is provided', () => {
+ renderModal({ error: new Error('Update failed') });
+ expect(screen.getByText('Update failed')).toBeInTheDocument();
+ });
+
+ it('resets draft when reopened with different description', () => {
+ const { rerender } = render(
+
+
+
+
+ ,
+ );
+ rerender(
+
+
+
+
+ ,
+ );
+ expect(screen.getByDisplayValue('Second')).toBeInTheDocument();
+ });
+});
diff --git a/mlflow/server/js/src/mcp-registry/components/UpdateDescriptionModal.tsx b/mlflow/server/js/src/mcp-registry/components/UpdateDescriptionModal.tsx
new file mode 100644
index 0000000000000..96297c887a990
--- /dev/null
+++ b/mlflow/server/js/src/mcp-registry/components/UpdateDescriptionModal.tsx
@@ -0,0 +1,75 @@
+import { useEffect, useState } from 'react';
+import { Alert, Input, Modal, useDesignSystemTheme } from '@databricks/design-system';
+import { FormattedMessage, useIntl } from 'react-intl';
+
+export const UpdateDescriptionModal = ({
+ visible,
+ currentDescription,
+ isLoading,
+ error,
+ onUpdate,
+ onCancel,
+ onClearError,
+}: {
+ visible: boolean;
+ currentDescription: string;
+ isLoading?: boolean;
+ error?: Error | null;
+ onUpdate: (description: string | null) => void;
+ onCancel: () => void;
+ onClearError?: () => void;
+}) => {
+ const { theme } = useDesignSystemTheme();
+ const intl = useIntl();
+ const [draft, setDraft] = useState(currentDescription);
+
+ useEffect(() => {
+ if (visible) {
+ setDraft(currentDescription);
+ }
+ }, [visible, currentDescription]);
+
+ return (
+
+ }
+ visible={visible}
+ destroyOnClose
+ confirmLoading={isLoading}
+ okText={
+
+ }
+ cancelText={
+
+ }
+ onOk={() => onUpdate(draft || null)}
+ onCancel={onCancel}
+ >
+ {error && (
+
+ )}
+ setDraft(e.target.value)}
+ autoSize={{ minRows: 3, maxRows: 10 }}
+ placeholder={intl.formatMessage({
+ defaultMessage: 'Enter a description',
+ description: 'Placeholder for MCP server version description textarea',
+ })}
+ />
+
+ );
+};
diff --git a/mlflow/server/js/src/mcp-registry/components/UpdateVersionDisplayNameModal.test.tsx b/mlflow/server/js/src/mcp-registry/components/UpdateVersionDisplayNameModal.test.tsx
new file mode 100644
index 0000000000000..4e21d88b3571b
--- /dev/null
+++ b/mlflow/server/js/src/mcp-registry/components/UpdateVersionDisplayNameModal.test.tsx
@@ -0,0 +1,76 @@
+import { describe, it, expect, jest } from '@jest/globals';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { IntlProvider } from 'react-intl';
+import { DesignSystemProvider } from '@databricks/design-system';
+import { UpdateVersionDisplayNameModal } from './UpdateVersionDisplayNameModal';
+
+const renderModal = (props: Partial> = {}) =>
+ render(
+
+
+
+
+ ,
+ );
+
+describe('UpdateVersionDisplayNameModal', () => {
+ it('renders with current display name', () => {
+ renderModal();
+ expect(screen.getByDisplayValue('Original Name')).toBeInTheDocument();
+ });
+
+ it('does not render when not visible', () => {
+ renderModal({ visible: false });
+ expect(screen.queryByDisplayValue('Original Name')).not.toBeInTheDocument();
+ });
+
+ it('calls onUpdate with trimmed value on save', async () => {
+ const onUpdate = jest.fn();
+ renderModal({ onUpdate });
+ const input = screen.getByDisplayValue('Original Name');
+ await userEvent.clear(input);
+ await userEvent.type(input, ' New Name ');
+ await userEvent.click(screen.getByText('Save'));
+ expect(onUpdate).toHaveBeenCalledWith('New Name');
+ });
+
+ it('shows error alert when error is provided', () => {
+ renderModal({ error: new Error('Failed to save') });
+ expect(screen.getByText('Failed to save')).toBeInTheDocument();
+ });
+
+ it('resets draft when visibility changes', () => {
+ const { rerender } = render(
+
+
+
+
+ ,
+ );
+ rerender(
+
+
+
+
+ ,
+ );
+ expect(screen.getByDisplayValue('Updated')).toBeInTheDocument();
+ });
+});
diff --git a/mlflow/server/js/src/mcp-registry/components/UpdateVersionDisplayNameModal.tsx b/mlflow/server/js/src/mcp-registry/components/UpdateVersionDisplayNameModal.tsx
index 8f1c437d27a80..b82dc1d4eb942 100644
--- a/mlflow/server/js/src/mcp-registry/components/UpdateVersionDisplayNameModal.tsx
+++ b/mlflow/server/js/src/mcp-registry/components/UpdateVersionDisplayNameModal.tsx
@@ -58,6 +58,10 @@ export const UpdateVersionDisplayNameModal = ({
componentId="mlflow.mcp_registry.detail.display_name_input"
value={draft}
onChange={(e) => setDraft(e.target.value)}
+ aria-label={intl.formatMessage({
+ defaultMessage: 'Display name',
+ description: 'Aria label for display name input',
+ })}
placeholder={intl.formatMessage({
defaultMessage: 'Enter display name',
description: 'Placeholder for version display name input',
diff --git a/mlflow/server/js/src/mcp-registry/components/UpdateVersionStatusModal.tsx b/mlflow/server/js/src/mcp-registry/components/UpdateVersionStatusModal.tsx
deleted file mode 100644
index a30ad49f6ab70..0000000000000
--- a/mlflow/server/js/src/mcp-registry/components/UpdateVersionStatusModal.tsx
+++ /dev/null
@@ -1,122 +0,0 @@
-import { useEffect, useState } from 'react';
-import {
- Alert,
- Modal,
- SimpleSelect,
- SimpleSelectOption,
- Typography,
- useDesignSystemTheme,
-} from '@databricks/design-system';
-import { FormattedMessage, useIntl } from 'react-intl';
-
-import type { MCPStatus } from '../types';
-import { STATUS_TRANSITIONS } from '../utils';
-
-const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);
-
-export const UpdateVersionStatusModal = ({
- visible,
- currentStatus,
- isLoading,
- error,
- onUpdate,
- onCancel,
-}: {
- visible: boolean;
- currentStatus: MCPStatus;
- isLoading?: boolean;
- error?: Error | null;
- onUpdate: (newStatus: MCPStatus) => void;
- onCancel: () => void;
-}) => {
- const { theme } = useDesignSystemTheme();
- const intl = useIntl();
- const allowedTransitions = STATUS_TRANSITIONS[currentStatus] ?? [];
- const isTerminal = allowedTransitions.length === 0;
- const [selectedStatus, setSelectedStatus] = useState(allowedTransitions[0]);
-
- useEffect(() => {
- if (visible) {
- const transitions = STATUS_TRANSITIONS[currentStatus] ?? [];
- setSelectedStatus(transitions[0]);
- }
- }, [visible, currentStatus]);
-
- return (
-
- }
- visible={visible}
- onCancel={onCancel}
- onOk={() => selectedStatus && onUpdate(selectedStatus)}
- okText={intl.formatMessage({
- defaultMessage: 'Update',
- description: 'MCP server update version status modal confirm button',
- })}
- confirmLoading={isLoading}
- okButtonProps={{ disabled: isTerminal || !selectedStatus }}
- >
-
- {error && (
-
- )}
- {isTerminal && (
-
- )}
- {!isTerminal && (
- <>
-
-
-
-
- {capitalize(currentStatus)}
-
-
-
-
-
- setSelectedStatus(target.value as MCPStatus)}
- >
- {(['draft', 'active', 'deprecated'] as MCPStatus[])
- .filter((status) => status !== currentStatus)
- .map((status) => (
-
- {capitalize(status)}
-
- ))}
-
-
- >
- )}
-
-
- );
-};
diff --git a/mlflow/server/js/src/mcp-registry/hooks/useCreateMCPServerVersionMutation.ts b/mlflow/server/js/src/mcp-registry/hooks/useCreateMCPServerVersionMutation.ts
index 647c391f3f861..004d63498c094 100644
--- a/mlflow/server/js/src/mcp-registry/hooks/useCreateMCPServerVersionMutation.ts
+++ b/mlflow/server/js/src/mcp-registry/hooks/useCreateMCPServerVersionMutation.ts
@@ -1,6 +1,7 @@
import { useMutation, useQueryClient } from '@mlflow/mlflow/src/common/utils/reactQueryHooks';
import { MCPRegistryApi } from '../api';
import type { MCPServerVersion, MCPStatus, MCPTool, ServerJSONPayload } from '../types';
+import { MCP_QUERY_KEYS } from '../utils';
type CreateMCPServerVersionPayload = {
serverJson: ServerJSONPayload;
@@ -26,32 +27,36 @@ export const useCreateMCPServerVersionMutation = () => {
tools,
});
- if (isNewServer) {
- const serverDisplayName = displayName || serverJson.title;
- if (serverDisplayName || serverJson.description) {
- await MCPRegistryApi.updateMCPServer(name, {
- display_name: serverDisplayName || undefined,
- description: serverJson.description || undefined,
- });
+ try {
+ if (isNewServer) {
+ const serverDisplayName = displayName || serverJson.title;
+ if (serverDisplayName || serverJson.description) {
+ await MCPRegistryApi.updateMCPServer(name, {
+ display_name: serverDisplayName || undefined,
+ description: serverJson.description || undefined,
+ });
+ }
}
- }
- if (tags) {
- const setTag = isNewServer
- ? (key: string, value: string) => MCPRegistryApi.setMCPServerTag(name, { key, value })
- : (key: string, value: string) =>
- MCPRegistryApi.setMCPServerVersionTag(name, version.version, { key, value });
- await Promise.all(Object.entries(tags).map(([key, value]) => setTag(key, value)));
+ if (tags) {
+ const setTag = isNewServer
+ ? (key: string, value: string) => MCPRegistryApi.setMCPServerTag(name, { key, value })
+ : (key: string, value: string) =>
+ MCPRegistryApi.setMCPServerVersionTag(name, version.version, { key, value });
+ await Promise.all(Object.entries(tags).map(([key, value]) => setTag(key, value)));
+ }
+ } catch (e) {
+ console.warn('Version created but secondary metadata/tag operation failed:', e);
}
return version;
},
onSuccess: (_data, { serverJson }) => {
const name = serverJson.name;
- queryClient.invalidateQueries(['mcp_servers_list']);
- queryClient.invalidateQueries(['mcp_server', name]);
- queryClient.invalidateQueries(['mcp_server_versions', name]);
- queryClient.invalidateQueries(['mcp_server_latest_version', name]);
+ queryClient.invalidateQueries([MCP_QUERY_KEYS.SERVERS_LIST]);
+ queryClient.invalidateQueries([MCP_QUERY_KEYS.SERVER, name]);
+ queryClient.invalidateQueries([MCP_QUERY_KEYS.SERVER_VERSIONS, name]);
+ queryClient.invalidateQueries([MCP_QUERY_KEYS.SERVER_LATEST_VERSION, name]);
},
});
};
diff --git a/mlflow/server/js/src/mcp-registry/hooks/useMCPServerDetailQuery.ts b/mlflow/server/js/src/mcp-registry/hooks/useMCPServerDetailQuery.ts
index ff43b9afc31d0..06489d71614e1 100644
--- a/mlflow/server/js/src/mcp-registry/hooks/useMCPServerDetailQuery.ts
+++ b/mlflow/server/js/src/mcp-registry/hooks/useMCPServerDetailQuery.ts
@@ -30,17 +30,20 @@ export const useMCPServerVersionsQuery = (name: string) => {
};
};
-export const useLatestMCPServerVersionQuery = (name: string) => {
- return useQuery(['mcp_server_latest_version', name], {
+export const useLatestMCPServerVersionQuery = (name: string, enabled = true) => {
+ return useQuery([MCP_QUERY_KEYS.SERVER_LATEST_VERSION, name], {
queryFn: async () => {
try {
return await MCPRegistryApi.getLatestMCPServerVersion(name);
- } catch {
- return undefined;
+ } catch (e: unknown) {
+ if (e instanceof Error && (e.message.includes('404') || e.message.includes('RESOURCE_DOES_NOT_EXIST'))) {
+ return undefined;
+ }
+ throw e;
}
},
retry: false,
- enabled: Boolean(name),
+ enabled: Boolean(name) && enabled,
});
};
diff --git a/mlflow/server/js/src/mcp-registry/hooks/useMCPServerDetailViewState.test.ts b/mlflow/server/js/src/mcp-registry/hooks/useMCPServerDetailViewState.test.ts
new file mode 100644
index 0000000000000..a9274e69cecff
--- /dev/null
+++ b/mlflow/server/js/src/mcp-registry/hooks/useMCPServerDetailViewState.test.ts
@@ -0,0 +1,85 @@
+import { describe, it, expect } from '@jest/globals';
+import { renderHook, act } from '@testing-library/react';
+import { useMCPServerDetailViewState, MCPServerDetailViewMode } from './useMCPServerDetailViewState';
+import { createMockMCPServerVersion } from '../test-utils';
+
+const v1 = createMockMCPServerVersion({ version: '1' });
+const v2 = createMockMCPServerVersion({ version: '2' });
+const v3 = createMockMCPServerVersion({ version: '3' });
+
+describe('useMCPServerDetailViewState', () => {
+ it('starts in preview mode with no selection', () => {
+ const { result } = renderHook(() => useMCPServerDetailViewState([v1, v2]));
+ expect(result.current.viewState.mode).toBe(MCPServerDetailViewMode.PREVIEW);
+ expect(result.current.selectedVersion).toBeUndefined();
+ });
+
+ it('setSelectedVersion updates the selected version', () => {
+ const { result } = renderHook(() => useMCPServerDetailViewState([v1, v2]));
+ act(() => result.current.setSelectedVersion('1'));
+ expect(result.current.selectedVersion).toBe('1');
+ });
+
+ it('setPreviewMode clears comparedVersion and sets preview mode', () => {
+ const { result } = renderHook(() => useMCPServerDetailViewState([v1, v2]));
+ act(() => result.current.setCompareMode());
+ expect(result.current.viewState.mode).toBe(MCPServerDetailViewMode.COMPARE);
+
+ act(() => result.current.setPreviewMode());
+ expect(result.current.viewState.mode).toBe(MCPServerDetailViewMode.PREVIEW);
+ expect(result.current.viewState.comparedVersion).toBeUndefined();
+ });
+
+ it('setCompareMode auto-selects baseline and compared versions', () => {
+ const { result } = renderHook(() => useMCPServerDetailViewState([v1, v2, v3]));
+ act(() => result.current.setSelectedVersion('1'));
+ act(() => result.current.setCompareMode());
+
+ expect(result.current.viewState.mode).toBe(MCPServerDetailViewMode.COMPARE);
+ expect(result.current.selectedVersion).toBeDefined();
+ expect(result.current.viewState.comparedVersion).toBeDefined();
+ expect(result.current.selectedVersion).not.toBe(result.current.viewState.comparedVersion);
+ });
+
+ it('switchSides swaps baseline and compared versions', () => {
+ const { result } = renderHook(() => useMCPServerDetailViewState([v1, v2]));
+ act(() => result.current.setSelectedVersion('1'));
+ act(() => result.current.setCompareMode());
+
+ const before = {
+ selected: result.current.selectedVersion,
+ compared: result.current.viewState.comparedVersion,
+ };
+
+ act(() => result.current.switchSides());
+
+ expect(result.current.selectedVersion).toBe(before.compared);
+ expect(result.current.viewState.comparedVersion).toBe(before.selected);
+ });
+
+ it('auto-swaps when selecting the same version as compared', () => {
+ const { result } = renderHook(() => useMCPServerDetailViewState([v1, v2]));
+ act(() => result.current.setSelectedVersion('1'));
+ act(() => {
+ result.current.setCompareMode();
+ });
+
+ const comparedBefore = result.current.viewState.comparedVersion;
+ act(() => result.current.setSelectedVersion(comparedBefore!));
+
+ expect(result.current.selectedVersion).toBe(comparedBefore);
+ expect(result.current.viewState.comparedVersion).not.toBe(comparedBefore);
+ });
+
+ it('auto-swaps when setting compared to the same version as selected', () => {
+ const { result } = renderHook(() => useMCPServerDetailViewState([v1, v2]));
+ act(() => result.current.setSelectedVersion('1'));
+ act(() => result.current.setCompareMode());
+
+ const selectedBefore = result.current.selectedVersion!;
+ act(() => result.current.setComparedVersion(selectedBefore));
+
+ expect(result.current.viewState.comparedVersion).toBe(selectedBefore);
+ expect(result.current.selectedVersion).not.toBe(selectedBefore);
+ });
+});
diff --git a/mlflow/server/js/src/mcp-registry/hooks/useMCPServerDetailViewState.ts b/mlflow/server/js/src/mcp-registry/hooks/useMCPServerDetailViewState.ts
index 88b1e9fac5a42..b9e986f75e83c 100644
--- a/mlflow/server/js/src/mcp-registry/hooks/useMCPServerDetailViewState.ts
+++ b/mlflow/server/js/src/mcp-registry/hooks/useMCPServerDetailViewState.ts
@@ -32,8 +32,14 @@ const viewStateReducer = (state: State, action: ViewAction): State => {
comparedVersion: action.comparedVersion,
};
case 'setComparedVersion':
+ if (action.comparedVersion === state.selectedVersion) {
+ return { ...state, comparedVersion: action.comparedVersion, selectedVersion: state.comparedVersion };
+ }
return { ...state, comparedVersion: action.comparedVersion };
case 'setSelectedVersion':
+ if (action.version === state.comparedVersion) {
+ return { ...state, selectedVersion: action.version, comparedVersion: state.selectedVersion };
+ }
return { ...state, selectedVersion: action.version };
case 'switchSides':
return { ...state, selectedVersion: state.comparedVersion, comparedVersion: state.selectedVersion };
diff --git a/mlflow/server/js/src/mcp-registry/hooks/useMCPServerVersionMutations.ts b/mlflow/server/js/src/mcp-registry/hooks/useMCPServerVersionMutations.ts
index d1974e8ec122c..c99a663e1daf0 100644
--- a/mlflow/server/js/src/mcp-registry/hooks/useMCPServerVersionMutations.ts
+++ b/mlflow/server/js/src/mcp-registry/hooks/useMCPServerVersionMutations.ts
@@ -1,6 +1,6 @@
import { useMutation, useQueryClient } from '@mlflow/mlflow/src/common/utils/reactQueryHooks';
import { MCPRegistryApi } from '../api';
-import type { MCPStatus } from '../types';
+import type { MCPStatus, MCPTool } from '../types';
import { MCP_QUERY_KEYS } from '../utils';
const useInvalidateServerQueries = () => {
@@ -9,32 +9,54 @@ const useInvalidateServerQueries = () => {
queryClient.invalidateQueries([MCP_QUERY_KEYS.SERVER, serverName]);
queryClient.invalidateQueries([MCP_QUERY_KEYS.SERVER_VERSIONS, serverName]);
queryClient.invalidateQueries([MCP_QUERY_KEYS.SERVER_BINDINGS, serverName]);
- queryClient.invalidateQueries(['mcp_server_latest_version', serverName]);
+ queryClient.invalidateQueries([MCP_QUERY_KEYS.SERVER_LATEST_VERSION, serverName]);
queryClient.invalidateQueries([MCP_QUERY_KEYS.SERVERS_LIST]);
queryClient.invalidateQueries([MCP_QUERY_KEYS.BINDINGS_LIST]);
};
};
-export const useUpdateMCPServerVersionStatus = (serverName: string) => {
+type UpdateMCPServerVersionPayload = {
+ version: string;
+ displayName?: string;
+ status?: MCPStatus;
+ tools?: MCPTool[] | null;
+ aliases?: { add: string[]; remove: string[] };
+};
+
+export const useUpdateMCPServerVersion = (serverName: string) => {
const invalidate = useInvalidateServerQueries();
- return useMutation({
- mutationFn: ({ version, status }) => MCPRegistryApi.updateMCPServerVersion(serverName, version, { status }),
- onSuccess: () => invalidate(serverName),
- });
-};
+ return useMutation({
+ mutationFn: async ({ version, displayName, status, tools, aliases }) => {
+ const versionUpdate: Record = {};
+ if (displayName !== undefined) {
+ versionUpdate['display_name'] = displayName || null;
+ }
+ if (status !== undefined) {
+ versionUpdate['status'] = status;
+ }
+ if (tools !== undefined) {
+ versionUpdate['tools'] = tools;
+ }
-export const useUpdateMCPServerVersionDisplayName = (serverName: string) => {
- const queryClient = useQueryClient();
+ const promises: Promise[] = [];
- return useMutation({
- mutationFn: ({ version, displayName }: { version: string; displayName: string }) =>
- MCPRegistryApi.updateMCPServerVersion(serverName, version, { display_name: displayName || null }),
- onSuccess: () => {
- queryClient.invalidateQueries(['mcp_server', serverName]);
- queryClient.invalidateQueries(['mcp_server_versions', serverName]);
- queryClient.invalidateQueries(['mcp_servers_list']);
+ if (Object.keys(versionUpdate).length > 0) {
+ promises.push(MCPRegistryApi.updateMCPServerVersion(serverName, version, versionUpdate));
+ }
+
+ if (aliases) {
+ promises.push(
+ ...aliases.add.map((alias) =>
+ MCPRegistryApi.setMCPServerAlias(serverName, { alias, version }),
+ ),
+ ...aliases.remove.map((alias) => MCPRegistryApi.deleteMCPServerAlias(serverName, alias)),
+ );
+ }
+
+ await Promise.all(promises);
},
+ onSuccess: () => invalidate(serverName),
});
};
@@ -47,17 +69,13 @@ export const useDeleteMCPServerVersion = (serverName: string) => {
});
};
-export const useSetLatestVersion = (serverName: string) => {
- const queryClient = useQueryClient();
+export const useUpdateMCPServerDisplayName = (serverName: string) => {
+ const invalidate = useInvalidateServerQueries();
return useMutation({
- mutationFn: (version: string | null) => MCPRegistryApi.updateMCPServer(serverName, { latest_version: version }),
- onSuccess: () => {
- queryClient.invalidateQueries(['mcp_server', serverName]);
- queryClient.invalidateQueries(['mcp_server_versions', serverName]);
- queryClient.invalidateQueries(['mcp_server_latest_version', serverName]);
- queryClient.invalidateQueries(['mcp_servers_list']);
- },
+ mutationFn: (displayName: string | null) =>
+ MCPRegistryApi.updateMCPServer(serverName, { display_name: displayName }),
+ onSuccess: () => invalidate(serverName),
});
};
diff --git a/mlflow/server/js/src/mcp-registry/hooks/useMCPServersListQuery.ts b/mlflow/server/js/src/mcp-registry/hooks/useMCPServersListQuery.ts
index 497b63fc39dac..02f48a762d6f4 100644
--- a/mlflow/server/js/src/mcp-registry/hooks/useMCPServersListQuery.ts
+++ b/mlflow/server/js/src/mcp-registry/hooks/useMCPServersListQuery.ts
@@ -12,7 +12,7 @@ export const useMCPServersListQuery = ({
storageKey: 'mcp_registry.page_size',
queryFn: ({ searchFilter: filter, pageToken, pageSize }) =>
MCPRegistryApi.searchMCPServers({
- filter_string: buildSearchFilterClause(filter, 'name'),
+ filter_string: buildSearchFilterClause(filter, 'display_name'),
page_token: pageToken,
max_results: pageSize,
}),
diff --git a/mlflow/server/js/src/mcp-registry/hooks/useSelectedMCPServerVersion.ts b/mlflow/server/js/src/mcp-registry/hooks/useSelectedMCPServerVersion.ts
deleted file mode 100644
index 80ca9c88509b3..0000000000000
--- a/mlflow/server/js/src/mcp-registry/hooks/useSelectedMCPServerVersion.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import { useCallback } from 'react';
-import { useSearchParams } from '@mlflow/mlflow/src/common/utils/RoutingUtils';
-
-const VERSION_QUERY_PARAM = 'version';
-
-export const useSelectedMCPServerVersion = (latestVersion?: string) => {
- const [searchParams, setSearchParams] = useSearchParams();
-
- const selectedVersion = searchParams.get(VERSION_QUERY_PARAM) ?? latestVersion;
-
- const setSelectedVersion = useCallback(
- (version: string | undefined) => {
- setSearchParams(
- (params) => {
- if (version === undefined) {
- params.delete(VERSION_QUERY_PARAM);
- return params;
- }
- params.set(VERSION_QUERY_PARAM, version);
- return params;
- },
- { replace: true },
- );
- },
- [setSearchParams],
- );
-
- return [selectedVersion, setSelectedVersion] as const;
-};
diff --git a/mlflow/server/js/src/mcp-registry/hooks/useUpdateMCPServerTags.ts b/mlflow/server/js/src/mcp-registry/hooks/useUpdateMCPServerTags.ts
new file mode 100644
index 0000000000000..85653483f2b78
--- /dev/null
+++ b/mlflow/server/js/src/mcp-registry/hooks/useUpdateMCPServerTags.ts
@@ -0,0 +1,57 @@
+import { useCallback } from 'react';
+import { useMutation, useQueryClient } from '@mlflow/mlflow/src/common/utils/reactQueryHooks';
+import { useEditKeyValueTagsModal } from '../../common/hooks/useEditKeyValueTagsModal';
+import { diffCurrentAndNewTags } from '../../common/utils/TagUtils';
+import { MCPRegistryApi } from '../api';
+import { MCP_QUERY_KEYS, tagsRecordToArray } from '../utils';
+import type { MCPServer } from '../types';
+
+type MCPServerTagEntity = { name: string; tags?: { key: string; value: string }[] };
+
+type UpdateTagsPayload = {
+ serverName: string;
+ toAdd: { key: string; value: string }[];
+ toDelete: { key: string }[];
+};
+
+export const useUpdateMCPServerTags = ({ onSuccess }: { onSuccess?: () => void } = {}) => {
+ const queryClient = useQueryClient();
+
+ const updateMutation = useMutation({
+ mutationFn: async ({ serverName, toAdd, toDelete }) =>
+ Promise.all([
+ ...toAdd.map(({ key, value }) => MCPRegistryApi.setMCPServerTag(serverName, { key, value })),
+ ...toDelete.map(({ key }) => MCPRegistryApi.deleteMCPServerTag(serverName, key)),
+ ]),
+ onSuccess: (_data, { serverName }) => {
+ queryClient.invalidateQueries([MCP_QUERY_KEYS.SERVERS_LIST]);
+ queryClient.invalidateQueries([MCP_QUERY_KEYS.SERVER, serverName]);
+ onSuccess?.();
+ },
+ });
+
+ const { EditTagsModal, showEditTagsModal } = useEditKeyValueTagsModal({
+ valueRequired: true,
+ saveTagsHandler: (entity, currentTags, newTags) => {
+ const { addedOrModifiedTags, deletedTags } = diffCurrentAndNewTags(currentTags, newTags);
+ return new Promise((resolve, reject) => {
+ updateMutation.mutate(
+ { serverName: entity.name, toAdd: addedOrModifiedTags, toDelete: deletedTags },
+ { onSuccess: () => resolve(), onError: reject },
+ );
+ });
+ },
+ });
+
+ const showEditServerTagsModal = useCallback(
+ (server: MCPServer) => {
+ showEditTagsModal({
+ name: server.name,
+ tags: tagsRecordToArray(server.tags),
+ });
+ },
+ [showEditTagsModal],
+ );
+
+ return { EditTagsModal, showEditServerTagsModal };
+};
diff --git a/mlflow/server/js/src/mcp-registry/hooks/useUpdateMCPServerVersionMetadataModal.tsx b/mlflow/server/js/src/mcp-registry/hooks/useUpdateMCPServerVersionMetadataModal.tsx
index c4f127b7b2d4d..932cfb695c405 100644
--- a/mlflow/server/js/src/mcp-registry/hooks/useUpdateMCPServerVersionMetadataModal.tsx
+++ b/mlflow/server/js/src/mcp-registry/hooks/useUpdateMCPServerVersionMetadataModal.tsx
@@ -2,9 +2,10 @@ import { useMutation, useQueryClient } from '@mlflow/mlflow/src/common/utils/rea
import { useEditKeyValueTagsModal } from '../../common/hooks/useEditKeyValueTagsModal';
import { MCPRegistryApi } from '../api';
import type { MCPServerVersion } from '../types';
+import { MCP_QUERY_KEYS, tagsRecordToArray } from '../utils';
import { useCallback } from 'react';
import { diffCurrentAndNewTags } from '../../common/utils/TagUtils';
-import { FormattedMessage } from 'react-intl';
+import { FormattedMessage, useIntl } from 'react-intl';
import type { KeyValueEntity } from '../../common/types';
type UpdateMCPServerVersionMetadataPayload = {
@@ -22,6 +23,7 @@ export const useUpdateMCPServerVersionMetadataModal = ({
onSuccess?: () => void;
}) => {
const queryClient = useQueryClient();
+ const intl = useIntl();
const updateMutation = useMutation({
mutationFn: async ({ serverName: name, version, toAdd, toDelete }) => {
@@ -39,10 +41,14 @@ export const useUpdateMCPServerVersionMetadataModal = ({
} = useEditKeyValueTagsModal<{ version: string; tags?: KeyValueEntity[] }>({
title: (
),
+ saveButtonLabel: intl.formatMessage({
+ defaultMessage: 'Save metadata',
+ description: 'Button label for saving metadata in the MCP server version metadata modal',
+ }),
valueRequired: true,
saveTagsHandler: (editedVersion, currentTags, newTags) => {
const { addedOrModifiedTags, deletedTags } = diffCurrentAndNewTags(currentTags, newTags);
@@ -57,7 +63,9 @@ export const useUpdateMCPServerVersionMetadataModal = ({
},
{
onSuccess: () => {
- queryClient.invalidateQueries(['mcp_server_versions', serverName]);
+ queryClient.invalidateQueries([MCP_QUERY_KEYS.SERVER_VERSIONS, serverName]);
+ queryClient.invalidateQueries([MCP_QUERY_KEYS.SERVER, serverName]);
+ queryClient.invalidateQueries([MCP_QUERY_KEYS.SERVER_LATEST_VERSION, serverName]);
resolve();
onSuccess?.();
},
@@ -72,7 +80,7 @@ export const useUpdateMCPServerVersionMetadataModal = ({
(version: MCPServerVersion) =>
showEditTagsModal({
version: version.version,
- tags: Object.entries(version.tags).map(([key, value]) => ({ key, value })),
+ tags: tagsRecordToArray(version.tags),
}),
[showEditTagsModal],
);
diff --git a/mlflow/server/js/src/mcp-registry/pages/MCPAccessBindingDetailPage.tsx b/mlflow/server/js/src/mcp-registry/pages/MCPAccessBindingDetailPage.tsx
index 61f2e997921ce..77feaadabed38 100644
--- a/mlflow/server/js/src/mcp-registry/pages/MCPAccessBindingDetailPage.tsx
+++ b/mlflow/server/js/src/mcp-registry/pages/MCPAccessBindingDetailPage.tsx
@@ -311,4 +311,4 @@ const MCPAccessBindingDetailPage = () => {
);
};
-export default withErrorBoundary(ErrorUtils.mlflowServices.EXPERIMENTS, MCPAccessBindingDetailPage);
+export default withErrorBoundary(ErrorUtils.mlflowServices.MCP_REGISTRY, MCPAccessBindingDetailPage);
diff --git a/mlflow/server/js/src/mcp-registry/pages/MCPRegistryPage.test.tsx b/mlflow/server/js/src/mcp-registry/pages/MCPRegistryPage.test.tsx
index f79d437a85e25..8d3ac37c3a99c 100644
--- a/mlflow/server/js/src/mcp-registry/pages/MCPRegistryPage.test.tsx
+++ b/mlflow/server/js/src/mcp-registry/pages/MCPRegistryPage.test.tsx
@@ -102,6 +102,7 @@ describe('MCPRegistryPage', () => {
});
it('converts plain text search into a valid name filter', async () => {
+ jest.setTimeout(15000);
let capturedFilterString: string | null = null;
server.use(
rest.get(getAjaxUrl('ajax-api/3.0/mlflow/mcp-servers'), (req, res, ctx) => {
@@ -119,9 +120,12 @@ describe('MCPRegistryPage', () => {
const searchInput = screen.getByPlaceholderText('Search MCP servers by name');
await userEvent.type(searchInput, 'raw');
- await waitFor(() => {
- expect(capturedFilterString).toBe("name ILIKE '%raw%'");
- });
+ await waitFor(
+ () => {
+ expect(capturedFilterString).toBe("name LIKE '%raw%'");
+ },
+ { timeout: 10000 },
+ );
});
it('shows Create MCP server button when servers exist', async () => {
diff --git a/mlflow/server/js/src/mcp-registry/pages/MCPRegistryPage.tsx b/mlflow/server/js/src/mcp-registry/pages/MCPRegistryPage.tsx
index 9316cc596ecdf..d5fd476f98ced 100644
--- a/mlflow/server/js/src/mcp-registry/pages/MCPRegistryPage.tsx
+++ b/mlflow/server/js/src/mcp-registry/pages/MCPRegistryPage.tsx
@@ -24,16 +24,13 @@ import { withErrorBoundary } from '../../common/utils/withErrorBoundary';
import ErrorUtils from '../../common/utils/ErrorUtils';
import { useNavigate, useSearchParams } from '../../common/utils/RoutingUtils';
import { ModelSearchInputHelpTooltip } from '../../model-registry/components/model-list/ModelListFilters';
-import { useMutation } from '../../common/utils/reactQueryHooks';
-import { useEditKeyValueTagsModal } from '../../common/hooks/useEditKeyValueTagsModal';
-import { diffCurrentAndNewTags } from '../../common/utils/TagUtils';
import { useMCPServersListQuery } from '../hooks/useMCPServersListQuery';
+import { useUpdateMCPServerTags } from '../hooks/useUpdateMCPServerTags';
import { useCreateMCPServerVersionModal } from '../hooks/useCreateMCPServerVersionModal';
import { useMCPAccessBindingsListQuery } from '../hooks/useMCPAccessBindingsListQuery';
import { MCPServerCardGrid } from '../components/MCPServerCardGrid';
import { MCPServerListTable } from '../components/MCPServerListTable';
import { emptyCenterStyles } from '../utils';
-import { MCPRegistryApi } from '../api';
import MCPRegistryRoutes from '../routes';
import type { MCPAccessBinding } from '../types';
import { MCPAccessBindingCardGrid } from '../components/MCPAccessBindingCardGrid';
@@ -91,49 +88,10 @@ const MCPRegistryPage = () => {
onSuccess: ({ name }) => navigate(MCPRegistryRoutes.getMCPServerDetailRoute(name)),
});
- type MCPServerTagEntity = { name: string; tags?: { key: string; value: string }[] };
-
- const updateTagsMutation = useMutation<
- unknown,
- Error,
- { serverName: string; toAdd: { key: string; value: string }[]; toDelete: { key: string }[] }
- >({
- mutationFn: async ({ serverName, toAdd, toDelete }) =>
- Promise.all([
- ...toAdd.map(({ key, value }) => MCPRegistryApi.setMCPServerTag(serverName, { key, value })),
- ...toDelete.map(({ key }) => MCPRegistryApi.deleteMCPServerTag(serverName, key)),
- ]),
- });
-
- const { EditTagsModal, showEditTagsModal } = useEditKeyValueTagsModal({
- valueRequired: true,
- saveTagsHandler: (entity, currentTags, newTags) => {
- const { addedOrModifiedTags, deletedTags } = diffCurrentAndNewTags(currentTags, newTags);
- return new Promise((resolve, reject) => {
- updateTagsMutation.mutate(
- { serverName: entity.name, toAdd: addedOrModifiedTags, toDelete: deletedTags },
- {
- onSuccess: () => {
- resolve();
- refetch();
- },
- onError: reject,
- },
- );
- });
- },
+ const { EditTagsModal, showEditServerTagsModal: handleEditTags } = useUpdateMCPServerTags({
+ onSuccess: refetch,
});
- const handleEditTags = useCallback(
- (server: MCPServer) => {
- showEditTagsModal({
- name: server.name,
- tags: Object.entries(server.tags).map(([key, value]) => ({ key, value })),
- });
- },
- [showEditTagsModal],
- );
-
const handleTabChange = useCallback(
(e: RadioChangeEvent) => {
const value = e.target.value as ActiveTab;
diff --git a/mlflow/server/js/src/mcp-registry/pages/MCPServerDetailPage.test.tsx b/mlflow/server/js/src/mcp-registry/pages/MCPServerDetailPage.test.tsx
index ac4b2a7b04d86..4f789e7b22481 100644
--- a/mlflow/server/js/src/mcp-registry/pages/MCPServerDetailPage.test.tsx
+++ b/mlflow/server/js/src/mcp-registry/pages/MCPServerDetailPage.test.tsx
@@ -1,5 +1,5 @@
import { describe, it, expect } from '@jest/globals';
-import { render, screen, waitFor, within } from '@testing-library/react';
+import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { IntlProvider } from 'react-intl';
import { DesignSystemProvider } from '@databricks/design-system';
@@ -22,8 +22,6 @@ import {
getMockedUpdateMCPServerErrorResponse,
getMockedSetMCPServerTagResponse,
getMockedDeleteMCPServerTagResponse,
- getMockedSetMCPServerAliasResponse,
- getMockedDeleteMCPServerAliasResponse,
} from '../test-utils';
const mockServer = createMockMCPServer({
@@ -100,7 +98,7 @@ describe('MCPServerDetailPage', () => {
expect(screen.getByText('Viewing version 1')).toBeInTheDocument();
});
expect(screen.getAllByText('dev.mainline/mcp').length).toBeGreaterThanOrEqual(1);
- expect(screen.getByText('A test server')).toBeInTheDocument();
+ expect(screen.getByText('Gives your AI agent your story map.')).toBeInTheDocument();
});
it('renders error state when server not found', async () => {
@@ -111,13 +109,13 @@ describe('MCPServerDetailPage', () => {
});
});
- it('expands JSON viewer', async () => {
+ it('expands configuration section', async () => {
renderPage();
await waitFor(() => {
expect(screen.getByText('Viewing version 1')).toBeInTheDocument();
});
- await userEvent.click(screen.getByText('View full configuration'));
+ await userEvent.click(screen.getByText('Configuration'));
await waitFor(() => {
expect(screen.getByText(/"name": "dev.mainline\/mcp"/)).toBeInTheDocument();
});
@@ -125,24 +123,29 @@ describe('MCPServerDetailPage', () => {
it('renders empty access bindings message', async () => {
renderPage();
+ await waitFor(() => {
+ expect(screen.getByText('Viewing version 1')).toBeInTheDocument();
+ });
+
+ await userEvent.click(screen.getByText('Access Bindings'));
await waitFor(() => {
expect(screen.getByText('No access bindings configured for this server.')).toBeInTheDocument();
});
});
- it('opens status update modal when edit status button is clicked', async () => {
+ it('opens edit version modal with status select when Edit is clicked', async () => {
renderPage();
await waitFor(() => {
expect(screen.getByText('Viewing version 1')).toBeInTheDocument();
});
- const editButton = document.querySelector(
- '[data-component-id="mlflow.mcp_registry.detail.edit_status"]',
+ const editBtn = document.querySelector(
+ '[data-component-id="mlflow.mcp_registry.detail.edit_version"]',
) as HTMLElement;
- await userEvent.click(editButton);
+ await userEvent.click(editBtn);
await waitFor(() => {
- expect(screen.getByText('Update version status')).toBeInTheDocument();
- expect(screen.getByText('Current status:')).toBeInTheDocument();
+ expect(screen.getByText('Edit version details')).toBeInTheDocument();
+ expect(screen.getByText('Status')).toBeInTheDocument();
});
});
@@ -208,6 +211,11 @@ describe('MCPServerDetailPage', () => {
});
server.use(getMockedSearchMCPAccessBindingsResponse([binding]));
renderPage();
+ await waitFor(() => {
+ expect(screen.getByText('Viewing version 1')).toBeInTheDocument();
+ });
+
+ await userEvent.click(screen.getByText('Access Bindings'));
await waitFor(() => {
expect(screen.getByText('https://mcp.example.com/server')).toBeInTheDocument();
});
@@ -265,7 +273,7 @@ describe('MCPServerDetailPage', () => {
});
});
- it('shows terminal state warning for deleted version status', async () => {
+ it('disables all status transitions for deleted version in edit modal', async () => {
const deletedVersion = createMockMCPServerVersion({
name: 'dev.mainline/mcp',
version: '1',
@@ -283,88 +291,14 @@ describe('MCPServerDetailPage', () => {
expect(screen.getByText('Viewing version 1')).toBeInTheDocument();
});
- const editButton = document.querySelector(
- '[data-component-id="mlflow.mcp_registry.detail.edit_status"]',
+ const editBtn = document.querySelector(
+ '[data-component-id="mlflow.mcp_registry.detail.edit_version"]',
) as HTMLElement;
- await userEvent.click(editButton);
+ await userEvent.click(editBtn);
await waitFor(() => {
- expect(screen.getByText(/terminal state/)).toBeInTheDocument();
- });
- });
-
- describe('set-as-latest flow', () => {
- it('shows "Pin as latest" for the resolved latest version when not pinned', async () => {
- server.use(getMockedUpdateMCPServerResponse());
- renderPage();
- await waitFor(() => {
- expect(screen.getByText('Viewing version 1')).toBeInTheDocument();
- });
-
- expect(screen.getByText('Pin as latest')).toBeInTheDocument();
- });
-
- it('shows "Unpin latest" when version is pinned as latest', async () => {
- const pinnedServer = createMockMCPServer({
- name: 'dev.mainline/mcp',
- display_name: 'Mainline',
- description: 'A test server',
- latest_version: '1',
- });
- server.use(getMockedGetMCPServerResponse(pinnedServer), getMockedUpdateMCPServerResponse());
- renderPage();
- await waitFor(() => {
- expect(screen.getByText('Viewing version 1')).toBeInTheDocument();
- });
-
- expect(screen.getByText('Unpin latest')).toBeInTheDocument();
- });
-
- it('shows "Set as latest" for a non-latest version', async () => {
- const version2 = createMockMCPServerVersion({
- name: 'dev.mainline/mcp',
- version: '2',
- status: 'active',
- server_json: { name: 'dev.mainline/mcp', version: '2.0.0' },
- });
- server.use(getMockedSearchMCPServerVersionsResponse([mockVersion, version2]), getMockedUpdateMCPServerResponse());
- renderPage();
- await waitFor(() => {
- expect(screen.getByText('Viewing version 1')).toBeInTheDocument();
- });
-
- await userEvent.click(screen.getByText('2'));
- await waitFor(() => {
- expect(screen.getByText('Viewing version 2')).toBeInTheDocument();
- });
- expect(screen.getByText('Set as latest')).toBeInTheDocument();
- });
-
- it('disables set-as-latest for draft versions that are not latest', async () => {
- const draftVersion = createMockMCPServerVersion({
- name: 'dev.mainline/mcp',
- version: '2',
- status: 'draft',
- server_json: { name: 'dev.mainline/mcp', version: '2.0.0' },
- });
- server.use(
- getMockedSearchMCPServerVersionsResponse([mockVersion, draftVersion]),
- getMockedUpdateMCPServerResponse(),
- );
- renderPage();
- await waitFor(() => {
- expect(screen.getByText('Viewing version 1')).toBeInTheDocument();
- });
-
- await userEvent.click(screen.getByText('2'));
- await waitFor(() => {
- expect(screen.getByText('Viewing version 2')).toBeInTheDocument();
- });
-
- const setLatestBtn = document.querySelector(
- '[data-component-id="mlflow.mcp_registry.detail.set_latest"]',
- ) as HTMLButtonElement;
- expect(setLatestBtn).toBeDisabled();
+ expect(screen.getByText('Edit version details')).toBeInTheDocument();
});
+ expect(screen.getByText('Status')).toBeInTheDocument();
});
describe('server description editing', () => {
@@ -372,7 +306,7 @@ describe('MCPServerDetailPage', () => {
server.use(getMockedUpdateMCPServerResponse());
renderPage();
await waitFor(() => {
- expect(screen.getByText('A test server')).toBeInTheDocument();
+ expect(screen.getByText('Gives your AI agent your story map.')).toBeInTheDocument();
});
const editBtn = document.querySelector(
@@ -386,7 +320,17 @@ describe('MCPServerDetailPage', () => {
name: 'dev.mainline/mcp',
display_name: 'Mainline',
});
- server.use(getMockedGetMCPServerResponse(noDescServer));
+ const noDescVersion = createMockMCPServerVersion({
+ name: 'dev.mainline/mcp',
+ version: '1',
+ status: 'active',
+ server_json: { name: 'dev.mainline/mcp', version: '1.0.0', title: 'Mainline' },
+ });
+ server.use(
+ getMockedGetMCPServerResponse(noDescServer),
+ getMockedSearchMCPServerVersionsResponse([noDescVersion]),
+ getMockedGetLatestMCPServerVersionResponse(noDescVersion),
+ );
renderPage();
await waitFor(() => {
expect(screen.getByText('Viewing version 1')).toBeInTheDocument();
@@ -399,7 +343,7 @@ describe('MCPServerDetailPage', () => {
server.use(getMockedUpdateMCPServerResponse());
renderPage();
await waitFor(() => {
- expect(screen.getByText('A test server')).toBeInTheDocument();
+ expect(screen.getByText('Gives your AI agent your story map.')).toBeInTheDocument();
});
const editBtn = document.querySelector(
@@ -407,7 +351,7 @@ describe('MCPServerDetailPage', () => {
) as HTMLElement;
await userEvent.click(editBtn);
await waitFor(() => {
- expect(screen.getByText('Edit description')).toBeInTheDocument();
+ expect(screen.getByText('Edit server description')).toBeInTheDocument();
});
});
@@ -415,7 +359,7 @@ describe('MCPServerDetailPage', () => {
server.use(getMockedUpdateMCPServerErrorResponse(500, 'Server error'));
renderPage();
await waitFor(() => {
- expect(screen.getByText('A test server')).toBeInTheDocument();
+ expect(screen.getByText('Gives your AI agent your story map.')).toBeInTheDocument();
});
const editBtn = document.querySelector(
@@ -423,7 +367,7 @@ describe('MCPServerDetailPage', () => {
) as HTMLElement;
await userEvent.click(editBtn);
await waitFor(() => {
- expect(screen.getByText('Edit description')).toBeInTheDocument();
+ expect(screen.getByText('Edit server description')).toBeInTheDocument();
});
await userEvent.click(screen.getByText('Save'));
@@ -483,21 +427,6 @@ describe('MCPServerDetailPage', () => {
});
});
- describe('reset latest version', () => {
- it('shows reset latest version in overflow menu', async () => {
- server.use(getMockedUpdateMCPServerResponse());
- renderPage();
- await waitFor(() => {
- expect(screen.getAllByText('Mainline').length).toBeGreaterThanOrEqual(1);
- });
-
- await userEvent.click(screen.getByRole('button', { name: 'More actions' }));
- const menuItems = await screen.findAllByRole('menuitem');
- const resetItem = menuItems.find((item) => item.textContent === 'Reset latest version');
- expect(resetItem).toBeDefined();
- });
- });
-
it('Compare toggle is disabled with a single version', async () => {
renderPage();
await waitFor(() => {
diff --git a/mlflow/server/js/src/mcp-registry/pages/MCPServerDetailPage.tsx b/mlflow/server/js/src/mcp-registry/pages/MCPServerDetailPage.tsx
index e2e24a1b2a10e..203b80ef4acef 100644
--- a/mlflow/server/js/src/mcp-registry/pages/MCPServerDetailPage.tsx
+++ b/mlflow/server/js/src/mcp-registry/pages/MCPServerDetailPage.tsx
@@ -18,7 +18,7 @@ import type { TagColors } from '@databricks/design-system';
import { FormattedMessage, useIntl } from 'react-intl';
import { ScrollablePageWrapper } from '../../common/components/ScrollablePageWrapper';
-import { Link, useNavigate, useParams } from '../../common/utils/RoutingUtils';
+import { Link, useNavigate, useParams, useSearchParams } from '../../common/utils/RoutingUtils';
import { withErrorBoundary } from '../../common/utils/withErrorBoundary';
import ErrorUtils from '../../common/utils/ErrorUtils';
import { ConfirmationModal } from '../../admin/ConfirmationModal';
@@ -31,7 +31,10 @@ import {
useLatestMCPServerVersionQuery,
useMCPAccessBindingsQuery,
} from '../hooks/useMCPServerDetailQuery';
-import { useDeleteMCPServer, useSetLatestVersion } from '../hooks/useMCPServerVersionMutations';
+import {
+ useDeleteMCPServer,
+ useUpdateMCPServerDisplayName,
+} from '../hooks/useMCPServerVersionMutations';
import { useDeleteAccessBindingMutation } from '../hooks/useAccessBindingMutation';
import type { MCPAccessBinding } from '../types';
import { useCreateMCPServerVersionModal } from '../hooks/useCreateMCPServerVersionModal';
@@ -58,18 +61,17 @@ const MCPServerDetailPage = () => {
const intl = useIntl();
const navigate = useNavigate();
const params = useParams<{ serverName: string }>();
+ const [searchParams] = useSearchParams();
+ const versionFromUrl = searchParams.get('version') ?? undefined;
const serverName = decodeURIComponent(params.serverName ?? '');
const [deleteServerModalVisible, setDeleteServerModalVisible] = useState(false);
const [addBindingModalOpen, setAddBindingModalOpen] = useState(false);
const [editingBinding, setEditingBinding] = useState(undefined);
const [deletingBinding, setDeletingBinding] = useState(undefined);
const [editServerDisplayNameVisible, setEditServerDisplayNameVisible] = useState(false);
- const [serverDisplayNameSaving, setServerDisplayNameSaving] = useState(false);
- const [serverDisplayNameError, setServerDisplayNameError] = useState(null);
const deleteServerMutation = useDeleteMCPServer();
+ const updateDisplayNameMutation = useUpdateMCPServerDisplayName(serverName);
const deleteBindingMutation = useDeleteAccessBindingMutation();
- const setLatestMutation = useSetLatestVersion(serverName);
-
const {
data: server,
isLoading: serverLoading,
@@ -102,18 +104,27 @@ const MCPServerDetailPage = () => {
}
const currentStillValid = versions.some((v) => v.version === selectedVersion);
if (!currentStillValid) {
- setSelectedVersion(versions[0].version);
+ const urlVersion =
+ versionFromUrl && versions.some((v) => v.version === versionFromUrl) ? versionFromUrl : undefined;
+ setSelectedVersion(urlVersion ?? versions[0].version);
}
- if (viewState.comparedVersion && !versions.some((v) => v.version === viewState.comparedVersion)) {
+ if (viewState.mode === MCPServerDetailViewMode.COMPARE && versions.length < 2) {
+ setPreviewMode();
+ } else if (viewState.comparedVersion && !versions.some((v) => v.version === viewState.comparedVersion)) {
setComparedVersion(
versions[0]?.version === selectedVersion ? (versions[1]?.version ?? '') : (versions[0]?.version ?? ''),
);
}
- }, [versions, selectedVersion, viewState.comparedVersion, setComparedVersion, setSelectedVersion]);
-
- useEffect(() => {
- setLatestMutation.reset();
- }, [selectedVersion]); // eslint-disable-line react-hooks/exhaustive-deps
+ }, [
+ versions,
+ selectedVersion,
+ versionFromUrl,
+ viewState.comparedVersion,
+ viewState.mode,
+ setComparedVersion,
+ setSelectedVersion,
+ setPreviewMode,
+ ]);
const currentVersion = versions?.find((v) => v.version === selectedVersion);
@@ -163,13 +174,6 @@ const MCPServerDetailPage = () => {
await Promise.all([refetchServer(), refetchVersions(), refetchLatestVersion()]);
}, [refetchServer, refetchVersions, refetchLatestVersion]);
- const handleSetLatest = useCallback(
- (version: string | null) => {
- setLatestMutation.mutate(version);
- },
- [setLatestMutation],
- );
-
const { CreateMCPServerVersionModal, openModal: openCreateVersionModal } = useCreateMCPServerVersionModal({
serverName: serverName,
latestVersion: currentVersion,
@@ -289,16 +293,6 @@ const MCPServerDetailPage = () => {
description="MCP server detail edit server display name action"
/>
- handleSetLatest(null)}
- >
-
-
setDeleteServerModalVisible(true)}
@@ -409,15 +403,7 @@ const MCPServerDetailPage = () => {
}}
onDeleteBinding={setDeletingBinding}
onEditMetadata={showEditMetadataModal}
- onSetLatest={handleSetLatest}
- setLatestLoading={setLatestMutation.isLoading}
- setLatestError={setLatestMutation.error as Error | null}
- onClearLatestError={() => setLatestMutation.reset()}
resolvedLatestVersion={resolvedLatestVersion}
- onUpdateDescription={async (description) => {
- await MCPRegistryApi.updateMCPServer(serverName, { description });
- await refetchAll();
- }}
/>
)}
@@ -432,27 +418,23 @@ const MCPServerDetailPage = () => {
editBinding={editingBinding}
lockedServer={serverName}
defaultVersion={currentVersion?.version}
+ filterToVersion={currentVersion?.version}
+ filterAliases={currentVersion ? aliasesByVersion[currentVersion.version] : undefined}
/>
{EditMCPServerVersionMetadataModal}
{CreateMCPServerVersionModal}
{
- setServerDisplayNameSaving(true);
- setServerDisplayNameError(null);
- MCPRegistryApi.updateMCPServer(serverName, { display_name: newDisplayName || null })
- .then(() => {
- setEditServerDisplayNameVisible(false);
- refetchAll();
- })
- .catch((e: Error) => setServerDisplayNameError(e))
- .finally(() => setServerDisplayNameSaving(false));
+ updateDisplayNameMutation.mutate(newDisplayName || null, {
+ onSuccess: () => setEditServerDisplayNameVisible(false),
+ });
}}
onCancel={() => {
- setServerDisplayNameError(null);
+ updateDisplayNameMutation.reset();
setEditServerDisplayNameVisible(false);
}}
/>
diff --git a/mlflow/server/js/src/mcp-registry/test-utils.ts b/mlflow/server/js/src/mcp-registry/test-utils.ts
index 34db04ddbc4ad..af1ec4a77e6d1 100644
--- a/mlflow/server/js/src/mcp-registry/test-utils.ts
+++ b/mlflow/server/js/src/mcp-registry/test-utils.ts
@@ -112,8 +112,4 @@ export const getMockedSetMCPServerTagResponse = () =>
export const getMockedDeleteMCPServerTagResponse = () =>
rest.delete(getAjaxUrl(`${BASE_URL}/:name/tags/:key`), (_req, res, ctx) => res(ctx.json({})));
-export const getMockedSetMCPServerAliasResponse = () =>
- rest.post(getAjaxUrl(`${BASE_URL}/:name/aliases`), (_req, res, ctx) => res(ctx.json({})));
-export const getMockedDeleteMCPServerAliasResponse = () =>
- rest.delete(getAjaxUrl(`${BASE_URL}/:name/aliases/:alias`), (_req, res, ctx) => res(ctx.json({})));
diff --git a/mlflow/server/js/src/mcp-registry/utils.test.ts b/mlflow/server/js/src/mcp-registry/utils.test.ts
index ebab69a4dd70a..ff7aec6dcdbe8 100644
--- a/mlflow/server/js/src/mcp-registry/utils.test.ts
+++ b/mlflow/server/js/src/mcp-registry/utils.test.ts
@@ -80,23 +80,24 @@ describe('buildSearchFilterClause', () => {
expect(buildSearchFilterClause('', 'name')).toBeUndefined();
});
- it('wraps plain text in ILIKE clause', () => {
- expect(buildSearchFilterClause('test', 'name')).toBe("name ILIKE '%test%'");
+ it('wraps plain text in LIKE clause', () => {
+ expect(buildSearchFilterClause('test', 'name')).toBe("name LIKE '%test%'");
});
it('uses the specified field name', () => {
- expect(buildSearchFilterClause('test', 'server_name')).toBe("server_name ILIKE '%test%'");
+ expect(buildSearchFilterClause('test', 'server_name')).toBe("server_name LIKE '%test%'");
});
it('escapes single quotes in the search term', () => {
- expect(buildSearchFilterClause("it's", 'name')).toBe("name ILIKE '%it''s%'");
+ expect(buildSearchFilterClause("it's", 'name')).toBe("name LIKE '%it''s%'");
});
it('passes through explicit SQL filter syntax', () => {
expect(buildSearchFilterClause("status = 'active'", 'name')).toBe("status = 'active'");
});
- it('passes through ILIKE expressions', () => {
+ it('passes through LIKE and ILIKE expressions', () => {
+ expect(buildSearchFilterClause("name LIKE '%foo%'", 'name')).toBe("name LIKE '%foo%'");
expect(buildSearchFilterClause("name ILIKE '%foo%'", 'name')).toBe("name ILIKE '%foo%'");
});
@@ -115,7 +116,8 @@ describe('formatTransportType', () => {
});
it('returns raw value for unknown types', () => {
- expect(formatTransportType('unknown' as any)).toBe('unknown');
+ // @ts-expect-error testing unknown transport type
+ expect(formatTransportType('unknown')).toBe('unknown');
});
});
diff --git a/mlflow/server/js/src/mcp-registry/utils.ts b/mlflow/server/js/src/mcp-registry/utils.ts
index 8593516047159..b86c2243434ad 100644
--- a/mlflow/server/js/src/mcp-registry/utils.ts
+++ b/mlflow/server/js/src/mcp-registry/utils.ts
@@ -1,5 +1,5 @@
import type { TagProps } from '@databricks/design-system';
-import type { MCPRemoteTransportType, MCPStatus, ServerJSONPayload } from './types';
+import type { MCPRemoteTransportType, MCPStatus, MCPTool, ServerJSONPayload } from './types';
export const STATUS_TAG_COLOR: Record = {
draft: 'charcoal',
@@ -39,6 +39,7 @@ export const MCP_QUERY_KEYS = {
SERVERS_LIST: 'mcp_servers_list',
SERVER: 'mcp_server',
SERVER_VERSIONS: 'mcp_server_versions',
+ SERVER_LATEST_VERSION: 'mcp_server_latest_version',
SERVER_BINDINGS: 'mcp_server_bindings',
BINDINGS_LIST: 'mcp_bindings_list',
BINDING_DETAIL: 'mcp_binding_detail',
@@ -51,6 +52,9 @@ export const resolveDisplayName = (server: { display_name?: string; name: string
return server.display_name || server.name;
};
+export const tagsRecordToArray = (tags: Record): { key: string; value: string }[] =>
+ Object.entries(tags).map(([key, value]) => ({ key, value }));
+
export const resolveVersionDisplayName = (
version: { display_name?: string; server_json?: { title?: string } } | null | undefined,
fallback: string,
@@ -78,7 +82,7 @@ export const buildSearchFilterClause = (searchFilter: string | undefined, field:
if (sqlKeywordPattern.test(searchFilter)) {
return searchFilter;
}
- return `${field} ILIKE '%${searchFilter.replace(/'/g, "''")}%'`;
+ return `${field} LIKE '%${searchFilter.replace(/'/g, "''")}%'`;
};
export const isValidEndpointUrl = (url: string): boolean => {
@@ -131,7 +135,7 @@ export const validateServerJson = (value: string): ServerJsonValidationResult =>
return { valid: true, parsed: obj as ServerJSONPayload };
};
-export const validateToolsJson = (value: string): { valid: boolean; error?: string; parsed?: unknown[] } => {
+export const validateToolsJson = (value: string): { valid: boolean; error?: string; parsed?: MCPTool[] } => {
const trimmed = value?.trim();
if (!trimmed) {
return { valid: true };
@@ -158,5 +162,5 @@ export const validateToolsJson = (value: string): { valid: boolean; error?: stri
}
}
- return { valid: true, parsed: parsed as unknown[] };
+ return { valid: true, parsed: parsed as MCPTool[] };
};
From 1750be9b3832b5d5fed6c3d8b95e076c27cf2b6b Mon Sep 17 00:00:00 2001
From: Nana Nosirova <10577112+nananosirova@users.noreply.github.com>
Date: Fri, 12 Jun 2026 18:48:03 -0400
Subject: [PATCH 5/5] Add structured server.json sections: tools, expandable
remotes, schema toggle
---
.../componentId-registry.js | 48 +-
mlflow/server/js/src/lang/default/en.json | 256 +++--
.../components/AccessBindingModal.test.tsx | 1 +
.../components/MCPServerAccessBindings.tsx | 5 +-
.../components/MCPServerVersionDetail.tsx | 173 +---
.../components/ServerJSONSection.tsx | 948 ++++++++++++++++++
.../UpdateDescriptionModal.test.tsx | 7 +-
.../components/UpdateDescriptionModal.tsx | 4 +-
.../useCreateMCPServerVersionMutation.ts | 4 +-
.../hooks/useMCPServerVersionMutations.ts | 4 +-
.../pages/MCPRegistryPage.test.tsx | 10 +-
.../pages/MCPServerDetailPage.test.tsx | 160 +--
.../pages/MCPServerDetailPage.tsx | 5 +-
.../server/js/src/mcp-registry/test-utils.ts | 2 -
mlflow/server/js/src/mcp-registry/types.ts | 1 +
15 files changed, 1328 insertions(+), 300 deletions(-)
create mode 100644 mlflow/server/js/src/mcp-registry/components/ServerJSONSection.tsx
diff --git a/.github/actions/check-component-ids/componentId-registry.js b/.github/actions/check-component-ids/componentId-registry.js
index 363c47b0cba7c..69eed2544a209 100644
--- a/.github/actions/check-component-ids/componentId-registry.js
+++ b/.github/actions/check-component-ids/componentId-registry.js
@@ -1888,7 +1888,6 @@ module.exports = {
"mlflow.mcp_registry.detail.actions": "",
"mlflow.mcp_registry.detail.actions.delete": "",
"mlflow.mcp_registry.detail.actions.edit_display_name": "",
- "mlflow.mcp_registry.detail.actions.reset_latest": "",
"mlflow.mcp_registry.detail.add_binding": "",
"mlflow.mcp_registry.detail.binding.card": "",
"mlflow.mcp_registry.detail.binding.delete": "",
@@ -1902,30 +1901,53 @@ module.exports = {
"mlflow.mcp_registry.detail.delete_version": "",
"mlflow.mcp_registry.detail.delete_version_modal": "",
"mlflow.mcp_registry.detail.display_name_input": "",
- "mlflow.mcp_registry.detail.edit_status": "",
+ "mlflow.mcp_registry.detail.edit_version": "",
"mlflow.mcp_registry.detail.error": "",
"mlflow.mcp_registry.detail.repository": "",
"mlflow.mcp_registry.detail.select_baseline.tooltip": "",
"mlflow.mcp_registry.detail.select_compared.tooltip": "",
- "mlflow.mcp_registry.detail.set_latest": "",
- "mlflow.mcp_registry.detail.set_latest.tooltip": "",
- "mlflow.mcp_registry.detail.set_latest_error": "",
- "mlflow.mcp_registry.detail.status_select": "",
"mlflow.mcp_registry.detail.tags.edit": "",
- "mlflow.mcp_registry.detail.toggle_json": "",
"mlflow.mcp_registry.detail.update_display_name_error": "",
"mlflow.mcp_registry.detail.update_display_name_modal": "",
- "mlflow.mcp_registry.detail.update_status_error": "",
- "mlflow.mcp_registry.detail.update_status_modal": "",
- "mlflow.mcp_registry.detail.update_status_terminal": "",
- "mlflow.mcp_registry.detail.version.add_description": "",
+ "mlflow.mcp_registry.detail.copy_package_identifier": "",
+ "mlflow.mcp_registry.detail.copy_package_identifier_button": "",
+ "mlflow.mcp_registry.detail.copy_remote_url": "",
+ "mlflow.mcp_registry.detail.copy_remote_url_button": "",
+ "mlflow.mcp_registry.detail.env_var_required": "",
+ "mlflow.mcp_registry.detail.env_var_secret": "",
+ "mlflow.mcp_registry.detail.header_required": "",
+ "mlflow.mcp_registry.detail.header_secret": "",
+ "mlflow.mcp_registry.detail.package_registry_tag": "",
+ "mlflow.mcp_registry.detail.raw_json.copy": "",
+ "mlflow.mcp_registry.detail.raw_json.copy_button": "",
+ "mlflow.mcp_registry.detail.raw_json.toggle": "",
+ "mlflow.mcp_registry.detail.raw_tools_json.copy": "",
+ "mlflow.mcp_registry.detail.raw_tools_json.copy_button": "",
+ "mlflow.mcp_registry.detail.raw_tools_json.toggle": "",
+ "mlflow.mcp_registry.detail.remote_transport_tag": "",
+ "mlflow.mcp_registry.detail.toggle_env_vars": "",
+ "mlflow.mcp_registry.detail.toggle_packages": "",
+ "mlflow.mcp_registry.detail.toggle_tools": "",
+ "mlflow.mcp_registry.detail.tool_annotation_tag": "",
+ "mlflow.mcp_registry.detail.tool_input_schema.copy": "",
+ "mlflow.mcp_registry.detail.tool_input_schema.copy_button": "",
+ "mlflow.mcp_registry.detail.tool_input_schema.toggle": "",
+ "mlflow.mcp_registry.detail.tool_name_tag": "",
+ "mlflow.mcp_registry.detail.tool_output_schema.copy": "",
+ "mlflow.mcp_registry.detail.tool_output_schema.copy_button": "",
+ "mlflow.mcp_registry.detail.tool_output_schema.toggle": "",
+ "mlflow.mcp_registry.detail.version_tabs": "",
"mlflow.mcp_registry.detail.version.add_metadata": "",
"mlflow.mcp_registry.detail.version.description.error": "",
"mlflow.mcp_registry.detail.version.description.modal": "",
"mlflow.mcp_registry.detail.version.description.textarea": "",
- "mlflow.mcp_registry.detail.version.edit_description": "",
- "mlflow.mcp_registry.detail.version.edit_display_name": "",
+ "mlflow.mcp_registry.detail.version.edit_display_name_input": "",
"mlflow.mcp_registry.detail.version.edit_metadata": "",
+ "mlflow.mcp_registry.detail.version.edit_status_select": "",
+ "mlflow.mcp_registry.detail.version.edit_tools_input": "",
+ "mlflow.mcp_registry.detail.version.edit_version_error": "",
+ "mlflow.mcp_registry.detail.version.edit_version_modal": "",
+ "mlflow.mcp_registry.detail.version.tools_validation_error": "",
"mlflow.mcp_registry.detail.version_status": "",
"mlflow.mcp_registry.detail.version_status_tag": "",
"mlflow.mcp_registry.detail.versions.header": "",
diff --git a/mlflow/server/js/src/lang/default/en.json b/mlflow/server/js/src/lang/default/en.json
index ab3b52f427009..5e32af62d7a3f 100644
--- a/mlflow/server/js/src/lang/default/en.json
+++ b/mlflow/server/js/src/lang/default/en.json
@@ -83,10 +83,6 @@
"defaultMessage": "Last updated",
"description": "Header for the dataset last-updated column"
},
- "+QPD70": {
- "defaultMessage": "Add/Edit Version Metadata",
- "description": "Title for a modal that allows the user to add or edit metadata tags on MCP server versions."
- },
"+T+iqa": {
"defaultMessage": "Select baseline run",
"description": "Placeholder text for the baseline run selector dropdown"
@@ -147,10 +143,18 @@
"defaultMessage": "Max",
"description": "Column title for the column displaying the maximum metric values for a metric"
},
+ "+itf/D": {
+ "defaultMessage": "Save",
+ "description": "Save button"
+ },
"+jMswj": {
"defaultMessage": "Target:",
"description": "Binding card target label"
},
+ "+jTk/P": {
+ "defaultMessage": "Show less",
+ "description": "MCP server package show less env vars button"
+ },
"+khKtf": {
"defaultMessage": "None",
"description": "A short label for experiments with no experiment kind"
@@ -219,6 +223,10 @@
"defaultMessage": "Does the app's response avoid harmful or toxic content?",
"description": "Hint for Safety template"
},
+ "/Adc0h": {
+ "defaultMessage": "Copy identifier",
+ "description": "Tooltip for copy package identifier button"
+ },
"/Aup8Q": {
"defaultMessage": "Request time",
"description": "Column label for request time"
@@ -319,10 +327,6 @@
"defaultMessage": "Service unavailable error",
"description": "Request failed due to service being available (HTTP STATUS 503) generic error message"
},
- "/YXV/m": {
- "defaultMessage": "Pin as latest",
- "description": "MCP server pin as latest version button"
- },
"/ZsbLo": {
"defaultMessage": "Add feedback",
"description": "Label for the button to add new feedback"
@@ -375,10 +379,6 @@
"defaultMessage": "{totalTokens} total tokens",
"description": "Experiment page > artifact compare view > results table > total number of evaluated tokens"
},
- "/pGvl1": {
- "defaultMessage": "Edit status",
- "description": "Aria label for edit status button"
- },
"/qIHh7": {
"defaultMessage": "Traces",
"description": "Label for the scorer evaluation scope selection"
@@ -619,10 +619,6 @@
"defaultMessage": "View Examples",
"description": "Experiment page > new run modal > prompt examples button"
},
- "147Q2W": {
- "defaultMessage": "Edit description",
- "description": "Aria label for edit description button"
- },
"16BvfJ": {
"defaultMessage": "{timeSince, plural, =1 {1 year} other {# years}} ago",
"description": "Text for time in years since given date for MLflow views"
@@ -763,6 +759,10 @@
"defaultMessage": "MCP Registry",
"description": "Sidebar link for MCP Registry page"
},
+ "1oaTIy": {
+ "defaultMessage": "secret",
+ "description": "Header secret badge"
+ },
"1oofcq": {
"defaultMessage": "Show less",
"description": "Link that collapses an expanded list when clicked"
@@ -887,14 +887,14 @@
"defaultMessage": "Search",
"description": "Search placeholder"
},
+ "2ScZy5": {
+ "defaultMessage": "Transport:",
+ "description": "MCP server package transport label"
+ },
"2Us2jl": {
"defaultMessage": "X axis",
"description": "Label for X axis in Contour chart configurator in compare runs chart config modal"
},
- "2VOGgj": {
- "defaultMessage": "Unpin latest",
- "description": "MCP server unpin latest version button"
- },
"2WOIGG": {
"defaultMessage": "Prompt created",
"description": "Webhook event label"
@@ -1255,6 +1255,10 @@
"defaultMessage": "Expectations",
"description": "Section title for expectations in the V2 dataset record side panel"
},
+ "4QTnoO": {
+ "defaultMessage": "View raw server.json",
+ "description": "MCP server version detail view raw JSON button"
+ },
"4Qft47": {
"defaultMessage": "{nodeCount, plural, =0 {} one {# node} other {# nodes}}",
"description": "Count of selected nodes displayed in the node level metric charts node selector"
@@ -1287,10 +1291,6 @@
"defaultMessage": "Close",
"description": "Close button for tag details modal"
},
- "4eVj/Q": {
- "defaultMessage": "Update version status",
- "description": "MCP server update version status modal title"
- },
"4g2H7S": {
"defaultMessage": "Prompt content is required",
"description": "A validation state for the chat prompt content in the prompt creation modal"
@@ -1363,10 +1363,18 @@
"defaultMessage": "Click to pin the run",
"description": "A tooltip for the pin icon button in the runs chart tooltip next to the not pinned run"
},
+ "59sogb": {
+ "defaultMessage": "Enter tools JSON array",
+ "description": "Placeholder for version tools input"
+ },
"5B5dhT": {
"defaultMessage": "Validate the model before deployment",
"description": "Heading text for validating the model before deploying it for serving"
},
+ "5F/eep": {
+ "defaultMessage": "Tools",
+ "description": "Version edit tools label"
+ },
"5GCYzy": {
"defaultMessage": "Experiment Runs - Databricks",
"description": "Title on a page used to manage MLflow experiments runs"
@@ -1959,6 +1967,10 @@
"defaultMessage": "Type a key",
"description": "Key-value tag editor modal > Tag dropdown > Tag input placeholder"
},
+ "8CXWXX": {
+ "defaultMessage": "Tools ({count})",
+ "description": "MCP server version detail tools subsection heading"
+ },
"8DoNdT": {
"defaultMessage": "Save",
"description": "Save button text for edit endpoint name modal"
@@ -2051,6 +2063,10 @@
"defaultMessage": "Enable Usage Tracking in the Overview tab to view logs",
"description": "Tooltip shown on disabled Logs tab explaining that usage tracking must be enabled first"
},
+ "8mdDKH": {
+ "defaultMessage": "Copy URL",
+ "description": "Tooltip for copy remote URL button"
+ },
"8oh0iW": {
"defaultMessage": "Clear filters",
"description": "Usage overview > clear metrics filters button"
@@ -2315,6 +2331,10 @@
"defaultMessage": "Reset period",
"description": "Budget reset period label"
},
+ "9yiunK": {
+ "defaultMessage": "Copy JSON",
+ "description": "Tooltip for copy raw server.json button"
+ },
"9zZeT2": {
"defaultMessage": "Delete records",
"description": "Title for the V2 dataset records bulk-delete confirmation modal"
@@ -2791,6 +2811,10 @@
"defaultMessage": "System Metrics",
"description": "Experiment tracking > runs charts > cards > RunsChartsDifferenceChartCard > system metrics heading"
},
+ "CHdcpF": {
+ "defaultMessage": "Show {count} more",
+ "description": "MCP server version detail show more packages button"
+ },
"CO81il": {
"defaultMessage": "No usage data available",
"description": "Empty state title"
@@ -2803,10 +2827,6 @@
"defaultMessage": "GenAI apps & agents",
"description": "A short label for custom experiments automatically identified as being focused on generative AI app and agent development"
},
- "CPq0qA": {
- "defaultMessage": "This version is in a terminal state and cannot be transitioned.",
- "description": "MCP server terminal status warning"
- },
"CRr6Tx": {
"defaultMessage": "Create and manage judges",
"description": "Title for the empty state of the judges page"
@@ -3119,6 +3139,10 @@
"defaultMessage": "{count} selected",
"description": "Label for item selector showing count of selected items"
},
+ "ED/0qk": {
+ "defaultMessage": "Packages ({count})",
+ "description": "MCP server version detail packages subsection heading"
+ },
"ED1+Xu": {
"defaultMessage": "Prompts & versions",
"description": "Label for the versions section in the MLflow experiment navbar"
@@ -3191,10 +3215,18 @@
"defaultMessage": "Metrics",
"description": "Label for the ungrouped metrics column group in the logged model column selector"
},
+ "Ehnbx0": {
+ "defaultMessage": "Environment Variables ({count})",
+ "description": "MCP server package environment variables heading with count"
+ },
"EiFPFP": {
"defaultMessage": "Custom Guardrail",
"description": "Custom guardrail type name"
},
+ "EiV1ml": {
+ "defaultMessage": "Edit version details",
+ "description": "MCP server version edit details modal title"
+ },
"EjPmMR": {
"defaultMessage": "Workspace Manager",
"description": "Roles table column header for the workspace-admin marker (multi-tenant)"
@@ -3263,10 +3295,6 @@
"defaultMessage": "Stop Sequences",
"description": "Experiment page > prompt lab > stop parameter label"
},
- "FAilrQ": {
- "defaultMessage": "Reset latest version",
- "description": "MCP server detail reset pinned latest version action"
- },
"FBR3Q/": {
"defaultMessage": "Overview",
"description": "Label for the overview tab on the logged model details page"
@@ -3491,6 +3519,10 @@
"defaultMessage": "Run evaluation",
"description": "Home page quick action title for running evaluations"
},
+ "GeC7uZ": {
+ "defaultMessage": "Show less",
+ "description": "MCP server version detail show less tools button"
+ },
"Gg/vLZ": {
"defaultMessage": "Delete Model",
"description": "Title text for delete model modal on model view page"
@@ -3551,6 +3583,10 @@
"defaultMessage": "null",
"description": "Null value in the evaluations table."
},
+ "H3W2Hz": {
+ "defaultMessage": "URL Variables",
+ "description": "MCP server remote URL variables heading"
+ },
"H7JwOl": {
"defaultMessage": "Delete version",
"description": "A label for a button to delete prompt version on the prompt details page"
@@ -4055,6 +4091,10 @@
"defaultMessage": "STRING",
"description": "Header label for string assessment type"
},
+ "Je8178": {
+ "defaultMessage": "Hide raw server.json",
+ "description": "MCP server version detail hide raw JSON button"
+ },
"JfFfzy": {
"defaultMessage": "Sample rate:",
"description": "Sample rate label for scorer"
@@ -4087,10 +4127,6 @@
"defaultMessage": "Delete",
"description": "Bulk-delete button on the users table (no rows selected)"
},
- "JqEZzt": {
- "defaultMessage": "New status:",
- "description": "MCP server new status label in update modal"
- },
"Jr4PZf": {
"defaultMessage": "server.json:",
"description": "Label for server.json field in create MCP server modal"
@@ -4139,6 +4175,10 @@
"defaultMessage": "Experiment renamed",
"description": "Partial save status message when experiment rename succeeds"
},
+ "K8AByX": {
+ "defaultMessage": "Output Schema",
+ "description": "MCP tool output schema toggle"
+ },
"K8JcAN": {
"defaultMessage": "Datasets",
"description": "Label for the datasets filter dropdown in the logged model list page"
@@ -4263,10 +4303,6 @@
"defaultMessage": "The dataset may have been deleted, or you may not have access to it.",
"description": "Body shown when the V2 evaluation dataset detail page fails to fetch its dataset"
},
- "KjmpxQ": {
- "defaultMessage": "View full configuration",
- "description": "MCP server detail toggle JSON viewer"
- },
"Kkr/RI": {
"defaultMessage": "Configure charts",
"description": "Experiment page > view controls > global settings for line chart view > dropdown button label"
@@ -4939,6 +4975,10 @@
"defaultMessage": "Expectations",
"description": "Column selector label for the expectations column"
},
+ "OHUKmu": {
+ "defaultMessage": "Add/Edit Metadata",
+ "description": "Title for a modal that allows the user to add or edit metadata tags on MCP server versions."
+ },
"OLZyxe": {
"defaultMessage": "{errorCount, plural, one {# error} other {# errors}}",
"description": "Error count in tooltip"
@@ -5199,14 +5239,14 @@
"defaultMessage": "GenAI apps & agents",
"description": "Label for experiments focused on generative AI model development"
},
+ "Pgl9HJ": {
+ "defaultMessage": "Hide raw JSON",
+ "description": "MCP server version detail hide raw tools JSON button"
+ },
"Pgxfhl": {
"defaultMessage": "Role",
"description": "Roles table column header for the role name"
},
- "PitYnw": {
- "defaultMessage": "Current status:",
- "description": "MCP server current status label in update modal"
- },
"PlP56F": {
"defaultMessage": "Enter value",
"description": "Usage overview > filter row > value input placeholder"
@@ -5319,6 +5359,10 @@
"defaultMessage": "Total",
"description": "Label for total token usage"
},
+ "QL5BPA": {
+ "defaultMessage": "{action} tool {name}",
+ "description": "Aria label for expanding/collapsing a tool row"
+ },
"QLRaAz": {
"defaultMessage": "Select all rows on this page",
"description": "Aria label for the select-all checkbox in the V2 dataset records table header"
@@ -5827,10 +5871,6 @@
"defaultMessage": "Line Smoothness",
"description": "Label for the smoothness slider for the graph plot for metrics"
},
- "T9MvMc": {
- "defaultMessage": "Set as latest",
- "description": "MCP server set as latest version button"
- },
"TBX+Gs": {
"defaultMessage": "Add/Edit tags",
"description": "Key-value tag editor modal > Title of the update tags modal"
@@ -5927,6 +5967,10 @@
"defaultMessage": "Inputs",
"description": "Table section name for schema inputs in the model comparison page"
},
+ "Tj7qTS": {
+ "defaultMessage": "Copy JSON",
+ "description": "Tooltip for copy JSON button"
+ },
"Tl63jz": {
"defaultMessage": "All runs are filtered",
"description": "Empty state title text for experiment runs page when all runs have been filtered out"
@@ -6379,6 +6423,10 @@
"defaultMessage": "Search chat sessions by input",
"description": "Placeholder text for the search input in the chat sessions table"
},
+ "W7JOu2": {
+ "defaultMessage": "required",
+ "description": "Header required badge"
+ },
"W8A4OF": {
"defaultMessage": "Configure OpenTelemetry export",
"description": "Step 1 title for OpenTelemetry traces onboarding"
@@ -6551,10 +6599,6 @@
"defaultMessage": "Input an artifact location (optional)",
"description": "Input placeholder to enter artifact location for create experiment"
},
- "X+11OS": {
- "defaultMessage": "Edit display name",
- "description": "Aria label for edit display name button"
- },
"X+boXI": {
"defaultMessage": "Copied",
"description": "Tooltip text shown when copy operation completes"
@@ -6647,6 +6691,10 @@
"defaultMessage": "Learn more",
"description": "Link text in column header tooltip that opens documentation in a new tab"
},
+ "XWtgGm": {
+ "defaultMessage": "Status",
+ "description": "Version edit status label"
+ },
"XX8+x1": {
"defaultMessage": "View prompt template",
"description": "Experiment page > artifact compare view > run column header prompt metadata > \"view prompt template\" button label"
@@ -6831,10 +6879,6 @@
"defaultMessage": "Start:",
"description": "Label for the start time of a span"
},
- "YY9VcI": {
- "defaultMessage": "Add description",
- "description": "MCP server version add description button"
- },
"YZDyOo": {
"defaultMessage": "Path:",
"description": "Label to display the full path of where the artifact of the experiment runs is located"
@@ -6911,10 +6955,6 @@
"defaultMessage": "Create",
"description": "Create budget policy button text"
},
- "Z+69vv": {
- "defaultMessage": "Update",
- "description": "MCP server update version status modal confirm button"
- },
"Z+tEhr": {
"defaultMessage": "Compare selected runs",
"description": "Tooltip for the compare button when enabled"
@@ -7443,6 +7483,10 @@
"defaultMessage": "Run ID:",
"description": "Text for run ID header in the main table in the model comparison page"
},
+ "bjH2oX": {
+ "defaultMessage": "Display name",
+ "description": "Aria label for display name input"
+ },
"bjoGjg": {
"defaultMessage": "Last hour",
"description": "Option for the start select dropdown to filter runs from the last hour"
@@ -7507,6 +7551,10 @@
"defaultMessage": "Type",
"description": "Run page > Overview > Experiment ID section label"
},
+ "by0DzI": {
+ "defaultMessage": "JSON content",
+ "description": "Aria label for JSON code block"
+ },
"byhyEj": {
"defaultMessage": "Re-Run judge",
"description": "Button text for re-running judge"
@@ -7655,6 +7703,10 @@
"defaultMessage": "Something went wrong",
"description": "Generic title used when a dataset action surfaces an unexpected error"
},
+ "cTwsmx": {
+ "defaultMessage": "Copy JSON",
+ "description": "Tooltip for copy raw tools JSON button"
+ },
"cXhWaG": {
"defaultMessage": "Select Model",
"description": "Header for the model selection step in issue detection modal"
@@ -7743,6 +7795,10 @@
"defaultMessage": "Last updated:",
"description": "Binding detail last updated label"
},
+ "d2ZU/7": {
+ "defaultMessage": "secret",
+ "description": "MCP server package env var secret badge"
+ },
"d2b9xa": {
"defaultMessage": "Enable Usage Tracking in the Overview tab to configure guardrails",
"description": "Tooltip shown on disabled Guardrails tab explaining that usage tracking must be enabled first"
@@ -7791,6 +7847,10 @@
"defaultMessage": "{count} {count, plural, one {hour} other {hours}}",
"description": "Formatted trace archival retention in hours"
},
+ "dDbtkI": {
+ "defaultMessage": "Tools ({count})",
+ "description": "MCP server version detail tools tab"
+ },
"dEGUt+": {
"defaultMessage": "Use this button to automatically detect quality issues in your traces using AI. This feature helps you identify problems like correctness, latency, and other quality concerns in your agent.",
"description": "Detect issues guidance popover message"
@@ -7887,6 +7947,10 @@
"defaultMessage": "You cannot select rows when comparing runs",
"description": "Tooltip message for the select button when comparing runs"
},
+ "dsPent": {
+ "defaultMessage": "Headers ({count})",
+ "description": "MCP server remote headers heading with count"
+ },
"dt3hj5": {
"defaultMessage": "Add tags",
"description": "Run page > Overview > Tags cell > 'Add' button label"
@@ -8835,6 +8899,10 @@
"defaultMessage": "My webhook",
"description": "Webhook name placeholder"
},
+ "iWh6qQ": {
+ "defaultMessage": "Version:",
+ "description": "MCP server package version label"
+ },
"iXb99e": {
"defaultMessage": "Box Plot",
"description": "Tab pane title for box plot on the compare runs page"
@@ -8883,6 +8951,10 @@
"defaultMessage": "Failed to duplicate some endpoints. Please try again.",
"description": "Gateway > Endpoints list > Duplicate error message"
},
+ "ik/j6P": {
+ "defaultMessage": "Show {count} more",
+ "description": "MCP server package show more env vars button"
+ },
"imKpxw": {
"defaultMessage": "ID",
"description": "Label for the ID section"
@@ -9051,6 +9123,10 @@
"defaultMessage": "All runs are hidden. Select at least one run to view charts.",
"description": "Experiment tracking > runs charts > indication displayed when no runs are selected for comparison"
},
+ "jZVaFN": {
+ "defaultMessage": "Input Schema",
+ "description": "MCP tool input schema toggle"
+ },
"jcJXyE": {
"defaultMessage": "Summarization",
"description": "LLM template option"
@@ -9091,10 +9167,18 @@
"defaultMessage": "Pending",
"description": "Label for pending state of a experiment logged model"
},
+ "joZkJS": {
+ "defaultMessage": "Remotes ({count})",
+ "description": "MCP server version detail remotes subsection heading"
+ },
"joh88n": {
"defaultMessage": "{numValue} for run \"{runName}\"",
"description": "Error/null assessment tooltip"
},
+ "jp25Wv": {
+ "defaultMessage": "Save metadata",
+ "description": "Button label for saving metadata in the MCP server version metadata modal"
+ },
"jpvRrY": {
"defaultMessage": "Pre-LLM Guardrails",
"description": "Pipeline BEFORE stage label"
@@ -9543,6 +9627,10 @@
"defaultMessage": "Last modified",
"description": "Last modified column header"
},
+ "mDb6cp": {
+ "defaultMessage": "{action} remote {url}",
+ "description": "Aria label for expanding/collapsing a remote row"
+ },
"mGUpJv": {
"defaultMessage": "Failed to create record",
"description": "Generic fallback save-error text for the dataset record side panel (create mode)"
@@ -9791,6 +9879,10 @@
"defaultMessage": "Pass",
"description": "The label for a passing asseessment above a bar-chart in the summary stats."
},
+ "nE+gTQ": {
+ "defaultMessage": "Show less",
+ "description": "MCP server version detail show less packages button"
+ },
"nEnDEu": {
"defaultMessage": "Cancel",
"description": "Cancel create evaluation dataset button text"
@@ -9803,6 +9895,10 @@
"defaultMessage": "Machine learning",
"description": "Label for custom experiments focused on machine learning"
},
+ "nGF/Tf": {
+ "defaultMessage": "Show {count} more",
+ "description": "MCP server version detail show more tools button"
+ },
"nIk08y": {
"defaultMessage": "Trace view",
"description": "Tooltip for traces preview mode toggle in evaluation runs table controls"
@@ -10011,6 +10107,10 @@
"defaultMessage": "Description",
"description": "Title text for the description section under details tab on the model\n view page"
},
+ "oHglm6": {
+ "defaultMessage": "Configuration",
+ "description": "MCP server version detail configuration tab"
+ },
"oKNOju": {
"defaultMessage": "Conversational tool call efficiency",
"description": "LLM template option"
@@ -10103,6 +10203,10 @@
"defaultMessage": "PII Detection",
"description": "PII guardrail type name"
},
+ "oj/VFA": {
+ "defaultMessage": "Display name",
+ "description": "Version edit display name label"
+ },
"ojLGIx": {
"defaultMessage": "Enter a feedback name",
"description": "Placeholder for the feedback name typeahead"
@@ -10963,6 +11067,10 @@
"defaultMessage": "Dataset metadata",
"description": "Title for the V2 evaluation dataset metadata modal"
},
+ "t+UhZI": {
+ "defaultMessage": "required",
+ "description": "MCP server package env var required badge"
+ },
"t+aG6y": {
"defaultMessage": "User",
"description": "Display text for the 'user' role in a GenAI chat message."
@@ -11427,10 +11535,6 @@
"defaultMessage": "Back to experiment list",
"description": "Tooltip for experiments button"
},
- "vr2aGt": {
- "defaultMessage": "Draft versions cannot be set as latest",
- "description": "Tooltip explaining why set as latest is disabled for draft versions"
- },
"vwD2zW": {
"defaultMessage": "Unified APIs",
"description": "Unified APIs tab title"
@@ -11479,6 +11583,10 @@
"defaultMessage": "Use a custom model name",
"description": "Label for custom model input section"
},
+ "w5RsIq": {
+ "defaultMessage": "{action} package {identifier}",
+ "description": "Aria label for expanding/collapsing a package row"
+ },
"w80phd": {
"defaultMessage": "Error",
"description": "Title for error fallback component in experiment evaluation runs UI"
@@ -11583,6 +11691,10 @@
"defaultMessage": "Here's how to optimize your prompt with your dataset in your Python code:",
"description": "Description of how to optimize a prompt with a dataset in Python"
},
+ "wdBU9n": {
+ "defaultMessage": "View raw JSON",
+ "description": "MCP server version detail view raw tools JSON button"
+ },
"wh+Eos": {
"defaultMessage": "Create prompt",
"description": "A header for the create prompt modal in the prompt management UI"
@@ -11731,6 +11843,10 @@
"defaultMessage": "Latency (AVG)",
"description": "Column header for average latency"
},
+ "xRb5P+": {
+ "defaultMessage": "Edit",
+ "description": "MCP server edit version button"
+ },
"xRw7H2": {
"defaultMessage": "Created by",
"description": "Column title text for creator username in model version table"
@@ -12015,6 +12131,10 @@
"defaultMessage": "Source run",
"description": "Label for the column indicating a run being the source of the logged model's metric (i.e. source run). Displayed in the logged model details metrics table."
},
+ "z4Cxm8": {
+ "defaultMessage": "Select version for comparison",
+ "description": "Aria label for the version comparison radio group"
+ },
"z4aclU": {
"defaultMessage": "Discard",
"description": "Confirm-button text for the discard-unsaved-changes prompt"
@@ -12179,6 +12299,10 @@
"defaultMessage": "Adherence",
"description": "Issue category title for adherence"
},
+ "znbl+r": {
+ "defaultMessage": "Access Bindings",
+ "description": "MCP server version detail access bindings tab"
+ },
"zp4MBw": {
"defaultMessage": "Token count",
"description": "Label for the token count section"
@@ -12207,6 +12331,10 @@
"defaultMessage": "An error occurred while attempting to delete traces. Please refresh the page and try again.",
"description": "Experiment page > traces view controls > Delete traces modal > Error message"
},
+ "zznpND": {
+ "defaultMessage": "Aliases",
+ "description": "Version edit aliases label"
+ },
"zzuzri": {
"defaultMessage": "Description",
"description": "Webhook description field label"
diff --git a/mlflow/server/js/src/mcp-registry/components/AccessBindingModal.test.tsx b/mlflow/server/js/src/mcp-registry/components/AccessBindingModal.test.tsx
index 4415dbe45b48a..b0e732f89240c 100644
--- a/mlflow/server/js/src/mcp-registry/components/AccessBindingModal.test.tsx
+++ b/mlflow/server/js/src/mcp-registry/components/AccessBindingModal.test.tsx
@@ -83,6 +83,7 @@ describe('AccessBindingModal', () => {
renderModal();
const input = screen.getByPlaceholderText('https://mcp.example.com/server');
await userEvent.type(input, 'not-a-url');
+ await userEvent.tab();
await waitFor(() => {
expect(screen.getByText('Enter a valid HTTP or HTTPS URL')).toBeInTheDocument();
});
diff --git a/mlflow/server/js/src/mcp-registry/components/MCPServerAccessBindings.tsx b/mlflow/server/js/src/mcp-registry/components/MCPServerAccessBindings.tsx
index 3cdecf2cfa894..09282555e4251 100644
--- a/mlflow/server/js/src/mcp-registry/components/MCPServerAccessBindings.tsx
+++ b/mlflow/server/js/src/mcp-registry/components/MCPServerAccessBindings.tsx
@@ -146,7 +146,10 @@ export const MCPServerAccessBindings = ({
onClick={onAddBinding}
size="small"
>
-
+
)}
diff --git a/mlflow/server/js/src/mcp-registry/components/MCPServerVersionDetail.tsx b/mlflow/server/js/src/mcp-registry/components/MCPServerVersionDetail.tsx
index aa5e13af7ccae..38e9dca8693d3 100644
--- a/mlflow/server/js/src/mcp-registry/components/MCPServerVersionDetail.tsx
+++ b/mlflow/server/js/src/mcp-registry/components/MCPServerVersionDetail.tsx
@@ -7,6 +7,7 @@ import {
Modal,
PencilIcon,
Spacer,
+ Tabs,
Tag,
SimpleSelect,
SimpleSelectOption,
@@ -20,14 +21,11 @@ import { FormattedMessage, useIntl } from 'react-intl';
import type { MCPAccessBinding, MCPServer, MCPServerVersion } from '../types';
import { STATUS_TAG_COLOR, STATUS_TRANSITIONS, resolveDisplayName } from '../utils';
import type { MCPStatus } from '../types';
-import { CollapsibleSection } from '../../common/components/CollapsibleSection';
import { MCPServerAccessBindings } from './MCPServerAccessBindings';
+import { ServerJSONSection, ToolsSection } from './ServerJSONSection';
import { ConfirmationModal } from '../../admin/ConfirmationModal';
import { ModelVersionTableAliasesCell } from '../../model-registry/components/aliases/ModelVersionTableAliasesCell';
-import {
- useUpdateMCPServerVersion,
- useDeleteMCPServerVersion,
-} from '../hooks/useMCPServerVersionMutations';
+import { useUpdateMCPServerVersion, useDeleteMCPServerVersion } from '../hooks/useMCPServerVersionMutations';
import { KeyValueTag } from '../../common/components/KeyValueTag';
import { AliasSelect } from '../../common/components/AliasSelect';
import { LATEST_ALIAS, RESERVED_ALIASES, validateToolsJson } from '../utils';
@@ -73,7 +71,6 @@ export const MCPServerVersionDetail = ({
const [editVersionToolsText, setEditVersionToolsText] = useState('');
const [deleteModalVisible, setDeleteModalVisible] = useState(false);
const [toolsValidationError, setToolsValidationError] = useState
(null);
-
const updateVersionMutation = useUpdateMCPServerVersion(server.name);
const deleteVersionMutation = useDeleteMCPServerVersion(server.name);
@@ -123,9 +120,7 @@ export const MCPServerVersionDetail = ({
)}
{version.server_json?.description && (
-
- {version.server_json.description}
-
+ {version.server_json.description}
)}
@@ -280,122 +275,47 @@ export const MCPServerVersionDetail = ({