Skip to content

Commit 55f014c

Browse files
authored
Add endpoint field for remote mcp servers (#2555)
* Add endpoint field for remote mcp servers Signed-off-by: ppadti <ppadti@redhat.com> * Fix lint issue Signed-off-by: ppadti <ppadti@redhat.com> * Add remote label beside mcp server name in details page Signed-off-by: ppadti <ppadti@redhat.com> * Fix the extra whitespace in K8sNameDescriptionField Signed-off-by: ppadti <ppadti@redhat.com> --------- Signed-off-by: ppadti <ppadti@redhat.com>
1 parent cd8ceb3 commit 55f014c

9 files changed

Lines changed: 172 additions & 32 deletions

File tree

clients/ui/bff/internal/mocks/static_data_mock.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2545,6 +2545,9 @@ func GetMcpServerMocks() []models.McpServer {
25452545
SecureEndpoint: &trueVal,
25462546
ReadOnlyTools: &trueVal,
25472547
},
2548+
Endpoints: &models.McpEndpoints{
2549+
HTTP: stringToPointer("https://api.mcpservers.org/grafana-mcp/v1"),
2550+
},
25482551
}
25492552

25502553
gitMcp := models.McpServer{

clients/ui/frontend/src/__tests__/cypress/cypress/pages/mcpCatalog.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,14 @@ class McpServerDetails {
122122
return cy.findByTestId('mcp-server-deployment-mode');
123123
}
124124

125+
findRemoteTitleLabel() {
126+
return cy.findByTestId('mcp-server-details-remote-label');
127+
}
128+
129+
findEndpointCopy() {
130+
return cy.findByTestId('mcp-server-endpoint-copy');
131+
}
132+
125133
findTransportType() {
126134
return cy.findByTestId('mcp-server-transport-type');
127135
}

clients/ui/frontend/src/__tests__/cypress/cypress/tests/mocked/mcpCatalog/mcpServerDetails.cy.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,23 @@ describe('MCP Server Details Page', () => {
6868

6969
mcpServerDetails.findDescription().should('contain.text', kubernetesServer.description);
7070
});
71+
72+
it('should not show Remote title label for local deployment', () => {
73+
mcpServerDetails.visit(kubernetesServer.id);
74+
mcpServerDetails.findRemoteTitleLabel().should('not.exist');
75+
});
76+
77+
it('should show Remote title label when deploymentMode is remote', () => {
78+
const remoteServer = mockMcpServer({
79+
id: 'remote-header-test',
80+
name: 'GitHub',
81+
deploymentMode: 'remote',
82+
});
83+
initServerDetailIntercept(remoteServer);
84+
mcpServerDetails.visit(remoteServer.id);
85+
mcpServerDetails.findRemoteTitleLabel().should('be.visible');
86+
mcpServerDetails.findRemoteTitleLabel().should('contain.text', 'Remote');
87+
});
7188
});
7289

7390
describe('README card', () => {
@@ -93,6 +110,25 @@ describe('MCP Server Details Page', () => {
93110
initServerDetailIntercept(kubernetesServer);
94111
});
95112

113+
it('should not display endpoint when API omits endpoints', () => {
114+
mcpServerDetails.visit(kubernetesServer.id);
115+
cy.findByTestId('mcp-server-endpoint-copy').should('not.exist');
116+
});
117+
118+
it('should display endpoint with copy when endpoints are present', () => {
119+
const host = 'splunk-mcp-server.demo-namespace.svc.cluster.local:8080';
120+
const serverWithEndpoint = mockMcpServer({
121+
id: 'endpoint-test-server',
122+
name: 'Splunk MCP',
123+
deploymentMode: 'remote',
124+
endpoints: { http: host },
125+
});
126+
initServerDetailIntercept(serverWithEndpoint);
127+
mcpServerDetails.visit(serverWithEndpoint.id);
128+
mcpServerDetails.findEndpointCopy().should('be.visible');
129+
mcpServerDetails.findEndpointCopy().find('input').should('have.value', host);
130+
});
131+
96132
it('should display labels, license, version, and deployment mode', () => {
97133
mcpServerDetails.visit(kubernetesServer.id);
98134

clients/ui/frontend/src/app/pages/mcpCatalog/components/McpCatalogCard.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ import { TruncatedText } from 'mod-arch-shared';
1414
import { ApplicationsIcon } from '@patternfly/react-icons';
1515
import { Link, type LinkProps } from 'react-router-dom';
1616
import type { McpServer } from '~/app/mcpServerCatalogTypes';
17-
import { getSecurityIndicatorLabels } from '~/app/pages/mcpCatalog/utils/mcpCatalogUtils';
17+
import {
18+
getSecurityIndicatorLabels,
19+
isMcpRemoteDeploymentMode,
20+
} from '~/app/pages/mcpCatalog/utils/mcpCatalogUtils';
1821
import {
1922
McpCardIconType,
2023
McpCardIconByLabel,
@@ -36,9 +39,6 @@ const SecurityTag: React.FC<{ label: string }> = ({ label }) => (
3639
);
3740

3841
const McpCatalogCard: React.FC<McpCatalogCardProps> = React.memo(({ server }) => {
39-
const deploymentType =
40-
server.deploymentMode === 'local' ? McpCardIconType.LOCAL_TO_CLUSTER : McpCardIconType.REMOTE;
41-
const deploymentConfig = getMcpCardIconConfig(deploymentType);
4242
const securityLabels = getSecurityIndicatorLabels(server.securityIndicators);
4343
const serverId = server.id;
4444

@@ -59,10 +59,10 @@ const McpCatalogCard: React.FC<McpCatalogCardProps> = React.memo(({ server }) =>
5959
<ApplicationsIcon />
6060
</span>
6161
</FlexItem>
62-
{server.deploymentMode === 'remote' && (
62+
{isMcpRemoteDeploymentMode(server.deploymentMode) && (
6363
<FlexItem>
6464
<Label data-testid={`mcp-catalog-card-deployment-${serverId}`}>
65-
{deploymentConfig.label}
65+
{getMcpCardIconConfig(McpCardIconType.REMOTE).label}
6666
</Label>
6767
</FlexItem>
6868
)}

clients/ui/frontend/src/app/pages/mcpCatalog/screens/McpServerDetailsPage.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
EmptyStateFooter,
1313
Flex,
1414
FlexItem,
15+
Label,
1516
Stack,
1617
StackItem,
1718
} from '@patternfly/react-core';
@@ -21,6 +22,11 @@ import { useMcpServerWithAPI } from '~/app/hooks/mcpServerCatalog/useMcpServer';
2122
import { McpCatalogContext } from '~/app/context/mcpCatalog/McpCatalogContext';
2223
import { mcpCatalogUrl } from '~/app/routes/mcpCatalog/mcpCatalog';
2324
import ScrollViewOnMount from '~/app/shared/components/ScrollViewOnMount';
25+
import {
26+
McpCardIconType,
27+
getMcpCardIconConfig,
28+
} from '~/app/pages/mcpCatalog/components/McpCatalogCardIcons';
29+
import { isMcpRemoteDeploymentMode } from '~/app/pages/mcpCatalog/utils/mcpCatalogUtils';
2430
import McpServerDetailsView from './McpServerDetailsView';
2531

2632
const McpServerDetailsPage: React.FC = () => {
@@ -64,7 +70,20 @@ const McpServerDetailsPage: React.FC = () => {
6470
)}
6571
<Stack>
6672
<StackItem>
67-
<FlexItem>{server.name}</FlexItem>
73+
<Flex
74+
gap={{ default: 'gapSm' }}
75+
alignItems={{ default: 'alignItemsCenter' }}
76+
flexWrap={{ default: 'wrap' }}
77+
>
78+
<FlexItem>{server.name}</FlexItem>
79+
{isMcpRemoteDeploymentMode(server.deploymentMode) && (
80+
<FlexItem>
81+
<Label data-testid="mcp-server-details-remote-label">
82+
{getMcpCardIconConfig(McpCardIconType.REMOTE).label}
83+
</Label>
84+
</FlexItem>
85+
)}
86+
</Flex>
6887
</StackItem>
6988
{server.provider && (
7089
<StackItem>

clients/ui/frontend/src/app/pages/mcpCatalog/screens/McpServerDetailsView.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import ExternalLink from '~/app/shared/components/ExternalLink';
2626
import MarkdownComponent from '~/app/shared/markdown/MarkdownComponent';
2727
import ModelTimestamp from '~/app/pages/modelRegistry/screens/components/ModelTimestamp';
2828
import McpServerToolsSection from '~/app/pages/mcpCatalog/screens/McpServerToolsSection';
29+
import { getMcpServerPrimaryEndpoint } from '~/app/pages/mcpCatalog/utils/mcpCatalogUtils';
2930

3031
type McpServerDetailsViewProps = {
3132
server: McpServer;
@@ -63,6 +64,7 @@ const getTransportTypeLabel = (transports?: string[]): string => {
6364
const McpServerDetailsView: React.FC<McpServerDetailsViewProps> = ({ server }) => {
6465
const deploymentModeLabel = getDeploymentModeLabel(server.deploymentMode);
6566
const transportTypeLabel = getTransportTypeLabel(server.transports);
67+
const primaryEndpoint = getMcpServerPrimaryEndpoint(server.endpoints);
6668

6769
return (
6870
<PageSection hasBodyWrapper={false} isFilled padding={{ default: 'noPadding' }}>
@@ -125,6 +127,21 @@ const McpServerDetailsView: React.FC<McpServerDetailsViewProps> = ({ server }) =
125127
</CardHeader>
126128
<CardBody>
127129
<DescriptionList>
130+
{primaryEndpoint && (
131+
<DescriptionListGroup>
132+
<DescriptionListTerm>Endpoint</DescriptionListTerm>
133+
<DescriptionListDescription>
134+
<ClipboardCopy
135+
hoverTip="Copy"
136+
clickTip="Copied"
137+
isReadOnly
138+
data-testid="mcp-server-endpoint-copy"
139+
>
140+
{primaryEndpoint}
141+
</ClipboardCopy>
142+
</DescriptionListDescription>
143+
</DescriptionListGroup>
144+
)}
128145
{server.tags && server.tags.length > 0 && (
129146
<DescriptionListGroup>
130147
<DescriptionListTerm>Labels</DescriptionListTerm>

clients/ui/frontend/src/app/pages/mcpCatalog/utils/__tests__/mcpCatalogUtils.spec.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,43 @@
11
import {
2+
getMcpServerPrimaryEndpoint,
23
getSecurityIndicatorLabels,
34
hasMcpFiltersApplied,
5+
isMcpRemoteDeploymentMode,
46
} from '~/app/pages/mcpCatalog/utils/mcpCatalogUtils';
57
import type { McpCatalogFiltersState } from '~/app/pages/mcpCatalog/types/mcpCatalogFilterOptions';
68

9+
describe('isMcpRemoteDeploymentMode', () => {
10+
it('returns true when mode is remote', () => {
11+
expect(isMcpRemoteDeploymentMode('remote')).toBe(true);
12+
});
13+
14+
it('returns false when mode is local or undefined', () => {
15+
expect(isMcpRemoteDeploymentMode('local')).toBe(false);
16+
expect(isMcpRemoteDeploymentMode(undefined)).toBe(false);
17+
});
18+
});
19+
20+
describe('getMcpServerPrimaryEndpoint', () => {
21+
it('returns undefined when endpoints missing or null', () => {
22+
expect(getMcpServerPrimaryEndpoint(undefined)).toBeUndefined();
23+
expect(getMcpServerPrimaryEndpoint(null)).toBeUndefined();
24+
expect(getMcpServerPrimaryEndpoint({})).toBeUndefined();
25+
});
26+
27+
it('returns trimmed http when set', () => {
28+
expect(getMcpServerPrimaryEndpoint({ http: ' host:8080 ' })).toBe('host:8080');
29+
});
30+
31+
it('prefers http over sse', () => {
32+
expect(getMcpServerPrimaryEndpoint({ http: 'https://a', sse: 'https://b' })).toBe('https://a');
33+
});
34+
35+
it('falls back to sse when http empty', () => {
36+
expect(getMcpServerPrimaryEndpoint({ http: '', sse: 'https://sse' })).toBe('https://sse');
37+
expect(getMcpServerPrimaryEndpoint({ http: ' ', sse: 'https://sse' })).toBe('https://sse');
38+
});
39+
});
40+
741
describe('getSecurityIndicatorLabels', () => {
842
it('returns empty array when securityIndicators is undefined or null', () => {
943
expect(getSecurityIndicatorLabels(undefined)).toEqual([]);

clients/ui/frontend/src/app/pages/mcpCatalog/utils/mcpCatalogUtils.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,29 @@
11
import type { McpCatalogFiltersState } from '~/app/pages/mcpCatalog/types/mcpCatalogFilterOptions';
22
import { BACKEND_TO_FRONTEND_FILTER_KEY, MCP_FILTER_KEYS } from '~/app/pages/mcpCatalog/const';
3-
import type { McpSecurityIndicator } from '~/app/mcpServerCatalogTypes';
3+
import type {
4+
McpDeploymentMode,
5+
McpEndpoints,
6+
McpSecurityIndicator,
7+
} from '~/app/mcpServerCatalogTypes';
8+
9+
export const isMcpRemoteDeploymentMode = (mode?: McpDeploymentMode): boolean => mode === 'remote';
10+
11+
export const getMcpServerPrimaryEndpoint = (
12+
endpoints?: McpEndpoints | null,
13+
): string | undefined => {
14+
if (!endpoints) {
15+
return undefined;
16+
}
17+
const http = endpoints.http?.trim();
18+
if (http) {
19+
return http;
20+
}
21+
const sse = endpoints.sse?.trim();
22+
if (sse) {
23+
return sse;
24+
}
25+
return undefined;
26+
};
427

528
const SECURITY_INDICATOR_LABELS: Record<keyof McpSecurityIndicator, string> = {
629
verifiedSource: 'Verified source',

clients/ui/frontend/src/concepts/k8s/K8sNameDescriptionField/K8sNameDescriptionField.tsx

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -90,32 +90,32 @@ const K8sNameDescriptionField: React.FC<K8sNameDescriptionFieldProps> = ({
9090
<>
9191
<FormGroup label={nameLabel} isRequired fieldId={`${dataTestId}-name`}>
9292
<FormFieldset component={nameInput} field="Name" />
93-
</FormGroup>
94-
{nameHelperText || (!showK8sField && !k8sName.state.immutable) ? (
95-
<HelperText>
96-
{nameHelperText && <HelperTextItem>{nameHelperText}</HelperTextItem>}
97-
{!showK8sField && !k8sName.state.immutable && (
98-
<>
99-
{k8sName.value && (
93+
{nameHelperText || (!showK8sField && !k8sName.state.immutable) ? (
94+
<HelperText>
95+
{nameHelperText && <HelperTextItem>{nameHelperText}</HelperTextItem>}
96+
{!showK8sField && !k8sName.state.immutable && (
97+
<>
98+
{k8sName.value && (
99+
<HelperTextItem>
100+
The resource name will be <b>{k8sName.value}</b>.
101+
</HelperTextItem>
102+
)}
100103
<HelperTextItem>
101-
The resource name will be <b>{k8sName.value}</b>.
104+
<Button
105+
data-testid={`${dataTestId}-editResourceLink`}
106+
variant="link"
107+
isInline
108+
onClick={() => setShowK8sField(true)}
109+
>
110+
Edit resource name
111+
</Button>{' '}
112+
<ResourceNameDefinitionTooltip />
102113
</HelperTextItem>
103-
)}
104-
<HelperTextItem>
105-
<Button
106-
data-testid={`${dataTestId}-editResourceLink`}
107-
variant="link"
108-
isInline
109-
onClick={() => setShowK8sField(true)}
110-
>
111-
Edit resource name
112-
</Button>{' '}
113-
<ResourceNameDefinitionTooltip />
114-
</HelperTextItem>
115-
</>
116-
)}
117-
</HelperText>
118-
) : null}
114+
</>
115+
)}
116+
</HelperText>
117+
) : null}
118+
</FormGroup>
119119

120120
<ResourceNameField
121121
allowEdit={showK8sField}

0 commit comments

Comments
 (0)