From a9489878c66f9be02b50d5298a765e63e4780a8b Mon Sep 17 00:00:00 2001 From: Alexander S <70532114+autumnjava@users.noreply.github.com> Date: Wed, 31 Dec 2025 17:35:06 +0100 Subject: [PATCH 1/6] initial mcp progress functionality --- api/server/services/MCP.js | 18 +++++ .../Chat/Messages/Content/MCPProgress.tsx | 59 +++++++++++++++ .../components/Chat/Messages/Content/Part.tsx | 8 +- .../Chat/Messages/Content/ToolCall.tsx | 33 ++++++++- client/src/hooks/SSE/useStepHandler.ts | 73 +++++++++++++++++++ client/src/store/mcp.ts | 45 ++++++++++++ packages/api/src/mcp/parsers.ts | 5 +- packages/api/src/mcp/types/index.ts | 2 +- packages/data-provider/src/types/agents.ts | 24 ++++-- .../data-provider/src/types/assistants.ts | 1 + 10 files changed, 256 insertions(+), 12 deletions(-) create mode 100755 client/src/components/Chat/Messages/Content/MCPProgress.tsx diff --git a/api/server/services/MCP.js b/api/server/services/MCP.js index 81d7107de40c..1f7ba65829bc 100644 --- a/api/server/services/MCP.js +++ b/api/server/services/MCP.js @@ -1,5 +1,6 @@ const { z } = require('zod'); const { tool } = require('@langchain/core/tools'); +const { ToolMessage } = require('@langchain/core/messages'); const { logger } = require('@librechat/data-schemas'); const { Providers, @@ -503,6 +504,23 @@ function createToolInstance({ oauthEnd, }); + // Extract isError flag from result tuple [content, artifacts, isError] + const isError = Array.isArray(result) && result.length > 2 ? result[2] : false; + + // If there's an error, return a ToolMessage with error status + if (isError && config?.toolCall?.id) { + const content = Array.isArray(result) ? result[0] : result; + const artifact = Array.isArray(result) && result.length > 1 ? result[1] : undefined; + + return new ToolMessage({ + name: normalizedToolKey, + tool_call_id: config.toolCall.id, + content: typeof content === 'string' ? content : JSON.stringify(content), + status: 'error', + artifact, + }); + } + if (isAssistantsEndpoint(provider) && Array.isArray(result)) { return result[0]; } diff --git a/client/src/components/Chat/Messages/Content/MCPProgress.tsx b/client/src/components/Chat/Messages/Content/MCPProgress.tsx new file mode 100755 index 000000000000..6ec8d01cc074 --- /dev/null +++ b/client/src/components/Chat/Messages/Content/MCPProgress.tsx @@ -0,0 +1,59 @@ +import { useMemo } from 'react'; +import { useAtomValue } from 'jotai'; +import { getMCPProgressAtom } from '~/store/mcp'; + +interface MCPProgressProps { + conversationId: string; + serverName: string; + toolName: string; +} + +export default function MCPProgress({ conversationId, serverName, toolName }: MCPProgressProps) { + const progressData = useAtomValue(getMCPProgressAtom(conversationId)); + + // Get the latest progress for this specific tool call + const latestProgress = useMemo(() => { + if (!progressData || progressData.length === 0) { + return null; + } + + // Filter for this specific server and tool + const toolProgress = progressData.filter( + (p) => p.serverName === serverName && p.toolName === toolName, + ); + + if (toolProgress.length === 0) { + return null; + } + + // Get the most recent progress update + return toolProgress[toolProgress.length - 1]; + }, [progressData, serverName, toolName]); + + if (!latestProgress) { + return null; + } + + const { progress, total, message } = latestProgress; + const percentage = total ? Math.round((progress / total) * 100) : Math.round(progress * 100); + + return ( +
+
+ + {serverName} - {toolName} + + {percentage}% +
+ +
+
+
+ + {message &&
{message}
} +
+ ); +} diff --git a/client/src/components/Chat/Messages/Content/Part.tsx b/client/src/components/Chat/Messages/Content/Part.tsx index 16de45d476f4..c27f764b8422 100644 --- a/client/src/components/Chat/Messages/Content/Part.tsx +++ b/client/src/components/Chat/Messages/Content/Part.tsx @@ -6,7 +6,7 @@ import { imageGenTools, isImageVisionTool, } from 'librechat-data-provider'; -import { memo } from 'react'; +import { memo, useContext } from 'react'; import type { TMessageContentParts, TAttachment } from 'librechat-data-provider'; import { OpenAIImageGen, EmptyText, Reasoning, ExecuteCode, AgentUpdate, Text } from './Parts'; import { ErrorMessage } from './MessageContent'; @@ -18,6 +18,7 @@ import WebSearch from './WebSearch'; import ToolCall from './ToolCall'; import ImageGen from './ImageGen'; import Image from './Image'; +import { MessageContext } from '~/Providers'; type PartProps = { part?: TMessageContentParts; @@ -30,6 +31,7 @@ type PartProps = { const Part = memo( ({ part, isSubmitting, attachments, isLast, showCursor, isCreatedByUser }: PartProps) => { + const { conversationId } = useContext(MessageContext); if (!part) { return null; } @@ -145,6 +147,8 @@ const Part = memo( auth={toolCall.auth} expires_at={toolCall.expires_at} isLast={isLast} + conversationId={conversationId} + isError={toolCall.isError} /> ); } else if (toolCall.type === ToolCallTypes.CODE_INTERPRETER) { @@ -194,6 +198,8 @@ const Part = memo( name={toolCall.function.name} output={toolCall.function.output} isLast={isLast} + conversationId={conversationId} + isError={toolCall.function.isError} /> ); } diff --git a/client/src/components/Chat/Messages/Content/ToolCall.tsx b/client/src/components/Chat/Messages/Content/ToolCall.tsx index b9feef1badf0..c2813f44fcec 100644 --- a/client/src/components/Chat/Messages/Content/ToolCall.tsx +++ b/client/src/components/Chat/Messages/Content/ToolCall.tsx @@ -7,6 +7,7 @@ import { useLocalize, useProgress } from '~/hooks'; import { AttachmentGroup } from './Parts'; import ToolCallInfo from './ToolCallInfo'; import ProgressText from './ProgressText'; +import MCPProgress from './MCPProgress'; import { logger, cn } from '~/utils'; export default function ToolCall({ @@ -18,6 +19,8 @@ export default function ToolCall({ output, attachments, auth, + conversationId, + isError: toolIsError, }: { initialProgress: number; isLast?: boolean; @@ -28,6 +31,8 @@ export default function ToolCall({ attachments?: TAttachment[]; auth?: string; expires_at?: number; + conversationId?: string | null; + isError?: boolean; }) { const localize = useLocalize(); const [showInfo, setShowInfo] = useState(false); @@ -58,8 +63,20 @@ export default function ToolCall({ }; }, [name]); - const error = - typeof output === 'string' && output.toLowerCase().includes('error processing tool'); + // old: + // const error = + // typeof output === 'string' && output.toLowerCase().includes('error processing tool'); + + // Check for error using the isError flag from the MCP protocol + const error = useMemo(() => { + // If isError field is explicitly set (from MCP protocol), use it + if (typeof toolIsError === 'boolean') { + return toolIsError; + } + // Fallback: check if the response is a string containing "error" + // This is kept for backwards compatibility with non-MCP tools + return typeof output === 'string' && output.toLowerCase().includes('error processing tool'); + }, [toolIsError, output]); const args = useMemo(() => { if (typeof _args === 'string') { @@ -101,6 +118,9 @@ export default function ToolCall({ const progress = useProgress(initialProgress); const cancelled = (!isSubmitting && progress < 1) || error === true; + // Type guard for conversationId + const hasConversationId = conversationId != null && conversationId !== ''; + const getFinishedText = () => { if (cancelled) { return localize('com_ui_cancelled'); @@ -181,6 +201,15 @@ export default function ToolCall({ error={cancelled} />
+ {isMCPToolCall && hasConversationId && !cancelled && progress < 1 && ( +
+ +
+ )}
void; @@ -61,6 +64,8 @@ export default function useStepHandler({ const toolCallIdMap = useRef(new Map()); const messageMap = useRef(new Map()); const stepMap = useRef(new Map()); + const setMcpProgress = useSetAtom(mcpProgressAtom); + const setMcpActive = useSetAtom(mcpActiveAtom); /** * Calculate content index for a run step. @@ -89,6 +94,56 @@ export default function useStepHandler({ [], ); + // Callback to update MCP progress state + const updateMCPProgress = useCallback( + ( + conversationId: string, + progressData: { + serverName: string; + toolName: string; + progress: number; + total?: number; + message?: string; + progressToken: string; + }, + ) => { + const newProgress: MCPProgress = { + progressToken: progressData.progressToken, + serverName: progressData.serverName, + toolName: progressData.toolName, + progress: progressData.progress, + total: progressData.total, + message: progressData.message, + timestamp: Date.now(), + }; + + setMcpProgress((currentProgress) => { + const conversationProgress = currentProgress[conversationId] || []; + + // Find existing progress for this token or add new + const existingIndex = conversationProgress.findIndex( + (p) => p.progressToken === progressData.progressToken, + ); + + let updatedProgress; + if (existingIndex >= 0) { + // Update existing progress + updatedProgress = [...conversationProgress]; + updatedProgress[existingIndex] = newProgress; + } else { + // Add new progress + updatedProgress = [...conversationProgress, newProgress]; + } + + return { + ...currentProgress, + [conversationId]: updatedProgress, + }; + }); + }, + [setMcpProgress], + ); + /** Metadata to propagate onto content parts for parallel rendering - uses ContentMetadata from data-provider */ const updateContent = ( @@ -472,6 +527,24 @@ export default function useStepHandler({ parentMessageId = submission?.initialResponse?.parentMessageId ?? ''; } + // Handle progress events for MCP tools + if (runStepDelta.delta.type === 'progress') { + const progressData = runStepDelta.delta; + const conversationId = + submission?.initialResponse?.conversationId || userMessage.conversationId; + if (progressData.serverName && progressData.toolName && conversationId) { + updateMCPProgress(conversationId, { + serverName: progressData.serverName, + toolName: progressData.toolName, + progress: progressData.progress, + total: progressData.total, + message: progressData.message, + progressToken: progressData.progressToken, + }); + } + return; + } + if (!runStep || !responseMessageId) { console.warn('No run step or runId found for run step delta event'); return; diff --git a/client/src/store/mcp.ts b/client/src/store/mcp.ts index e540b167e444..18feb6fac12c 100644 --- a/client/src/store/mcp.ts +++ b/client/src/store/mcp.ts @@ -55,3 +55,48 @@ export const getServerInitState = ( ): MCPServerInitState => { return states[serverName] || defaultServerInitState; }; + +/** + * Interface for MCP progress tracking + */ +export interface MCPProgress { + progressToken: string; + serverName: string; + toolName: string; + progress: number; + total?: number; + message?: string; + timestamp: number; +} + +/** + * Atom for tracking MCP tool call progress + * Maps conversationId to progress data + */ +export const mcpProgressAtom = atom>({}); + +/** + * Atom for tracking active MCP operations + * Maps conversationId to boolean (true if active) + */ +export const mcpActiveAtom = atom>({}); + +/** + * Get progress for a conversation using atomFamily + */ +export const getMCPProgressAtom = atomFamily((conversationId: string) => + atom((get) => { + const progress = get(mcpProgressAtom); + return progress[conversationId] || []; + }), +); + +/** + * Get active state for a conversation using atomFamily + */ +export const getMCPActiveAtom = atomFamily((conversationId: string) => + atom((get) => { + const active = get(mcpActiveAtom); + return active[conversationId] || false; + }), +); diff --git a/packages/api/src/mcp/parsers.ts b/packages/api/src/mcp/parsers.ts index 76e59b2e9cf1..d86503aaf141 100644 --- a/packages/api/src/mcp/parsers.ts +++ b/packages/api/src/mcp/parsers.ts @@ -93,6 +93,7 @@ export function formatToolContent( result: t.MCPToolCallResponse, provider: t.Provider, ): t.FormattedContentResult { + const isError = result?.isError ?? false; if (!RECOGNIZED_PROVIDERS.has(provider)) { return [parseAsString(result), undefined]; } @@ -212,8 +213,8 @@ UI Resource Markers Available: } if (CONTENT_ARRAY_PROVIDERS.has(provider)) { - return [formattedContent, artifacts]; + return [formattedContent, artifacts, isError]; } - return [currentTextBlock, artifacts]; + return [currentTextBlock, artifacts, isError]; } diff --git a/packages/api/src/mcp/types/index.ts b/packages/api/src/mcp/types/index.ts index 63a0153cad47..06862d474bf6 100644 --- a/packages/api/src/mcp/types/index.ts +++ b/packages/api/src/mcp/types/index.ts @@ -142,7 +142,7 @@ export type Artifacts = } | undefined; -export type FormattedContentResult = [string | FormattedContent[], undefined | Artifacts]; +export type FormattedContentResult = [string | FormattedContent[], undefined | Artifacts, boolean?]; export type ImageFormatter = (item: ImageContent) => FormattedContent; diff --git a/packages/data-provider/src/types/agents.ts b/packages/data-provider/src/types/agents.ts index ac3f46401900..c34d757aaef6 100644 --- a/packages/data-provider/src/types/agents.ts +++ b/packages/data-provider/src/types/agents.ts @@ -81,6 +81,8 @@ export namespace Agents { auth?: string; /** Expiration time */ expires_at?: number; + /** If true, indicates the tool call resulted in an error */ + isError?: boolean; }; export type ToolEndEvent = { @@ -240,12 +242,22 @@ export namespace Agents { type: StepTypes.TOOL_CALLS; tool_calls: AgentToolCall[]; }; - export type ToolCallDelta = { - type: StepTypes.TOOL_CALLS | string; - tool_calls?: ToolCallChunk[]; - auth?: string; - expires_at?: number; - }; + export type ToolCallDelta = + | { + type: StepTypes.TOOL_CALLS | string; + tool_calls?: ToolCallChunk[]; + auth?: string; + expires_at?: number; + } + | { + type: 'progress'; + progressToken: string; + serverName: string; + toolName: string; + progress: number; + total?: number; + message?: string; + }; export type AgentToolCall = FunctionToolCall | ToolCall; export interface ExtendedMessageContent { type?: string; diff --git a/packages/data-provider/src/types/assistants.ts b/packages/data-provider/src/types/assistants.ts index 9e1deb20c18e..23087c8af03a 100644 --- a/packages/data-provider/src/types/assistants.ts +++ b/packages/data-provider/src/types/assistants.ts @@ -342,6 +342,7 @@ export type CodeToolCall = { export type FunctionToolCall = { id: string; // The ID of the tool call object. function: { + isError?: boolean; arguments: string; // The arguments passed to the function. name: string; // The name of the function. output: string | null; // The output of the function, null if not submitted. From 56ae80bf159307fd266be83c307cd96205f2567a Mon Sep 17 00:00:00 2001 From: Alexander Smosljajev Date: Wed, 31 Dec 2025 19:27:38 +0100 Subject: [PATCH 2/6] - Add EventEmitter integration to MCPManager for progress event forwarding - Subscribe to MCP protocol progress notifications in MCPConnection - Implement direct SSE emission of progress events from MCP service - Handle progress delta events in client-side useStepHandler - Pass progressToken in MCP tool call requests for event correlation - Clean up progress tracking on tool completion and errors --- api/server/services/MCP.js | 49 +++++++++++++++++- client/src/hooks/SSE/useStepHandler.ts | 45 ++++++++++++++-- packages/api/src/mcp/MCPManager.ts | 72 ++++++++++++++++++++++++++ packages/api/src/mcp/connection.ts | 22 +++++++- 4 files changed, 182 insertions(+), 6 deletions(-) diff --git a/api/server/services/MCP.js b/api/server/services/MCP.js index 1f7ba65829bc..38c285c42ec2 100644 --- a/api/server/services/MCP.js +++ b/api/server/services/MCP.js @@ -447,12 +447,16 @@ function createToolInstance({ let abortHandler = null; /** @type {AbortSignal} */ let derivedSignal = null; + /** @type {ReturnType} */ + let mcpManager = null; + /** @type {Function | null} */ + let progressListener = null; try { const flowsCache = getLogStores(CacheKeys.FLOWS); const flowManager = getFlowStateManager(flowsCache); derivedSignal = config?.signal ? AbortSignal.any([config.signal]) : undefined; - const mcpManager = getMCPManager(userId); + mcpManager = getMCPManager(userId); const provider = (config?.metadata?.provider || _provider)?.toLowerCase(); const { args: _args, stepId, ...toolCall } = config.toolCall ?? {}; @@ -475,6 +479,44 @@ function createToolInstance({ streamId, }); + // Set up progress event listener for MCP tools + progressListener = (progressData) => { + if (progressData.serverName === serverName && progressData.userId === userId) { + /** @type {{ id: string; delta: any }} */ + const data = { + id: stepId, + delta: { + type: 'progress', + progressToken: progressData.progressToken, + progress: progressData.progress, + total: progressData.total, + message: progressData.message, + serverName: progressData.serverName, + toolName: progressData.toolName, + }, + }; + + try { + logger.info(`[MCP.js] DIRECTLY emitting progress to SSE (streamId: ${streamId ? 'yes' : 'no'})`, { + serverName: progressData.serverName, + toolName: progressData.toolName, + progress: progressData.progress, + total: progressData.total, + }); + const eventData = { event: GraphEvents.ON_RUN_STEP_DELTA, data }; + if (streamId) { + GenerationJobManager.emitChunk(streamId, eventData); + } else { + sendEvent(res, eventData); + } + } catch (error) { + logger.error(`[MCP Service] Error sending progress SSE event:`, error); + } + } + }; + + mcpManager.on('toolProgress', progressListener); + if (derivedSignal) { abortHandler = createAbortHandler({ userId, serverName, toolName, flowManager }); derivedSignal.addEventListener('abort', abortHandler, { once: true }); @@ -551,6 +593,11 @@ function createToolInstance({ `[MCP][${serverName}][${toolName}] tool call failed${error?.message ? `: ${error?.message}` : '.'}`, ); } finally { + // Clean up progress listener + if (mcpManager && progressListener) { + mcpManager.off('toolProgress', progressListener); + } + // Clean up abort handler to prevent memory leaks if (abortHandler && derivedSignal) { derivedSignal.removeEventListener('abort', abortHandler); diff --git a/client/src/hooks/SSE/useStepHandler.ts b/client/src/hooks/SSE/useStepHandler.ts index a01c6d98a935..9f4b9c9c3be2 100644 --- a/client/src/hooks/SSE/useStepHandler.ts +++ b/client/src/hooks/SSE/useStepHandler.ts @@ -533,6 +533,12 @@ export default function useStepHandler({ const conversationId = submission?.initialResponse?.conversationId || userMessage.conversationId; if (progressData.serverName && progressData.toolName && conversationId) { + // Set active state when receiving progress updates + setMcpActive((current) => ({ + ...current, + [conversationId]: true, + })); + updateMCPProgress(conversationId, { serverName: progressData.serverName, toolName: progressData.toolName, @@ -605,6 +611,16 @@ export default function useStepHandler({ parentMessageId = submission?.initialResponse?.parentMessageId ?? ''; } + // Clear active state for this conversation when tool completes + const conversationId = + submission?.initialResponse?.conversationId || userMessage.conversationId; + if (conversationId) { + setMcpActive((current) => ({ + ...current, + [conversationId]: false, + })); + } + if (!runStep || !responseMessageId) { console.warn('No run step or runId found for completed tool call event'); return; @@ -619,11 +635,24 @@ export default function useStepHandler({ tool_call: result.tool_call, }; - // Use server's index, offset by initialContent for edit scenarios - const currentIndex = runStep.index + initialContent.length; + // Completion events come back with wrong stepId, so find the next incomplete tool call + const targetToolCallId = result.tool_call?.id; + let targetIndex = runStep.index + initialContent.length; // fallback to original logic + + // Find the first incomplete tool call with matching ID + const incompleteIndex = updatedResponse.content?.findIndex( + (part) => + part?.type === ContentTypes.TOOL_CALL && + part[ContentTypes.TOOL_CALL]?.id === targetToolCallId && + !part[ContentTypes.TOOL_CALL]?.progress, + ); + if (incompleteIndex !== undefined && incompleteIndex >= 0) { + targetIndex = incompleteIndex; + } + updatedResponse = updateContent( updatedResponse, - currentIndex, + targetIndex, contentPart, true, getStepMetadata(runStep), @@ -644,7 +673,15 @@ export default function useStepHandler({ stepMap.current.clear(); }; }, - [getMessages, lastAnnouncementTimeRef, announcePolite, setMessages, calculateContentIndex], + [ + getMessages, + lastAnnouncementTimeRef, + announcePolite, + setMessages, + updateMCPProgress, + setMcpActive, + calculateContentIndex, + ], ); const clearStepMaps = useCallback(() => { diff --git a/packages/api/src/mcp/MCPManager.ts b/packages/api/src/mcp/MCPManager.ts index fbd5bd050dfc..8df6009c0043 100644 --- a/packages/api/src/mcp/MCPManager.ts +++ b/packages/api/src/mcp/MCPManager.ts @@ -1,4 +1,6 @@ import pick from 'lodash/pick'; +import { EventEmitter } from 'events'; +import { randomUUID } from 'crypto'; import { logger } from '@librechat/data-schemas'; import { CallToolResultSchema, ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; import type { RequestOptions } from '@modelcontextprotocol/sdk/shared/protocol.js'; @@ -22,6 +24,11 @@ import { processMCPEnv } from '~/utils/env'; */ export class MCPManager extends UserConnectionManager { private static instance: MCPManager | null; + private eventEmitter: EventEmitter = new EventEmitter(); + private progressTokens: Map< + string, + { serverName: string; toolName: string; userId?: string } + > = new Map(); /** Creates and initializes the singleton MCPManager instance */ public static async createInstance(configs: t.MCPServers): Promise { @@ -43,6 +50,38 @@ export class MCPManager extends UserConnectionManager { this.appConnections = new ConnectionsRepository(undefined); } + /** + * EventEmitter delegation methods for progress tracking + */ + on(event: string | symbol, listener: (...args: unknown[]) => void): this { + this.eventEmitter.on(event, listener); + return this; + } + + off(event: string | symbol, listener: (...args: unknown[]) => void): this { + this.eventEmitter.off(event, listener); + return this; + } + + emit(event: string | symbol, ...args: unknown[]): boolean { + return this.eventEmitter.emit(event, ...args); + } + + once(event: string | symbol, listener: (...args: unknown[]) => void): this { + this.eventEmitter.once(event, listener); + return this; + } + + removeListener(event: string | symbol, listener: (...args: unknown[]) => void): this { + this.eventEmitter.removeListener(event, listener); + return this; + } + + removeAllListeners(event?: string | symbol): this { + this.eventEmitter.removeAllListeners(event); + return this; + } + /** Retrieves an app-level or user-specific connection based on provided arguments */ public async getConnection( args: { @@ -193,6 +232,13 @@ Please follow these instructions when using tools from the respective MCP server const userId = user?.id; const logPrefix = userId ? `[MCP][User: ${userId}][${serverName}]` : `[MCP][${serverName}]`; + // Generate a unique progress token for this tool call + const progressToken = `${serverName}:${toolName}:${randomUUID()}`; + this.progressTokens.set(progressToken, { serverName, toolName, userId }); + + // Progress listener to forward events + let progressListener: ((progressData: unknown) => void) | undefined; + try { if (userId && user) this.updateUserLastActivity(userId); @@ -216,6 +262,23 @@ Please follow these instructions when using tools from the respective MCP server ); } + // Set up progress event listener + progressListener = (progressData: any) => { + if (progressData.progressToken === progressToken) { + this.emit('toolProgress', { + progressToken, + serverName, + toolName, + userId, + progress: progressData.progress, + total: progressData.total, + message: progressData.message, + }); + } + }; + + connection.on('progress', progressListener); + const rawConfig = (await MCPServersRegistry.getInstance().getServerConfig( serverName, userId, @@ -236,6 +299,9 @@ Please follow these instructions when using tools from the respective MCP server params: { name: toolName, arguments: toolArguments, + _meta: { + progressToken, + }, }, }, CallToolResultSchema, @@ -255,6 +321,12 @@ Please follow these instructions when using tools from the respective MCP server logger.error(`${logPrefix}[${toolName}] Tool call failed`, error); // Rethrowing allows the caller (createMCPTool) to handle the final user message throw error; + } finally { + // Clean up progress listener and token + if (connection && progressListener) { + connection.off('progress', progressListener); + } + this.progressTokens.delete(progressToken); } } } diff --git a/packages/api/src/mcp/connection.ts b/packages/api/src/mcp/connection.ts index b954a2e839b7..88941e7d39af 100644 --- a/packages/api/src/mcp/connection.ts +++ b/packages/api/src/mcp/connection.ts @@ -8,7 +8,11 @@ import { import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'; import { WebSocketClientTransport } from '@modelcontextprotocol/sdk/client/websocket.js'; -import { ResourceListChangedNotificationSchema } from '@modelcontextprotocol/sdk/types.js'; +import { + ResourceListChangedNotificationSchema, + ProgressNotificationSchema, + type ProgressNotification, +} from '@modelcontextprotocol/sdk/types.js'; import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'; import type { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js'; @@ -526,6 +530,7 @@ export class MCPConnection extends EventEmitter { }); this.subscribeToResources(); + this.subscribeToProgressNotifications(); } private async handleReconnection(): Promise { @@ -603,6 +608,21 @@ export class MCPConnection extends EventEmitter { }); } + private subscribeToProgressNotifications(): void { + this.client.setNotificationHandler( + ProgressNotificationSchema, + async (notification: ProgressNotification) => { + this.emit('progress', { + serverName: this.serverName, + progressToken: notification.params?.progressToken, + progress: notification.params?.progress, + total: notification.params?.total, + message: notification.params?.message, + }); + }, + ); + } + async connectClient(): Promise { if (this.connectionState === 'connected') { return; From a5e87ff966885ec96a0a14c01682b8ff182fa66b Mon Sep 17 00:00:00 2001 From: Alexander Smosljajev Date: Wed, 31 Dec 2025 19:32:06 +0100 Subject: [PATCH 3/6] remove debug log --- api/server/services/MCP.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/api/server/services/MCP.js b/api/server/services/MCP.js index 38c285c42ec2..13eb65703b1b 100644 --- a/api/server/services/MCP.js +++ b/api/server/services/MCP.js @@ -497,12 +497,6 @@ function createToolInstance({ }; try { - logger.info(`[MCP.js] DIRECTLY emitting progress to SSE (streamId: ${streamId ? 'yes' : 'no'})`, { - serverName: progressData.serverName, - toolName: progressData.toolName, - progress: progressData.progress, - total: progressData.total, - }); const eventData = { event: GraphEvents.ON_RUN_STEP_DELTA, data }; if (streamId) { GenerationJobManager.emitChunk(streamId, eventData); From bb95bce20f66effbc1f8c6abfade446f045764f1 Mon Sep 17 00:00:00 2001 From: Alexander Smosljajev Date: Sun, 4 Jan 2026 13:57:54 +0100 Subject: [PATCH 4/6] fix: Resolve TypeScript errors by adding optional MCP progress fields to ToolCallDelta --- client/src/hooks/SSE/useStepHandler.ts | 8 +++++- packages/data-provider/src/types/agents.ts | 29 ++++++++++------------ 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/client/src/hooks/SSE/useStepHandler.ts b/client/src/hooks/SSE/useStepHandler.ts index 9f4b9c9c3be2..b40c321098db 100644 --- a/client/src/hooks/SSE/useStepHandler.ts +++ b/client/src/hooks/SSE/useStepHandler.ts @@ -532,7 +532,13 @@ export default function useStepHandler({ const progressData = runStepDelta.delta; const conversationId = submission?.initialResponse?.conversationId || userMessage.conversationId; - if (progressData.serverName && progressData.toolName && conversationId) { + if ( + progressData.serverName && + progressData.toolName && + progressData.progressToken && + progressData.progress != null && + conversationId + ) { // Set active state when receiving progress updates setMcpActive((current) => ({ ...current, diff --git a/packages/data-provider/src/types/agents.ts b/packages/data-provider/src/types/agents.ts index c34d757aaef6..eefe3a0582db 100644 --- a/packages/data-provider/src/types/agents.ts +++ b/packages/data-provider/src/types/agents.ts @@ -242,22 +242,19 @@ export namespace Agents { type: StepTypes.TOOL_CALLS; tool_calls: AgentToolCall[]; }; - export type ToolCallDelta = - | { - type: StepTypes.TOOL_CALLS | string; - tool_calls?: ToolCallChunk[]; - auth?: string; - expires_at?: number; - } - | { - type: 'progress'; - progressToken: string; - serverName: string; - toolName: string; - progress: number; - total?: number; - message?: string; - }; + export type ToolCallDelta = { + type: StepTypes.TOOL_CALLS | string; + tool_calls?: ToolCallChunk[]; + auth?: string; + expires_at?: number; + // Progress-related fields for MCP tools + progressToken?: string; + serverName?: string; + toolName?: string; + progress?: number; + total?: number; + message?: string; + }; export type AgentToolCall = FunctionToolCall | ToolCall; export interface ExtendedMessageContent { type?: string; From 2e7387d57aa3eb755bd8ab76e70ee2d3fc5b86e9 Mon Sep 17 00:00:00 2001 From: Alexander Smosljajev Date: Sun, 4 Jan 2026 14:51:39 +0100 Subject: [PATCH 5/6] revert error logic --- .../components/Chat/Messages/Content/ToolCall.tsx | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/client/src/components/Chat/Messages/Content/ToolCall.tsx b/client/src/components/Chat/Messages/Content/ToolCall.tsx index c2813f44fcec..8ef32e734b43 100644 --- a/client/src/components/Chat/Messages/Content/ToolCall.tsx +++ b/client/src/components/Chat/Messages/Content/ToolCall.tsx @@ -63,20 +63,9 @@ export default function ToolCall({ }; }, [name]); - // old: - // const error = - // typeof output === 'string' && output.toLowerCase().includes('error processing tool'); + const error = + typeof output === 'string' && output.toLowerCase().includes('error processing tool'); - // Check for error using the isError flag from the MCP protocol - const error = useMemo(() => { - // If isError field is explicitly set (from MCP protocol), use it - if (typeof toolIsError === 'boolean') { - return toolIsError; - } - // Fallback: check if the response is a string containing "error" - // This is kept for backwards compatibility with non-MCP tools - return typeof output === 'string' && output.toLowerCase().includes('error processing tool'); - }, [toolIsError, output]); const args = useMemo(() => { if (typeof _args === 'string') { From 1900314348c6df068d590d503d8fae8fef1de3ce Mon Sep 17 00:00:00 2001 From: Alexander Smosljajev Date: Sun, 4 Jan 2026 14:52:23 +0100 Subject: [PATCH 6/6] remove empty line --- client/src/components/Chat/Messages/Content/ToolCall.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/client/src/components/Chat/Messages/Content/ToolCall.tsx b/client/src/components/Chat/Messages/Content/ToolCall.tsx index 8ef32e734b43..334981bfe22d 100644 --- a/client/src/components/Chat/Messages/Content/ToolCall.tsx +++ b/client/src/components/Chat/Messages/Content/ToolCall.tsx @@ -66,7 +66,6 @@ export default function ToolCall({ const error = typeof output === 'string' && output.toLowerCase().includes('error processing tool'); - const args = useMemo(() => { if (typeof _args === 'string') { return _args;