fix: prevent infinite loop of tool-input notifications in MCP Apps (#6374)

This commit is contained in:
Andrew Harvard
2026-01-07 12:02:14 -05:00
committed by GitHub
parent 76bf1d049f
commit dbfc9c8958
2 changed files with 68 additions and 17 deletions
@@ -86,6 +86,21 @@ export function useSandboxBridge(options: SandboxBridgeOptions): SandboxBridgeRe
case 'ui/notifications/initialized': case 'ui/notifications/initialized':
isGuestInitializedRef.current = true; isGuestInitializedRef.current = true;
// Send any pending tool data that arrived before initialization
if (toolInput) {
sendToSandbox({
jsonrpc: '2.0',
method: 'ui/notifications/tool-input',
params: { arguments: toolInput.arguments },
});
}
if (toolResult) {
sendToSandbox({
jsonrpc: '2.0',
method: 'ui/notifications/tool-result',
params: toolResult,
});
}
break; break;
case 'ui/notifications/size-changed': { case 'ui/notifications/size-changed': {
@@ -163,7 +178,16 @@ export function useSandboxBridge(options: SandboxBridgeOptions): SandboxBridgeRe
} }
} }
}, },
[resourceHtml, resourceCsp, resolvedTheme, sendToSandbox, onMcpRequest, onSizeChanged] [
resourceHtml,
resourceCsp,
resolvedTheme,
sendToSandbox,
onMcpRequest,
onSizeChanged,
toolInput,
toolResult,
]
); );
useEffect(() => { useEffect(() => {
@@ -1,6 +1,6 @@
import { ToolIconWithStatus, ToolCallStatus } from './ToolCallStatusIndicator'; import { ToolIconWithStatus, ToolCallStatus } from './ToolCallStatusIndicator';
import { getToolCallIcon } from '../utils/toolIconMapping'; import { getToolCallIcon } from '../utils/toolIconMapping';
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState, useMemo } from 'react';
import { Button } from './ui/button'; import { Button } from './ui/button';
import { ToolCallArguments, ToolCallArgumentValue } from './ToolCallArguments'; import { ToolCallArguments, ToolCallArgumentValue } from './ToolCallArguments';
import MarkdownContent from './MarkdownContent'; import MarkdownContent from './MarkdownContent';
@@ -71,12 +71,19 @@ function isEmbeddedResource(content: Content): content is EmbeddedResource {
return 'resource' in content && typeof (content as Record<string, unknown>).resource === 'object'; return 'resource' in content && typeof (content as Record<string, unknown>).resource === 'object';
} }
function maybeRenderMCPApp( interface McpAppWrapperProps {
toolRequest: ToolRequestMessageContent, toolRequest: ToolRequestMessageContent;
toolResponse: ToolResponseMessageContent | undefined, toolResponse?: ToolResponseMessageContent;
sessionId: string, sessionId: string;
append?: (value: string) => void append?: (value: string) => void;
): React.ReactNode { }
function McpAppWrapper({
toolRequest,
toolResponse,
sessionId,
append,
}: McpAppWrapperProps): React.ReactNode {
const requestWithMeta = toolRequest as ToolRequestWithMeta; const requestWithMeta = toolRequest as ToolRequestWithMeta;
let resourceUri = requestWithMeta._meta?.['ui/resourceUri']; let resourceUri = requestWithMeta._meta?.['ui/resourceUri'];
@@ -87,24 +94,37 @@ function maybeRenderMCPApp(
} }
} }
if (!resourceUri) return null; const extensionName =
if (requestWithMeta.toolCall.status !== 'success') return null; requestWithMeta.toolCall.status === 'success'
? requestWithMeta.toolCall.value.name.split('__')[0]
: '';
const extensionName = requestWithMeta.toolCall.value.name.split('__')[0]; const toolArguments =
requestWithMeta.toolCall.status === 'success'
? requestWithMeta.toolCall.value.arguments
: undefined;
let toolResult: CallToolResponse | undefined; // Memoize toolInput to prevent unnecessary re-renders
if (toolResponse) { const toolInput = useMemo(() => ({ arguments: toolArguments || {} }), [toolArguments]);
// Memoize toolResult to prevent unnecessary re-renders
const toolResult = useMemo(() => {
if (!toolResponse) return undefined;
const resultWithMeta = toolResponse.toolResult as ToolResultWithMeta; const resultWithMeta = toolResponse.toolResult as ToolResultWithMeta;
if (resultWithMeta?.status === 'success' && resultWithMeta.value) { if (resultWithMeta?.status === 'success' && resultWithMeta.value) {
toolResult = resultWithMeta.value; return resultWithMeta.value;
} }
} return undefined;
}, [toolResponse]);
if (!resourceUri) return null;
if (requestWithMeta.toolCall.status !== 'success') return null;
return ( return (
<div className="mt-3"> <div className="mt-3">
<McpAppRenderer <McpAppRenderer
resourceUri={resourceUri} resourceUri={resourceUri}
toolInput={{ arguments: requestWithMeta.toolCall.value.arguments || {} }} toolInput={toolInput}
toolResult={toolResult} toolResult={toolResult}
extensionName={extensionName} extensionName={extensionName}
sessionId={sessionId} sessionId={sessionId}
@@ -181,7 +201,14 @@ export default function ToolCallWithResponse({
} }
})} })}
{sessionId && maybeRenderMCPApp(toolRequest, toolResponse, sessionId, append)} {sessionId && (
<McpAppWrapper
toolRequest={toolRequest}
toolResponse={toolResponse}
sessionId={sessionId}
append={append}
/>
)}
</> </>
); );
} }