Repository: cline/cline
Pull request #3085: Protobus: updateMcpTimeout message
Author: login: celestial-vault
Created: 2025-04-23T22:50:22Z | Merged: 2025-04-24T21:57:00Z

### Description

Convert the updateMcpTimeout message to protobus

### Test Procedure

Tested that the functionality works the same as before through:
- Checking that the value is correctly set in the mcp_settings.json
- Checking that the UI correctly updates
- logging throughout the flow

### Type of Change

<!-- Put an 'x' in all boxes that apply -->

-   [ ] 🐛 Bug fix (non-breaking change which fixes an issue)
-   [ ] ✨ New feature (non-breaking change which adds functionality)
-   [ ] 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected)
-   [x] ♻️ Refactor Changes
-   [ ] 💅 Cosmetic Changes
-   [ ] 📚 Documentation update
-   [ ] 🏃 Workflow Changes

### Pre-flight Checklist

<!-- Put an 'x' in all boxes that apply -->

-   [x] Changes are limited to a single feature, bugfix or chore (split larger changes into separate PRs)
-   [x] Tests are passing (`npm test`) and code is formatted and linted (`npm run format && npm run lint`)
-   [x] I have created a changeset using `npm run changeset` (required for user-facing changes)
-   [x] I have reviewed [contributor guidelines](https://github.com/cline/cline/blob/main/CONTRIBUTING.md)

### Screenshots

<!-- For UI changes, add screenshots here -->

### Additional Notes

<!-- Add any additional notes for reviewers -->

<!-- ELLIPSIS_HIDDEN -->

----

> [!IMPORTANT]
> Convert `updateMcpTimeout` functionality to use protobus, updating RPC methods and UI components accordingly.
> 
>   - **Behavior**:
>     - Convert `updateMcpTimeout` to use protobus in `proto/mcp.proto`.
>     - Add `updateMcpTimeout` RPC method to `McpService`.
>     - Remove old `updateMcpTimeout` message handling in `index.ts`.
>   - **Methods**:
>     - Add `updateMcpTimeout` method in `updateMcpTimeout.ts` to handle new RPC.
>     - Register `updateMcpTimeout` in `methods.ts`.
>   - **UI**:
>     - Update `ServerRow.tsx` to use `McpServiceClient.updateMcpTimeout` for timeout changes.
>   - **Services**:
>     - Implement `updateServerTimeoutRPC` in `McpHub.ts` to update server timeout using protobus.
> 
> <sup>This description was created by </sup>[<img alt="Ellipsis" src="https://img.shields.io/badge/Ellipsis-blue?color=175173">](https://www.ellipsis.dev?ref=cline%2Fcline&utm_source=github&utm_medium=referral)<sup> for 0d44c9605c1cb227326a7e32cfb945e8ecfe2e8f. You can [customize](https://app.ellipsis.dev/cline/settings/summaries) this summary. It will automatically update as commits are pushed.</sup>

<!-- ELLIPSIS_HIDDEN -->


Diff:
diff --git a/.changeset/clean-chicken-smash.md b/.changeset/clean-chicken-smash.md
new file mode 100644
index 00000000000..3399ec6769a
--- /dev/null
+++ b/.changeset/clean-chicken-smash.md
@@ -0,0 +1,5 @@
+---
+"claude-dev": minor
+---
+
+Move updateMcpTimeout message to protobus
diff --git a/proto/mcp.proto b/proto/mcp.proto
index cfbe0e98131..498525fac50 100644
--- a/proto/mcp.proto
+++ b/proto/mcp.proto
@@ -6,6 +6,19 @@ import "common.proto";
 
 service McpService {
   rpc toggleMcpServer(ToggleMcpServerRequest) returns (McpServers);
+  rpc updateMcpTimeout(UpdateMcpTimeoutRequest) returns (McpServers);
+}
+
+message ToggleMcpServerRequest {
+  Metadata metadata = 1;
+  string server_name = 2;
+  bool disabled = 3;
+}
+
+message UpdateMcpTimeoutRequest {
+  Metadata metadata = 1;
+  string server_name = 2;
+  int32 timeout = 3;
 }
 
 message McpTool {
@@ -49,12 +62,6 @@ message McpServer {
   optional int32 timeout = 9;
 }
 
-message ToggleMcpServerRequest {
-  Metadata metadata = 1;
-  string server_name = 2;
-  bool disabled = 3;
-}
-
 message McpServers {
   repeated McpServer mcp_servers = 1;
 }
diff --git a/src/core/controller/index.ts b/src/core/controller/index.ts
index 799e7c40923..b60f9b753b5 100644
--- a/src/core/controller/index.ts
+++ b/src/core/controller/index.ts
@@ -682,16 +682,6 @@ export class Controller {
 				}
 				break
 			}
-			case "updateMcpTimeout": {
-				try {
-					if (message.serverName && message.timeout) {
-						await this.mcpHub?.updateServerTimeout(message.serverName, message.timeout)
-					}
-				} catch (error) {
-					console.error(`Failed to update timeout for server ${message.serverName}:`, error)
-				}
-				break
-			}
 			case "openExtensionSettings": {
 				const settingsFilter = message.text || ""
 				await vscode.commands.executeCommand(
diff --git a/src/core/controller/mcp/methods.ts b/src/core/controller/mcp/methods.ts
index 0fd56a9830e..0e292f0e6d2 100644
--- a/src/core/controller/mcp/methods.ts
+++ b/src/core/controller/mcp/methods.ts
@@ -4,9 +4,11 @@
 // Import all method implementations
 import { registerMethod } from "./index"
 import { toggleMcpServer } from "./toggleMcpServer"
+import { updateMcpTimeout } from "./updateMcpTimeout"
 
 // Register all mcp service methods
 export function registerAllMethods(): void {
 	// Register each method with the registry
 	registerMethod("toggleMcpServer", toggleMcpServer)
+	registerMethod("updateMcpTimeout", updateMcpTimeout)
 }
diff --git a/src/core/controller/mcp/updateMcpTimeout.ts b/src/core/controller/mcp/updateMcpTimeout.ts
new file mode 100644
index 00000000000..2897be12f15
--- /dev/null
+++ b/src/core/controller/mcp/updateMcpTimeout.ts
@@ -0,0 +1,27 @@
+import { convertMcpServersToProtoMcpServers } from "@/shared/proto-conversions/mcp/mcp-server-conversion"
+import { Controller } from ".."
+import { UpdateMcpTimeoutRequest, McpServers } from "../../../shared/proto/mcp"
+
+/**
+ * Updates the timeout configuration for an MCP server.
+ * @param controller - The Controller instance
+ * @param request - Contains server name and timeout value
+ * @returns Array of updated McpServer objects
+ */
+export async function updateMcpTimeout(controller: Controller, request: UpdateMcpTimeoutRequest): Promise<McpServers> {
+	try {
+		if (request.serverName && typeof request.serverName === "string" && typeof request.timeout === "number") {
+			const mcpServers = await controller.mcpHub?.updateServerTimeoutRPC(request.serverName, request.timeout)
+			console.log("mcpServers", mcpServers)
+			const convertedMcpServers = convertMcpServersToProtoMcpServers(mcpServers)
+			console.log("convertedMcpServers", convertedMcpServers)
+			return { mcpServers: convertedMcpServers }
+		} else {
+			console.error("Server name and timeout are required")
+			throw new Error("Server name and timeout are required")
+		}
+	} catch (error) {
+		console.error(`Failed to update timeout for server ${request.serverName}:`, error)
+		throw error
+	}
+}
diff --git a/src/services/mcp/McpHub.ts b/src/services/mcp/McpHub.ts
index f064da2c858..aeb3402f112 100644
--- a/src/services/mcp/McpHub.ts
+++ b/src/services/mcp/McpHub.ts
@@ -329,10 +329,9 @@ export class McpHub {
 				autoApprove: autoApproveConfig.includes(tool.name),
 			}))
 
-			// console.log(`[MCP] Fetched tools for ${serverName}:`, tools)
 			return tools
 		} catch (error) {
-			// console.error(`Failed to fetch tools for ${serverName}:`, error)
+			console.error(`Failed to fetch tools for ${serverName}:`, error)
 			return []
 		}
 	}
@@ -377,6 +376,53 @@ export class McpHub {
 		}
 	}
 
+	async updateServerConnectionsRPC(newServers: Record<string, McpServerConfig>): Promise<void> {
+		this.isConnecting = true
+		this.removeAllFileWatchers()
+		const currentNames = new Set(this.connections.map((conn) => conn.server.name))
+		const newNames = new Set(Object.keys(newServers))
+
+		// Delete removed servers
+		for (const name of currentNames) {
+			if (!newNames.has(name)) {
+				await this.deleteConnection(name)
+				console.log(`Deleted MCP server: ${name}`)
+			}
+		}
+
+		// Update or add servers
+		for (const [name, config] of Object.entries(newServers)) {
+			const currentConnection = this.connections.find((conn) => conn.server.name === name)
+
+			if (!currentConnection) {
+				// New server
+				try {
+					if (config.transportType === "stdio") {
+						this.setupFileWatcher(name, config)
+					}
+					await this.connectToServer(name, config)
+				} catch (error) {
+					console.error(`Failed to connect to new MCP server ${name}:`, error)
+				}
+			} else if (!deepEqual(JSON.parse(currentConnection.server.config), config)) {
+				// Existing server with changed config
+				try {
+					if (config.transportType === "stdio") {
+						this.setupFileWatcher(name, config)
+					}
+					await this.deleteConnection(name)
+					await this.connectToServer(name, config)
+					console.log(`Reconnected MCP server with updated config: ${name}`)
+				} catch (error) {
+					console.error(`Failed to reconnect MCP server ${name}:`, error)
+				}
+			}
+			// If server exists with same config, do nothing
+		}
+
+		this.isConnecting = false
+	}
+
 	async updateServerConnections(newServers: Record<string, McpServerConfig>): Promise<void> {
 		this.isConnecting = true
 		this.removeAllFileWatchers()
@@ -719,7 +765,7 @@ export class McpHub {
 		}
 	}
 
-	public async updateServerTimeout(serverName: string, timeout: number): Promise<void> {
+	public async updateServerTimeoutRPC(serverName: string, timeout: number): Promise<McpServer[]> {
 		try {
 			// Validate timeout against schema
 			const setConfigResult = BaseConfigSchema.shape.timeout.safeParse(timeout)
@@ -742,7 +788,18 @@ export class McpHub {
 
 			await fs.writeFile(settingsPath, JSON.stringify(config, null, 2))
 
-			await this.updateServerConnections(config.mcpServers)
+			await this.updateServerConnectionsRPC(config.mcpServers)
+
+			const serverOrder = Object.keys(config.mcpServers || {})
+			const updatedMcpServers = [...this.connections]
+				.sort((a, b) => {
+					const indexA = serverOrder.indexOf(a.server.name)
+					const indexB = serverOrder.indexOf(b.server.name)
+					return indexA - indexB
+				})
+				.map((connection) => connection.server)
+
+			return updatedMcpServers
 		} catch (error) {
 			console.error("Failed to update server timeout:", error)
 			if (error instanceof Error) {
diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts
index a651b1344d1..c81cced5876 100644
--- a/src/shared/WebviewMessage.ts
+++ b/src/shared/WebviewMessage.ts
@@ -47,7 +47,6 @@ export interface WebviewMessage {
 		| "openExtensionSettings"
 		| "requestVsCodeLmModels"
 		| "toggleToolAutoApprove"
-		| "toggleMcpServer"
 		| "getLatestState"
 		| "accountLoginClicked"
 		| "accountLogoutClicked"
@@ -62,7 +61,6 @@ export interface WebviewMessage {
 		| "fetchLatestMcpServersFromHub"
 		| "telemetrySetting"
 		| "openSettings"
-		| "updateMcpTimeout"
 		| "fetchOpenGraphData"
 		| "checkIsImageUrl"
 		| "invoke"
diff --git a/src/shared/proto/mcp.ts b/src/shared/proto/mcp.ts
index 46f11b99978..fb163f5bee5 100644
--- a/src/shared/proto/mcp.ts
+++ b/src/shared/proto/mcp.ts
@@ -53,6 +53,18 @@ export function mcpServerStatusToJSON(object: McpServerStatus): string {
 	}
 }
 
+export interface ToggleMcpServerRequest {
+	metadata?: Metadata | undefined
+	serverName: string
+	disabled: boolean
+}
+
+export interface UpdateMcpTimeoutRequest {
+	metadata?: Metadata | undefined
+	serverName: string
+	timeout: number
+}
+
 export interface McpTool {
 	name: string
 	description?: string | undefined
@@ -86,16 +98,196 @@ export interface McpServer {
 	timeout?: number | undefined
 }
 
-export interface ToggleMcpServerRequest {
-	metadata?: Metadata | undefined
-	serverName: string
-	disabled: boolean
-}
-
 export interface McpServers {
 	mcpServers: McpServer[]
 }
 
+function createBaseToggleMcpServerRequest(): ToggleMcpServerRequest {
+	return { metadata: undefined, serverName: "", disabled: false }
+}
+
+export const ToggleMcpServerRequest: MessageFns<ToggleMcpServerRequest> = {
+	encode(message: ToggleMcpServerRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
+		if (message.metadata !== undefined) {
+			Metadata.encode(message.metadata, writer.uint32(10).fork()).join()
+		}
+		if (message.serverName !== "") {
+			writer.uint32(18).string(message.serverName)
+		}
+		if (message.disabled !== false) {
+			writer.uint32(24).bool(message.disabled)
+		}
+		return writer
+	},
+
+	decode(input: BinaryReader | Uint8Array, length?: number): ToggleMcpServerRequest {
+		const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
+		let end = length === undefined ? reader.len : reader.pos + length
+		const message = createBaseToggleMcpServerRequest()
+		while (reader.pos < end) {
+			const tag = reader.uint32()
+			switch (tag >>> 3) {
+				case 1: {
+					if (tag !== 10) {
+						break
+					}
+
+					message.metadata = Metadata.decode(reader, reader.uint32())
+					continue
+				}
+				case 2: {
+					if (tag !== 18) {
+						break
+					}
+
+					message.serverName = reader.string()
+					continue
+				}
+				case 3: {
+					if (tag !== 24) {
+						break
+					}
+
+					message.disabled = reader.bool()
+					continue
+				}
+			}
+			if ((tag & 7) === 4 || tag === 0) {
+				break
+			}
+			reader.skip(tag & 7)
+		}
+		return message
+	},
+
+	fromJSON(object: any): ToggleMcpServerRequest {
+		return {
+			metadata: isSet(object.metadata) ? Metadata.fromJSON(object.metadata) : undefined,
+			serverName: isSet(object.serverName) ? globalThis.String(object.serverName) : "",
+			disabled: isSet(object.disabled) ? globalThis.Boolean(object.disabled) : false,
+		}
+	},
+
+	toJSON(message: ToggleMcpServerRequest): unknown {
+		const obj: any = {}
+		if (message.metadata !== undefined) {
+			obj.metadata = Metadata.toJSON(message.metadata)
+		}
+		if (message.serverName !== "") {
+			obj.serverName = message.serverName
+		}
+		if (message.disabled !== false) {
+			obj.disabled = message.disabled
+		}
+		return obj
+	},
+
+	create<I extends Exact<DeepPartial<ToggleMcpServerRequest>, I>>(base?: I): ToggleMcpServerRequest {
+		return ToggleMcpServerRequest.fromPartial(base ?? ({} as any))
+	},
+	fromPartial<I extends Exact<DeepPartial<ToggleMcpServerRequest>, I>>(object: I): ToggleMcpServerRequest {
+		const message = createBaseToggleMcpServerRequest()
+		message.metadata =
+			object.metadata !== undefined && object.metadata !== null ? Metadata.fromPartial(object.metadata) : undefined
+		message.serverName = object.serverName ?? ""
+		message.disabled = object.disabled ?? false
+		return message
+	},
+}
+
+function createBaseUpdateMcpTimeoutRequest(): UpdateMcpTimeoutRequest {
+	return { metadata: undefined, serverName: "", timeout: 0 }
+}
+
+export const UpdateMcpTimeoutRequest: MessageFns<UpdateMcpTimeoutRequest> = {
+	encode(message: UpdateMcpTimeoutRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
+		if (message.metadata !== undefined) {
+			Metadata.encode(message.metadata, writer.uint32(10).fork()).join()
+		}
+		if (message.serverName !== "") {
+			writer.uint32(18).string(message.serverName)
+		}
+		if (message.timeout !== 0) {
+			writer.uint32(24).int32(message.timeout)
+		}
+		return writer
+	},
+
+	decode(input: BinaryReader | Uint8Array, length?: number): UpdateMcpTimeoutRequest {
+		const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
+		let end = length === undefined ? reader.len : reader.pos + length
+		const message = createBaseUpdateMcpTimeoutRequest()
+		while (reader.pos < end) {
+			const tag = reader.uint32()
+			switch (tag >>> 3) {
+				case 1: {
+					if (tag !== 10) {
+						break
+					}
+
+					message.metadata = Metadata.decode(reader, reader.uint32())
+					continue
+				}
+				case 2: {
+					if (tag !== 18) {
+						break
+					}
+
+					message.serverName = reader.string()
+					continue
+				}
+				case 3: {
+					if (tag !== 24) {
+						break
+					}
+
+					message.timeout = reader.int32()
+					continue
+				}
+			}
+			if ((tag & 7) === 4 || tag === 0) {
+				break
+			}
+			reader.skip(tag & 7)
+		}
+		return message
+	},
+
+	fromJSON(object: any): UpdateMcpTimeoutRequest {
+		return {
+			metadata: isSet(object.metadata) ? Metadata.fromJSON(object.metadata) : undefined,
+			serverName: isSet(object.serverName) ? globalThis.String(object.serverName) : "",
+			timeout: isSet(object.timeout) ? globalThis.Number(object.timeout) : 0,
+		}
+	},
+
+	toJSON(message: UpdateMcpTimeoutRequest): unknown {
+		const obj: any = {}
+		if (message.metadata !== undefined) {
+			obj.metadata = Metadata.toJSON(message.metadata)
+		}
+		if (message.serverName !== "") {
+			obj.serverName = message.serverName
+		}
+		if (message.timeout !== 0) {
+			obj.timeout = Math.round(message.timeout)
+		}
+		return obj
+	},
+
+	create<I extends Exact<DeepPartial<UpdateMcpTimeoutRequest>, I>>(base?: I): UpdateMcpTimeoutRequest {
+		return UpdateMcpTimeoutRequest.fromPartial(base ?? ({} as any))
+	},
+	fromPartial<I extends Exact<DeepPartial<UpdateMcpTimeoutRequest>, I>>(object: I): UpdateMcpTimeoutRequest {
+		const message = createBaseUpdateMcpTimeoutRequest()
+		message.metadata =
+			object.metadata !== undefined && object.metadata !== null ? Metadata.fromPartial(object.metadata) : undefined
+		message.serverName = object.serverName ?? ""
+		message.timeout = object.timeout ?? 0
+		return message
+	},
+}
+
 function createBaseMcpTool(): McpTool {
 	return { name: "", description: undefined, inputSchema: undefined, autoApprove: undefined }
 }
@@ -622,99 +814,6 @@ export const McpServer: MessageFns<McpServer> = {
 	},
 }
 
-function createBaseToggleMcpServerRequest(): ToggleMcpServerRequest {
-	return { metadata: undefined, serverName: "", disabled: false }
-}
-
-export const ToggleMcpServerRequest: MessageFns<ToggleMcpServerRequest> = {
-	encode(message: ToggleMcpServerRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
-		if (message.metadata !== undefined) {
-			Metadata.encode(message.metadata, writer.uint32(10).fork()).join()
-		}
-		if (message.serverName !== "") {
-			writer.uint32(18).string(message.serverName)
-		}
-		if (message.disabled !== false) {
-			writer.uint32(24).bool(message.disabled)
-		}
-		return writer
-	},
-
-	decode(input: BinaryReader | Uint8Array, length?: number): ToggleMcpServerRequest {
-		const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
-		let end = length === undefined ? reader.len : reader.pos + length
-		const message = createBaseToggleMcpServerRequest()
-		while (reader.pos < end) {
-			const tag = reader.uint32()
-			switch (tag >>> 3) {
-				case 1: {
-					if (tag !== 10) {
-						break
-					}
-
-					message.metadata = Metadata.decode(reader, reader.uint32())
-					continue
-				}
-				case 2: {
-					if (tag !== 18) {
-						break
-					}
-
-					message.serverName = reader.string()
-					continue
-				}
-				case 3: {
-					if (tag !== 24) {
-						break
-					}
-
-					message.disabled = reader.bool()
-					continue
-				}
-			}
-			if ((tag & 7) === 4 || tag === 0) {
-				break
-			}
-			reader.skip(tag & 7)
-		}
-		return message
-	},
-
-	fromJSON(object: any): ToggleMcpServerRequest {
-		return {
-			metadata: isSet(object.metadata) ? Metadata.fromJSON(object.metadata) : undefined,
-			serverName: isSet(object.serverName) ? globalThis.String(object.serverName) : "",
-			disabled: isSet(object.disabled) ? globalThis.Boolean(object.disabled) : false,
-		}
-	},
-
-	toJSON(message: ToggleMcpServerRequest): unknown {
-		const obj: any = {}
-		if (message.metadata !== undefined) {
-			obj.metadata = Metadata.toJSON(message.metadata)
-		}
-		if (message.serverName !== "") {
-			obj.serverName = message.serverName
-		}
-		if (message.disabled !== false) {
-			obj.disabled = message.disabled
-		}
-		return obj
-	},
-
-	create<I extends Exact<DeepPartial<ToggleMcpServerRequest>, I>>(base?: I): ToggleMcpServerRequest {
-		return ToggleMcpServerRequest.fromPartial(base ?? ({} as any))
-	},
-	fromPartial<I extends Exact<DeepPartial<ToggleMcpServerRequest>, I>>(object: I): ToggleMcpServerRequest {
-		const message = createBaseToggleMcpServerRequest()
-		message.metadata =
-			object.metadata !== undefined && object.metadata !== null ? Metadata.fromPartial(object.metadata) : undefined
-		message.serverName = object.serverName ?? ""
-		message.disabled = object.disabled ?? false
-		return message
-	},
-}
-
 function createBaseMcpServers(): McpServers {
 	return { mcpServers: [] }
 }
@@ -790,6 +889,14 @@ export const McpServiceDefinition = {
 			responseStream: false,
 			options: {},
 		},
+		updateMcpTimeout: {
+			name: "updateMcpTimeout",
+			requestType: UpdateMcpTimeoutRequest,
+			requestStream: false,
+			responseType: McpServers,
+			responseStream: false,
+			options: {},
+		},
 	},
 } as const
 
diff --git a/webview-ui/src/components/mcp/configuration/tabs/installed/server-row/ServerRow.tsx b/webview-ui/src/components/mcp/configuration/tabs/installed/server-row/ServerRow.tsx
index d060258ad4d..91b00157889 100644
--- a/webview-ui/src/components/mcp/configuration/tabs/installed/server-row/ServerRow.tsx
+++ b/webview-ui/src/components/mcp/configuration/tabs/installed/server-row/ServerRow.tsx
@@ -18,6 +18,7 @@ import McpResourceRow from "./McpResourceRow"
 import { useExtensionState } from "@/context/ExtensionStateContext"
 import { McpServiceClient } from "@/services/grpc-client"
 import { convertProtoMcpServersToMcpServers } from "@shared/proto-conversions/mcp/mcp-server-conversion"
+import { McpServers, UpdateMcpTimeoutRequest } from "@shared/proto/mcp"
 // constant JSX.Elements
 const TimeoutOptions = [
 	{ value: "30", label: "30 seconds" },
@@ -77,11 +78,18 @@ const ServerRow = ({
 		const value = select.value
 		const num = parseInt(value)
 		setTimeoutValue(value)
-		vscode.postMessage({
-			type: "updateMcpTimeout",
+
+		McpServiceClient.updateMcpTimeout({
 			serverName: server.name,
 			timeout: num,
-		})
+		} as UpdateMcpTimeoutRequest)
+			.then((response: McpServers) => {
+				const mcpServers = convertProtoMcpServersToMcpServers(response.mcpServers)
+				setMcpServers(mcpServers)
+			})
+			.catch((error) => {
+				console.error("Error updating MCP server timeout", error)
+			})
 	}
 
 	const handleRestart = () => {


Reviews:
user: login: ellipsis-dev[bot]
body: > [!CAUTION]
> **Changes requested  :x:**
> <details>
> <summary>Reviewed everything up to 0d44c9605c1cb227326a7e32cfb945e8ecfe2e8f in <ellipsistime>2 minutes and 49 seconds</ellipsistime>. Click for details.</summary> 
> 
> - Reviewed `612` lines of code in `9` files
> - Skipped `0` files when reviewing.
> - Skipped posting `15` draft comments. View those below.
> - Modify your [settings](https://app.ellipsis.dev/cline/settings/code-review) and [rules](https://app.ellipsis.dev/cline/code-review/rules) to customize what types of comments Ellipsis leaves. And don't forget to react with :+1: or :-1: to teach Ellipsis.
> 
> <details>
> <summary>1. <code>proto/mcp.proto:18</code></summary>
> 
> - **Draft comment:** 
> The new RPC method 'updateMcpTimeout' and its request message include the required metadata field and use lowercase naming. This meets the RPC and metadata conventions.
> - **Reason this comment was not posted:**
> Confidence changes required: `0%` <= threshold `50%` None
> 
> 
> </details>
> 
> <details>
> <summary>2. <code>src/core/controller/mcp/updateMcpTimeout.ts:11</code></summary>
> 
> - **Draft comment:** 
> The updateMcpTimeout method properly validates the input and returns an updated list of MCP servers. Logging of both raw and converted server lists is clear.
> - **Reason this comment was not posted:**
> Confidence changes required: `0%` <= threshold `50%` None
> 
> 
> </details>
> 
> <details>
> <summary>3. <code>src/services/mcp/McpHub.ts:768</code></summary>
> 
> - **Draft comment:** 
> The updateServerTimeoutRPC function validates the timeout value, updates the settings file, and returns the updated servers sorted by order. This approach is consistent with existing practices.
> - **Reason this comment was not posted:**
> Confidence changes required: `0%` <= threshold `50%` None
> 
> 
> </details>
> 
> <details>
> <summary>4. <code>webview-ui/src/components/mcp/configuration/tabs/installed/server-row/ServerRow.tsx:82</code></summary>
> 
> - **Draft comment:** 
> The UI handling of timeout changes correctly calls the new updateMcpTimeout RPC via McpServiceClient and updates the state with the converted server list.
> - **Reason this comment was not posted:**
> Confidence changes required: `0%` <= threshold `50%` None
> 
> 
> </details>
> 
> <details>
> <summary>5. <code>.changeset/clean-chicken-smash.md:5</code></summary>
> 
> - **Draft comment:** 
> The changeset message is clear and descriptive.
> - **Reason this comment was not posted:**
> Confidence changes required: `0%` <= threshold `50%` None
> 
> 
> </details>
> 
> <details>
> <summary>6. <code>proto/mcp.proto:7</code></summary>
> 
> - **Draft comment:** 
> The new rpc 'updateMcpTimeout' uses lowercase as required and its message includes a metadata field. Looks good.
> - **Reason this comment was not posted:**
> Confidence changes required: `0%` <= threshold `50%` None
> 
> 
> </details>
> 
> <details>
> <summary>7. <code>src/core/controller/index.ts:682</code></summary>
> 
> - **Draft comment:** 
> Removal of the 'updateMcpTimeout' case from the controller is appropriate now that protobus handles it separately.
> - **Reason this comment was not posted:**
> Confidence changes required: `0%` <= threshold `50%` None
> 
> 
> </details>
> 
> <details>
> <summary>8. <code>src/core/controller/mcp/methods.ts:10</code></summary>
> 
> - **Draft comment:** 
> Registration of the new updateMcpTimeout method is correctly added.
> - **Reason this comment was not posted:**
> Confidence changes required: `0%` <= threshold `50%` None
> 
> 
> </details>
> 
> <details>
> <summary>9. <code>src/core/controller/mcp/updateMcpTimeout.ts:11</code></summary>
> 
> - **Draft comment:** 
> The updateMcpTimeout function validates the input and converts the MCP servers; error handling and logging are clear.
> - **Reason this comment was not posted:**
> Confidence changes required: `0%` <= threshold `50%` None
> 
> 
> </details>
> 
> <details>
> <summary>10. <code>src/services/mcp/McpHub.ts:719</code></summary>
> 
> - **Draft comment:** 
> Renamed updateServerTimeout to updateServerTimeoutRPC and updated its return type to provide the updated MCP server list. The zod validation and server reordering logic look good.
> - **Reason this comment was not posted:**
> Confidence changes required: `0%` <= threshold `50%` None
> 
> 
> </details>
> 
> <details>
> <summary>11. <code>src/shared/proto/mcp.ts:62</code></summary>
> 
> - **Draft comment:** 
> The generated message functions for UpdateMcpTimeoutRequest correctly handle encoding, decoding, and JSON conversion.
> - **Reason this comment was not posted:**
> Confidence changes required: `0%` <= threshold `50%` None
> 
> 
> </details>
> 
> <details>
> <summary>12. <code>webview-ui/src/components/mcp/configuration/tabs/installed/server-row/ServerRow.tsx:76</code></summary>
> 
> - **Draft comment:** 
> The handleTimeoutChange function correctly triggers the updateMcpTimeout RPC, updates the local UI state, and processes the response.
> - **Reason this comment was not posted:**
> Confidence changes required: `0%` <= threshold `50%` None
> 
> 
> </details>
> 
> <details>
> <summary>13. <code>webview-ui/src/components/mcp/configuration/tabs/installed/server-row/ServerRow.tsx:362</code></summary>
> 
> - **Draft comment:** 
> Overall, the UI components for MCP server rows (including timeout dropdown and action buttons) are well-implemented; consider debouncing rapid timeout changes if necessary.
> - **Reason this comment was not posted:**
> Confidence changes required: `20%` <= threshold `50%` None
> 
> 
> </details>
> 
> <details>
> <summary>14. <code>src/core/controller/index.ts:67</code></summary>
> 
> - **Draft comment:** 
> The identifier for latestAnnouncementId is set as "april-18-2025_21:15::00" which contains a double colon ("21:15::00"). Consider verifying if this is intentional or if it should be corrected to "21:15:00" for consistency.
> - **Reason this comment was not posted:**
> Comment was not on a location in the diff, so it can't be submitted as a review comment.
> 
> 
> </details>
> 
> <details>
> <summary>15. <code>src/shared/WebviewMessage.ts:35</code></summary>
> 
> - **Draft comment:** 
> There appears to be a typographical error in the union type member 'refreshRequestyModels'. Consider renaming it to 'refreshRequestModels' (or the intended spelling) for clarity.
> - **Reason this comment was not posted:**
> Comment was not on a location in the diff, so it can't be submitted as a review comment.
> 
> 
> </details>
> 
> 
> Workflow ID: <workflowid>`wflow_sshlotYZwXwDVrDs`</workflowid>
> </details>
> 
> <sup>You can customize </sup>[<img alt="Ellipsis" src="https://img.shields.io/badge/Ellipsis-blue?color=175173">](https://www.ellipsis.dev?ref=cline%2Fcline&utm_source=github&utm_medium=referral)<sup> by changing your [verbosity settings](https://app.ellipsis.dev/cline/settings/code-review), reacting with :+1: or :-1:, [replying to comments](https://docs.ellipsis.dev/features/code-review#teaching-ellipsis), or adding [code review rules](https://app.ellipsis.dev/cline/code-review/rules).</sup>
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T22:53:15Z
user: login: celestial-vault
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T22:54:30Z
user: login: canvrno
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T23:12:00Z
user: login: Garoth
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T21:11:29Z
user: login: celestial-vault
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T21:51:49Z
user: login: celestial-vault
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T21:54:22Z
user: login: Garoth
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T21:56:45Z

Review comments:
diff_hunk: @@ -377,6 +376,53 @@ export class McpHub {
 		}
 	}
 
+	async updateServerConnectionsRPC(newServers: Record<string, McpServerConfig>): Promise<void> {
path: src/services/mcp/McpHub.ts
user: login: ellipsis-dev[bot]
body: Consider modifying the existing `updateServerConnections` to make the webview notification optional instead of duplicating the entire function.

- function `updateServerConnections` ([McpHub.ts](https://github.com/cline/cline/blob/0d44c9605c1cb227326a7e32cfb945e8ecfe2e8f/src/services/mcp/McpHub.ts#L426-L471))

created_at: 2025-04-23T22:53:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -377,6 +376,53 @@ export class McpHub {
 		}
 	}
 
+	async updateServerConnectionsRPC(newServers: Record<string, McpServerConfig>): Promise<void> {
path: src/services/mcp/McpHub.ts
user: login: celestial-vault
body: This function is called in several other places in McpHub, so it's cleanest to make a new function for now.
created_at: 2025-04-23T22:54:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,27 @@
+import { convertMcpServersToProtoMcpServers } from "@/shared/proto-conversions/mcp/mcp-server-conversion"
+import { Controller } from ".."
+import { UpdateMcpTimeoutRequest, McpServers } from "../../../shared/proto/mcp"
+
+/**
+ * Updates the timeout configuration for an MCP server.
+ * @param controller - The Controller instance
+ * @param request - Contains server name and timeout value
+ * @returns Array of updated McpServer objects
+ */
+export async function updateMcpTimeout(controller: Controller, request: UpdateMcpTimeoutRequest): Promise<McpServers> {
+	try {
+		if (request.serverName && typeof request.serverName === "string" && typeof request.timeout === "number") {
+			const mcpServers = await controller.mcpHub?.updateServerTimeoutRPC(request.serverName, request.timeout)
+			console.log("mcpServers", mcpServers)
+			const convertedMcpServers = convertMcpServersToProtoMcpServers(mcpServers)
+			console.log("convertedMcpServers", convertedMcpServers)
+			return { mcpServers: convertedMcpServers }
+		} else {
+			console.error("Server name and timeout are required")
+			throw new Error("Server name and timeout are required")
path: src/core/controller/mcp/updateMcpTimeout.ts
user: login: Garoth
body: If this throws the error here, wouldn't it be caught immediately after by the catch?
created_at: 2025-04-24T21:08:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -377,6 +376,53 @@ export class McpHub {
 		}
 	}
 
+	async updateServerConnectionsRPC(newServers: Record<string, McpServerConfig>): Promise<void> {
+		this.isConnecting = true
+		this.removeAllFileWatchers()
+		const currentNames = new Set(this.connections.map((conn) => conn.server.name))
+		const newNames = new Set(Object.keys(newServers))
+
+		// Delete removed servers
+		for (const name of currentNames) {
+			if (!newNames.has(name)) {
+				await this.deleteConnection(name)
+				console.log(`Deleted MCP server: ${name}`)
+			}
+		}
+
+		// Update or add servers
+		for (const [name, config] of Object.entries(newServers)) {
+			const currentConnection = this.connections.find((conn) => conn.server.name === name)
+
+			if (!currentConnection) {
+				// New server
+				try {
+					if (config.transportType === "stdio") {
+						this.setupFileWatcher(name, config)
+					}
+					await this.connectToServer(name, config)
+				} catch (error) {
+					console.error(`Failed to connect to new MCP server ${name}:`, error)
+				}
+			} else if (!deepEqual(JSON.parse(currentConnection.server.config), config)) {
+				// Existing server with changed config
+				try {
+					if (config.transportType === "stdio") {
+						this.setupFileWatcher(name, config)
+					}
+					await this.deleteConnection(name)
+					await this.connectToServer(name, config)
+					console.log(`Reconnected MCP server with updated config: ${name}`)
+				} catch (error) {
+					console.error(`Failed to reconnect MCP server ${name}:`, error)
+				}
+			}
+			// If server exists with same config, do nothing
+		}
+
+		this.isConnecting = false
+	}
+
 	async updateServerConnections(newServers: Record<string, McpServerConfig>): Promise<void> {
path: src/services/mcp/McpHub.ts
user: login: Garoth
body: Did you say that this function is no longer used? Maybe we do delete it so we don't forget
created_at: 2025-04-24T21:10:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -377,6 +376,53 @@ export class McpHub {
 		}
 	}
 
+	async updateServerConnectionsRPC(newServers: Record<string, McpServerConfig>): Promise<void> {
+		this.isConnecting = true
+		this.removeAllFileWatchers()
+		const currentNames = new Set(this.connections.map((conn) => conn.server.name))
+		const newNames = new Set(Object.keys(newServers))
+
+		// Delete removed servers
+		for (const name of currentNames) {
+			if (!newNames.has(name)) {
+				await this.deleteConnection(name)
+				console.log(`Deleted MCP server: ${name}`)
+			}
+		}
+
+		// Update or add servers
+		for (const [name, config] of Object.entries(newServers)) {
+			const currentConnection = this.connections.find((conn) => conn.server.name === name)
+
+			if (!currentConnection) {
+				// New server
+				try {
+					if (config.transportType === "stdio") {
+						this.setupFileWatcher(name, config)
+					}
+					await this.connectToServer(name, config)
+				} catch (error) {
+					console.error(`Failed to connect to new MCP server ${name}:`, error)
+				}
+			} else if (!deepEqual(JSON.parse(currentConnection.server.config), config)) {
+				// Existing server with changed config
+				try {
+					if (config.transportType === "stdio") {
+						this.setupFileWatcher(name, config)
+					}
+					await this.deleteConnection(name)
+					await this.connectToServer(name, config)
+					console.log(`Reconnected MCP server with updated config: ${name}`)
+				} catch (error) {
+					console.error(`Failed to reconnect MCP server ${name}:`, error)
+				}
+			}
+			// If server exists with same config, do nothing
+		}
+
+		this.isConnecting = false
+	}
+
 	async updateServerConnections(newServers: Record<string, McpServerConfig>): Promise<void> {
path: src/services/mcp/McpHub.ts
user: login: celestial-vault
body: This one is used in several places still, yeah
created_at: 2025-04-24T21:51:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,27 @@
+import { convertMcpServersToProtoMcpServers } from "@/shared/proto-conversions/mcp/mcp-server-conversion"
+import { Controller } from ".."
+import { UpdateMcpTimeoutRequest, McpServers } from "../../../shared/proto/mcp"
+
+/**
+ * Updates the timeout configuration for an MCP server.
+ * @param controller - The Controller instance
+ * @param request - Contains server name and timeout value
+ * @returns Array of updated McpServer objects
+ */
+export async function updateMcpTimeout(controller: Controller, request: UpdateMcpTimeoutRequest): Promise<McpServers> {
+	try {
+		if (request.serverName && typeof request.serverName === "string" && typeof request.timeout === "number") {
+			const mcpServers = await controller.mcpHub?.updateServerTimeoutRPC(request.serverName, request.timeout)
+			console.log("mcpServers", mcpServers)
+			const convertedMcpServers = convertMcpServersToProtoMcpServers(mcpServers)
+			console.log("convertedMcpServers", convertedMcpServers)
+			return { mcpServers: convertedMcpServers }
+		} else {
+			console.error("Server name and timeout are required")
+			throw new Error("Server name and timeout are required")
path: src/core/controller/mcp/updateMcpTimeout.ts
user: login: celestial-vault
body: Yes, but throwing the error seemed the only way to reliably stop at this point and make it clear that that's the issue, given that this in within an else block. It was more for the logging in my mind.
created_at: 2025-04-24T21:54:22Z
author_association: CONTRIBUTOR

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-04-23T22:50:25Z
body: ###  🦋  Changeset detected

Latest commit: 0d44c9605c1cb227326a7e32cfb945e8ecfe2e8f

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 1 package</summary>

  | Name       | Type  |
| ---------- | ----- |
| claude-dev | Minor |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/cline/cline/new/protobus-updateMcpTimeout?filename=.changeset/light-mails-marry.md&value=---%0A%22claude-dev%22%3A%20patch%0A---%0A%0AProtobus%3A%20updateMcpTimeout%20message%0A)


author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-23T22:50:49Z
body: 🔗 Linear issue created: https://linear.app/cline-bot/issue/ENG-599/[github]-protobus-updatemcptimeout-message
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-23T22:53:52Z
body: <!-- COVERAGE_REPORT -->
## Coverage Report

### Extension Coverage

Base branch: 45%

PR branch: 45%

⚠️ **Warning: Coverage decreased by 0.01%**

Consider adding tests to cover your changes.

### Webview Coverage

Base branch: 11%

PR branch: 11%

⚠️ **Warning: Coverage decreased by 0.01%**

Consider adding tests to cover your changes.

### Overall Assessment

⚠️ **Test coverage has decreased in this PR**

Please consider adding tests to maintain or improve coverage.



<sub>Last updated: 2025-04-23T22:53:51.581131</sub>
author_association: CONTRIBUTOR