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;