-
Notifications
You must be signed in to change notification settings - Fork 2.4k
fix: prevent infinite loop of tool-input notifications in MCP Apps #6374
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,6 +1,6 @@ | ||||||||||||||||||||||||||
| import { ToolIconWithStatus, ToolCallStatus } from './ToolCallStatusIndicator'; | ||||||||||||||||||||||||||
| 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 { ToolCallArguments, ToolCallArgumentValue } from './ToolCallArguments'; | ||||||||||||||||||||||||||
| 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'; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| function maybeRenderMCPApp( | ||||||||||||||||||||||||||
| toolRequest: ToolRequestMessageContent, | ||||||||||||||||||||||||||
| toolResponse: ToolResponseMessageContent | undefined, | ||||||||||||||||||||||||||
| sessionId: string, | ||||||||||||||||||||||||||
| append?: (value: string) => void | ||||||||||||||||||||||||||
| ): React.ReactNode { | ||||||||||||||||||||||||||
| interface McpAppWrapperProps { | ||||||||||||||||||||||||||
| toolRequest: ToolRequestMessageContent; | ||||||||||||||||||||||||||
| toolResponse?: ToolResponseMessageContent; | ||||||||||||||||||||||||||
| sessionId: string; | ||||||||||||||||||||||||||
| append?: (value: string) => void; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| function McpAppWrapper({ | ||||||||||||||||||||||||||
| toolRequest, | ||||||||||||||||||||||||||
| toolResponse, | ||||||||||||||||||||||||||
| sessionId, | ||||||||||||||||||||||||||
| append, | ||||||||||||||||||||||||||
| }: McpAppWrapperProps): React.ReactNode { | ||||||||||||||||||||||||||
| const requestWithMeta = toolRequest as ToolRequestWithMeta; | ||||||||||||||||||||||||||
| let resourceUri = requestWithMeta._meta?.['ui/resourceUri']; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
|
@@ -87,24 +94,37 @@ function maybeRenderMCPApp( | |||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if (!resourceUri) return null; | ||||||||||||||||||||||||||
| if (requestWithMeta.toolCall.status !== 'success') return null; | ||||||||||||||||||||||||||
| const extensionName = | ||||||||||||||||||||||||||
| requestWithMeta.toolCall.status === 'success' | ||||||||||||||||||||||||||
| ? requestWithMeta.toolCall.value.name.split('__')[0] | ||||||||||||||||||||||||||
| : ''; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const toolArguments = | ||||||||||||||||||||||||||
| requestWithMeta.toolCall.status === 'success' | ||||||||||||||||||||||||||
| ? requestWithMeta.toolCall.value.arguments | ||||||||||||||||||||||||||
| : undefined; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const extensionName = requestWithMeta.toolCall.value.name.split('__')[0]; | ||||||||||||||||||||||||||
| // Memoize toolInput to prevent unnecessary re-renders | ||||||||||||||||||||||||||
| const toolInput = useMemo(() => ({ arguments: toolArguments || {} }), [toolArguments]); | ||||||||||||||||||||||||||
|
Comment on lines
+107
to
+108
|
||||||||||||||||||||||||||
| // Memoize toolInput to prevent unnecessary re-renders | |
| const toolInput = useMemo(() => ({ arguments: toolArguments || {} }), [toolArguments]); | |
| const toolArgumentsString = useMemo( | |
| () => (toolArguments ? JSON.stringify(toolArguments) : ''), | |
| [toolArguments], | |
| ); | |
| // Memoize toolInput to prevent unnecessary re-renders | |
| const toolInput = useMemo( | |
| () => ({ arguments: toolArguments || {} }), | |
| [toolArgumentsString], | |
| ); |
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The useMemo dependency array for toolResult uses toolResponse, but toolResponse is an object that may have the same content with a different reference on each render. This doesn't prevent re-renders. Consider using JSON.stringify(toolResponse) in the dependency array or extracting the actual result value and comparing that.
| }, [toolResponse]); | |
| }, [JSON.stringify(toolResponse?.toolResult ?? null)]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding toolInput and toolResult to the handleJsonRpcMessage dependency array causes the callback to be recreated on every change, which triggers the message listener to be re-registered (line 200). This is inefficient and could cause the callback to access stale values. Since these values are only used in the 'ui/notifications/initialized' case and are accessed from the closure, consider using refs for toolInput and toolResult instead, or remove them from the dependency array and accept that they'll be captured from the closure at creation time.