Skip to content

Commit 19cf454

Browse files
Fix: Prevent infinite render loop when MCP servers have invalid data
- Add defensive checks in ToolsSection to filter out invalid MCP servers - Sanitize MCP server statuses in Redux config slice - Use server.id as React key (fallback to name) for stable rendering - Add null checks for server properties (prompts, resources, etc.) - Prevent render issues from corrupted/incomplete MCP data after deletion Fixes CON-5030 Co-authored-by: Karthik Chandra <[email protected]>
1 parent b91c7a6 commit 19cf454

File tree

2 files changed

+78
-17
lines changed

2 files changed

+78
-17
lines changed

gui/src/pages/config/sections/ToolsSection.tsx

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,8 @@ function MCPServerPreview({
160160
sectionKey: string;
161161
}) => {
162162
const isExpanded = expandedSections[sectionKey];
163-
const hasItems = items.length > 0;
163+
const safeItems = Array.isArray(items) ? items : [];
164+
const hasItems = safeItems.length > 0;
164165

165166
return (
166167
<div>
@@ -176,7 +177,7 @@ function MCPServerPreview({
176177
{icon}
177178
<span className="text-sm">{title}</span>
178179
<div className="flex h-5 w-5 items-center justify-center rounded-md bg-gray-600 px-0.5 text-xs font-medium text-white">
179-
{items.length}
180+
{safeItems.length}
180181
</div>
181182
</div>
182183
</div>
@@ -186,14 +187,14 @@ function MCPServerPreview({
186187
<div className="mx-2 my-2 mb-3">
187188
{hasItems ? (
188189
<div className="space-y-1">
189-
{items.map((item, idx) => {
190+
{safeItems.map((item, idx) => {
190191
return (
191192
<div
192193
key={idx}
193194
className="text-description rounded bg-gray-50 bg-opacity-5 px-2 py-1 text-xs"
194195
>
195-
<code>{item.name}</code>
196-
{item.description && (
196+
<code>{item?.name ?? "Unknown"}</code>
197+
{item?.description && (
197198
<div className="mt-1 text-xs text-gray-500">
198199
{item.description}
199200
</div>
@@ -337,7 +338,7 @@ function MCPServerPreview({
337338
allToolsOff={allToolsOff}
338339
duplicateDetection={duplicateDetection}
339340
/>
340-
{server.prompts.length > 0 && (
341+
{Array.isArray(server.prompts) && server.prompts.length > 0 && (
341342
<ResourceRow
342343
title="Prompts"
343344
items={server.prompts}
@@ -347,17 +348,21 @@ function MCPServerPreview({
347348
sectionKey={`${server.id}-prompts`}
348349
/>
349350
)}
350-
{(server.resources.length > 0 ||
351-
server.resourceTemplates.length > 0) && (
351+
{(Array.isArray(server.resources) && server.resources.length > 0) ||
352+
(Array.isArray(server.resourceTemplates) &&
353+
server.resourceTemplates.length > 0) ? (
352354
<ResourceRow
353355
title="Resources"
354-
items={[...server.resources, ...server.resourceTemplates]}
356+
items={[
357+
...(server.resources ?? []),
358+
...(server.resourceTemplates ?? []),
359+
]}
355360
icon={
356361
<CircleStackIcon className="text-description h-4 w-4 flex-shrink-0" />
357362
}
358363
sectionKey={`${server.id}-resources`}
359364
/>
360-
)}
365+
) : null}
361366
</div>
362367

363368
{/* Error display below expandable section */}
@@ -447,10 +452,26 @@ export function ToolsSection() {
447452
.map((server) => [server.name, server]) ?? [],
448453
);
449454

450-
return (servers ?? []).map((doc: MCPServerStatus) => ({
451-
block: doc,
452-
blockFromYaml: yamlServersByName.get(doc.name),
453-
}));
455+
// Filter out any invalid servers and ensure they have required properties
456+
return (servers ?? [])
457+
.filter((doc: MCPServerStatus) => {
458+
// Ensure server has required properties to prevent render issues
459+
return (
460+
doc &&
461+
typeof doc.id === "string" &&
462+
typeof doc.name === "string" &&
463+
doc.name.length > 0 &&
464+
Array.isArray(doc.prompts) &&
465+
Array.isArray(doc.resources) &&
466+
Array.isArray(doc.resourceTemplates) &&
467+
Array.isArray(doc.errors) &&
468+
Array.isArray(doc.infos)
469+
);
470+
})
471+
.map((doc: MCPServerStatus) => ({
472+
block: doc,
473+
blockFromYaml: yamlServersByName.get(doc.name),
474+
}));
454475
}, [servers, selectedProfile]);
455476

456477
const handleAddMcpServer = () => {
@@ -524,7 +545,7 @@ export function ToolsSection() {
524545
{mergedBlocks.length > 0 ? (
525546
mergedBlocks.map(({ block, blockFromYaml }) => (
526547
<MCPServerPreview
527-
key={block.name}
548+
key={block.id || block.name}
528549
server={block}
529550
serverFromYaml={blockFromYaml}
530551
allToolsOff={allToolsOff}

gui/src/redux/slices/configSlice.ts

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,36 @@ export const EMPTY_CONFIG: BrowserSerializedContinueConfig = {
3636
rules: [],
3737
};
3838

39+
/**
40+
* Sanitizes MCP server statuses to ensure they have valid properties
41+
* and prevent rendering issues from corrupted/incomplete data.
42+
*/
43+
function sanitizeMcpServerStatuses(
44+
statuses: any[],
45+
): BrowserSerializedContinueConfig["mcpServerStatuses"] {
46+
if (!Array.isArray(statuses)) {
47+
return [];
48+
}
49+
50+
return statuses.filter((server) => {
51+
// Filter out invalid servers that could cause render loops
52+
return (
53+
server &&
54+
typeof server === "object" &&
55+
typeof server.id === "string" &&
56+
server.id.length > 0 &&
57+
typeof server.name === "string" &&
58+
server.name.length > 0 &&
59+
// Ensure arrays exist even if empty
60+
Array.isArray(server.prompts) &&
61+
Array.isArray(server.resources) &&
62+
Array.isArray(server.resourceTemplates) &&
63+
Array.isArray(server.errors) &&
64+
Array.isArray(server.infos)
65+
);
66+
});
67+
}
68+
3969
export const INITIAL_CONFIG_SLICE: ConfigState = {
4070
configError: undefined,
4171
config: EMPTY_CONFIG,
@@ -66,15 +96,25 @@ export const configSlice = createSlice({
6696
if (!config) {
6797
state.config = EMPTY_CONFIG;
6898
} else {
69-
state.config = config;
99+
// Sanitize MCP server statuses to prevent render issues
100+
state.config = {
101+
...config,
102+
mcpServerStatuses: sanitizeMcpServerStatuses(
103+
config.mcpServerStatuses,
104+
),
105+
};
70106
}
71107
state.loading = false;
72108
},
73109
updateConfig: (
74110
state,
75111
{ payload: config }: PayloadAction<BrowserSerializedContinueConfig>,
76112
) => {
77-
state.config = config;
113+
// Sanitize MCP server statuses when updating config
114+
state.config = {
115+
...config,
116+
mcpServerStatuses: sanitizeMcpServerStatuses(config.mcpServerStatuses),
117+
};
78118
},
79119
setConfigLoading: (state, { payload: loading }: PayloadAction<boolean>) => {
80120
state.loading = loading;

0 commit comments

Comments
 (0)