From ddca8411a6831498182f576ef970e407a39caf9f Mon Sep 17 00:00:00 2001 From: Ara Date: Wed, 11 Jun 2025 14:20:54 +0530 Subject: [PATCH] Supporting Notifications MCP with Cline (#4129) * Adding real time client * Adding real time client * Adding real time client * first commit * Adding thinking Slider for Gemini models --------- Co-authored-by: Cline Evaluation --- .changeset/sour-poets-smile.md | 5 + .vscode/tasks.json | 40 ++++- proto/ui.proto | 15 +- src/core/task/index.ts | 22 +++ src/services/mcp/McpHub.ts | 137 ++++++++++++++++++ src/shared/ExtensionMessage.ts | 1 + src/shared/proto-conversions/cline-message.ts | 2 + webview-ui/src/components/chat/ChatRow.tsx | 30 ++++ 8 files changed, 243 insertions(+), 9 deletions(-) create mode 100644 .changeset/sour-poets-smile.md diff --git a/.changeset/sour-poets-smile.md b/.changeset/sour-poets-smile.md new file mode 100644 index 0000000000..1fac657fca --- /dev/null +++ b/.changeset/sour-poets-smile.md @@ -0,0 +1,5 @@ +--- +"claude-dev": patch +--- + +Supporting Notifications MCP with Cline diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 25d7c42547..5e47a904ee 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -128,7 +128,25 @@ "type": "npm", "script": "watch:esbuild", "group": "build", - "problemMatcher": "$esbuild-watch", + "problemMatcher": { + "pattern": [ + { + "regexp": "^✘ \\[ERROR\\] (.*)$", + "message": 1 + }, + { + "regexp": "^\\s+(.*):(\\d+):(\\d+):$", + "file": 1, + "line": 2, + "column": 3 + } + ], + "background": { + "activeOnStart": true, + "beginsPattern": "^\\[watch\\] build started$", + "endsPattern": "^\\[watch\\] build finished$" + } + }, "isBackground": true, "label": "npm: watch:esbuild", "dependsOn": ["npm: protos"], @@ -146,7 +164,25 @@ "type": "npm", "script": "watch:esbuild:test", "group": "build", - "problemMatcher": "$esbuild-watch", + "problemMatcher": { + "pattern": [ + { + "regexp": "^✘ \\[ERROR\\] (.*)$", + "message": 1 + }, + { + "regexp": "^\\s+(.*):(\\d+):(\\d+):$", + "file": 1, + "line": 2, + "column": 3 + } + ], + "background": { + "activeOnStart": true, + "beginsPattern": "^\\[watch\\] build started$", + "endsPattern": "^\\[watch\\] build finished$" + } + }, "isBackground": true, "label": "npm: watch:esbuild:test", "dependsOn": ["npm: protos"], diff --git a/proto/ui.proto b/proto/ui.proto index 64e7de785c..216ac1db6f 100644 --- a/proto/ui.proto +++ b/proto/ui.proto @@ -65,13 +65,14 @@ enum ClineSay { BROWSER_ACTION_RESULT = 16; MCP_SERVER_REQUEST_STARTED = 17; MCP_SERVER_RESPONSE = 18; - USE_MCP_SERVER_SAY = 19; - DIFF_ERROR = 20; - DELETED_API_REQS = 21; - CLINEIGNORE_ERROR = 22; - CHECKPOINT_CREATED = 23; - LOAD_MCP_DOCUMENTATION = 24; - INFO = 25; + MCP_NOTIFICATION = 19; + USE_MCP_SERVER_SAY = 20; + DIFF_ERROR = 21; + DELETED_API_REQS = 22; + CLINEIGNORE_ERROR = 23; + CHECKPOINT_CREATED = 24; + LOAD_MCP_DOCUMENTATION = 25; + INFO = 26; } // Enum for ClineSayTool tool types diff --git a/src/core/task/index.ts b/src/core/task/index.ts index 0a1b00c86f..f071bea747 100644 --- a/src/core/task/index.ts +++ b/src/core/task/index.ts @@ -233,6 +233,12 @@ export class Task { this.chatSettings = chatSettings this.enableCheckpoints = enableCheckpointsSetting + // Set up MCP notification callback for real-time notifications + this.mcpHub.setNotificationCallback(async (serverName: string, level: string, message: string) => { + // Display notification in chat immediately + await this.say("mcp_notification", `[${serverName}] ${message}`) + }) + // Initialize taskId first if (historyItem) { this.taskId = historyItem.id @@ -1196,6 +1202,9 @@ export class Task { this.clineIgnoreController.dispose() this.fileContextTracker.dispose() await this.diffViewProvider.revertChanges() // need to await for when we want to make sure directories/files are reverted before re-starting the task from a checkpoint + + // Clear the notification callback when task is aborted + this.mcpHub.clearNotificationCallback() } // Checkpoints @@ -3323,8 +3332,21 @@ export class Task { // now execute the tool await this.say("mcp_server_request_started") // same as browser_action_result + + // Check for any pending notifications before the tool call + const notificationsBefore = this.mcpHub.getPendingNotifications() + for (const notification of notificationsBefore) { + await this.say("mcp_notification", `[${notification.serverName}] ${notification.message}`) + } + const toolResult = await this.mcpHub.callTool(server_name, tool_name, parsedArguments) + // Check for any pending notifications after the tool call + const notificationsAfter = this.mcpHub.getPendingNotifications() + for (const notification of notificationsAfter) { + await this.say("mcp_notification", `[${notification.serverName}] ${notification.message}`) + } + // TODO: add progress indicator const toolResultImages = diff --git a/src/services/mcp/McpHub.ts b/src/services/mcp/McpHub.ts index 29808a348f..680697304f 100644 --- a/src/services/mcp/McpHub.ts +++ b/src/services/mcp/McpHub.ts @@ -54,6 +54,17 @@ export class McpHub { connections: McpConnection[] = [] isConnecting: boolean = false + // Store notifications for display in chat + private pendingNotifications: Array<{ + serverName: string + level: string + message: string + timestamp: number + }> = [] + + // Callback for sending notifications to active task + private notificationCallback?: (serverName: string, level: string, message: string) => void + constructor( getMcpServersPath: () => Promise, getSettingsDirectoryPath: () => Promise, @@ -321,6 +332,100 @@ export class McpHub { connection.server.status = "connected" connection.server.error = "" + // Register notification handler for real-time messages + console.log(`[MCP Debug] Setting up notification handlers for server: ${name}`) + console.log(`[MCP Debug] Client instance:`, connection.client) + console.log(`[MCP Debug] Transport type:`, config.type) + + // Try to set notification handler using the client's method + try { + // Import the notification schema from MCP SDK + const { z } = await import("zod") + + // Define the notification schema for notifications/message + const NotificationMessageSchema = z.object({ + method: z.literal("notifications/message"), + params: z + .object({ + level: z.enum(["debug", "info", "warning", "error"]).optional(), + logger: z.string().optional(), + data: z.string().optional(), + message: z.string().optional(), + }) + .optional(), + }) + + // Set the notification handler + connection.client.setNotificationHandler(NotificationMessageSchema as any, async (notification: any) => { + console.log(`[MCP Notification] ${name}:`, JSON.stringify(notification, null, 2)) + + const params = notification.params || {} + const level = params.level || "info" + const data = params.data || params.message || "" + const logger = params.logger || "" + + console.log(`[MCP Message Notification] ${name}: level=${level}, data=${data}, logger=${logger}`) + + // Format the message + const message = logger ? `[${logger}] ${data}` : data + + // Send notification directly to active task if callback is set + if (this.notificationCallback) { + console.log(`[MCP Debug] Sending notification to active task: ${message}`) + this.notificationCallback(name, level, message) + } else { + // Fallback: store for later retrieval + console.log(`[MCP Debug] No active task, storing notification: ${message}`) + this.pendingNotifications.push({ + serverName: name, + level, + message, + timestamp: Date.now(), + }) + } + + // Also show as VS Code notification for now (can be removed later if desired) + switch (level) { + case "error": + vscode.window.showErrorMessage(`MCP ${name}: ${message}`) + break + case "warning": + vscode.window.showWarningMessage(`MCP ${name}: ${message}`) + break + default: + vscode.window.showInformationMessage(`MCP ${name}: ${message}`) + } + + // Forward to webview if available + if (this.postMessageToWebview) { + await this.postMessageToWebview({ + type: "mcpNotification", + serverName: name, + notification: { + level, + data, + logger, + timestamp: Date.now(), + }, + } as any) + } + }) + console.log(`[MCP Debug] Successfully set notifications/message handler for ${name}`) + + // Also set a fallback handler for any other notification types + connection.client.fallbackNotificationHandler = async (notification: any) => { + console.log(`[MCP Fallback Notification] ${name}:`, JSON.stringify(notification, null, 2)) + + // Show in VS Code for visibility + vscode.window.showInformationMessage( + `MCP ${name}: ${notification.method || "unknown"} - ${JSON.stringify(notification.params || {})}`, + ) + } + console.log(`[MCP Debug] Successfully set fallback notification handler for ${name}`) + } catch (error) { + console.error(`[MCP Debug] Error setting notification handlers for ${name}:`, error) + } + // Initial fetch of tools and resources connection.server.tools = await this.fetchToolsList(name) connection.server.resources = await this.fetchResourcesList(name) @@ -949,6 +1054,38 @@ export class McpHub { } } + /** + * Get and clear pending notifications + * @returns Array of pending notifications + */ + getPendingNotifications(): Array<{ + serverName: string + level: string + message: string + timestamp: number + }> { + const notifications = [...this.pendingNotifications] + this.pendingNotifications = [] + return notifications + } + + /** + * Set the notification callback for real-time notifications + * @param callback Function to call when notifications arrive + */ + setNotificationCallback(callback: (serverName: string, level: string, message: string) => void): void { + this.notificationCallback = callback + console.log("[MCP Debug] Notification callback set") + } + + /** + * Clear the notification callback + */ + clearNotificationCallback(): void { + this.notificationCallback = undefined + console.log("[MCP Debug] Notification callback cleared") + } + async dispose(): Promise { this.removeAllFileWatchers() for (const connection of this.connections) { diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 492b830bcf..f3de2b97f9 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -163,6 +163,7 @@ export type ClineSay = | "browser_action_result" | "mcp_server_request_started" | "mcp_server_response" + | "mcp_notification" | "use_mcp_server" | "diff_error" | "deleted_api_reqs" diff --git a/src/shared/proto-conversions/cline-message.ts b/src/shared/proto-conversions/cline-message.ts index 2511c0b959..2051040ca6 100644 --- a/src/shared/proto-conversions/cline-message.ts +++ b/src/shared/proto-conversions/cline-message.ts @@ -89,6 +89,7 @@ function convertClineSayToProtoEnum(say: AppClineSay | undefined): ClineSay | un browser_action_result: ClineSay.BROWSER_ACTION_RESULT, mcp_server_request_started: ClineSay.MCP_SERVER_REQUEST_STARTED, mcp_server_response: ClineSay.MCP_SERVER_RESPONSE, + mcp_notification: ClineSay.MCP_NOTIFICATION, use_mcp_server: ClineSay.USE_MCP_SERVER_SAY, diff_error: ClineSay.DIFF_ERROR, deleted_api_reqs: ClineSay.DELETED_API_REQS, @@ -132,6 +133,7 @@ function convertProtoEnumToClineSay(say: ClineSay): AppClineSay | undefined { [ClineSay.BROWSER_ACTION_RESULT]: "browser_action_result", [ClineSay.MCP_SERVER_REQUEST_STARTED]: "mcp_server_request_started", [ClineSay.MCP_SERVER_RESPONSE]: "mcp_server_response", + [ClineSay.MCP_NOTIFICATION]: "mcp_notification", [ClineSay.USE_MCP_SERVER_SAY]: "use_mcp_server", [ClineSay.DIFF_ERROR]: "diff_error", [ClineSay.DELETED_API_REQS]: "deleted_api_reqs", diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index a718c07a0d..2a47d8060b 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -1054,6 +1054,36 @@ export const ChatRowContent = ({ return null // we should never see this message type case "mcp_server_response": return + case "mcp_notification": + return ( +
+ +
+ MCP Notification: + {message.text} +
+
+ ) case "text": return (