Skip to content

Commit 87670a3

Browse files
Everyone Gets a Timeout (RooCodeInc#1889)
* timeouts for individual servers * changeset * remove logger * use const and descriptive function for time settings
1 parent 0f12400 commit 87670a3

File tree

8 files changed

+135
-31
lines changed

8 files changed

+135
-31
lines changed

.changeset/dull-planets-battle.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"claude-dev": minor
3+
---
4+
5+
Added timeout configuration for individual MCP servers.

src/core/prompts/system.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -759,8 +759,9 @@ IMPORTANT: Regardless of what else you see in the MCP settings file, you must de
759759
"args": ["/path/to/weather-server/build/index.js"],
760760
"env": {
761761
"OPENWEATHER_API_KEY": "user-provided-api-key"
762-
}
763-
},
762+
},
763+
"timeout": 60, // 60 seconds should be set as the default timeout for all MCP servers
764+
}
764765
}
765766
}
766767
\`\`\`

src/core/webview/ClineProvider.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -821,6 +821,16 @@ export class ClineProvider implements vscode.WebviewViewProvider {
821821
}
822822
break
823823
}
824+
case "updateMcpTimeout": {
825+
try {
826+
if (message.serverName && message.timeout) {
827+
await this.mcpHub?.updateServerTimeout(message.serverName, message.timeout)
828+
}
829+
} catch (error) {
830+
console.error(`Failed to update timeout for server ${message.serverName}:`, error)
831+
}
832+
break
833+
}
824834
case "openExtensionSettings": {
825835
const settingsFilter = message.text || ""
826836
await vscode.commands.executeCommand(

src/services/mcp/McpHub.ts

Lines changed: 59 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import * as vscode from "vscode"
1616
import { z } from "zod"
1717
import { ClineProvider, GlobalFileNames } from "../../core/webview/ClineProvider"
1818
import {
19+
DEFAULT_MCP_TIMEOUT_SECONDS,
1920
McpMode,
2021
McpResource,
2122
McpResourceResponse,
@@ -26,7 +27,7 @@ import {
2627
} from "../../shared/mcp"
2728
import { fileExistsAtPath } from "../../utils/fs"
2829
import { arePathsEqual } from "../../utils/path"
29-
30+
import { secondsToMs } from "../../utils/time"
3031
export type McpConnection = {
3132
server: McpServer
3233
client: Client
@@ -42,6 +43,7 @@ const StdioConfigSchema = z.object({
4243
env: z.record(z.string()).optional(),
4344
autoApprove: AutoApproveSchema.optional(),
4445
disabled: z.boolean().optional(),
46+
timeout: z.number().min(1).max(3600).optional().default(DEFAULT_MCP_TIMEOUT_SECONDS),
4547
})
4648

4749
const McpSettingsSchema = z.object({
@@ -242,28 +244,6 @@ export class McpHub {
242244
}
243245
transport.start = async () => {} // No-op now, .connect() won't fail
244246

245-
// // Set up notification handlers
246-
// client.setNotificationHandler(
247-
// // @ts-ignore-next-line
248-
// { method: "notifications/tools/list_changed" },
249-
// async () => {
250-
// console.log(`Tools changed for server: ${name}`)
251-
// connection.server.tools = await this.fetchTools(name)
252-
// await this.notifyWebviewOfServerChanges()
253-
// },
254-
// )
255-
256-
// client.setNotificationHandler(
257-
// // @ts-ignore-next-line
258-
// { method: "notifications/resources/list_changed" },
259-
// async () => {
260-
// console.log(`Resources changed for server: ${name}`)
261-
// connection.server.resources = await this.fetchResources(name)
262-
// connection.server.resourceTemplates = await this.fetchResourceTemplates(name)
263-
// await this.notifyWebviewOfServerChanges()
264-
// },
265-
// )
266-
267247
// Connect
268248
await client.connect(transport)
269249
connection.server.status = "connected"
@@ -343,10 +323,6 @@ export class McpHub {
343323
const connection = this.connections.find((conn) => conn.server.name === name)
344324
if (connection) {
345325
try {
346-
// connection.client.removeNotificationHandler("notifications/tools/list_changed")
347-
// connection.client.removeNotificationHandler("notifications/resources/list_changed")
348-
// connection.client.removeNotificationHandler("notifications/stderr")
349-
// connection.client.removeNotificationHandler("notifications/stderr")
350326
await connection.transport.close()
351327
await connection.client.close()
352328
} catch (error) {
@@ -563,6 +539,7 @@ export class McpHub {
563539
if (connection.server.disabled) {
564540
throw new Error(`Server "${serverName}" is disabled`)
565541
}
542+
566543
return await connection.client.request(
567544
{
568545
method: "resources/read",
@@ -586,6 +563,17 @@ export class McpHub {
586563
throw new Error(`Server "${serverName}" is disabled and cannot be used`)
587564
}
588565

566+
let timeout = secondsToMs(DEFAULT_MCP_TIMEOUT_SECONDS) // sdk expects ms
567+
568+
try {
569+
const config = JSON.parse(connection.server.config)
570+
const parsedConfig = StdioConfigSchema.parse(config)
571+
timeout = secondsToMs(parsedConfig.timeout)
572+
} catch (error) {
573+
console.error(`Failed to parse timeout configuration for server ${serverName}: ${error}`)
574+
// Continue with default timeout
575+
}
576+
589577
return await connection.client.request(
590578
{
591579
method: "tools/call",
@@ -595,6 +583,9 @@ export class McpHub {
595583
},
596584
},
597585
CallToolResultSchema,
586+
{
587+
timeout,
588+
},
598589
)
599590
}
600591

@@ -663,6 +654,47 @@ export class McpHub {
663654
}
664655
}
665656

657+
public async updateServerTimeout(serverName: string, timeout: number): Promise<void> {
658+
try {
659+
// Validate timeout against schema
660+
const setConfigResult = StdioConfigSchema.shape.timeout.safeParse(timeout)
661+
if (!setConfigResult.success) {
662+
throw new Error(`Invalid timeout value: ${timeout}. Must be between 1 and 3600 seconds.`)
663+
}
664+
665+
const settingsPath = await this.getMcpSettingsFilePath()
666+
const content = await fs.readFile(settingsPath, "utf-8")
667+
const config = JSON.parse(content)
668+
669+
if (!config.mcpServers?.[serverName]) {
670+
throw new Error(`Server "${serverName}" not found in settings`)
671+
}
672+
673+
// Update the timeout in the config
674+
config.mcpServers[serverName] = {
675+
...config.mcpServers[serverName],
676+
timeout,
677+
}
678+
679+
// Write updated config back to file
680+
await fs.writeFile(settingsPath, JSON.stringify(config, null, 2))
681+
682+
// Update server connections to apply the new timeout
683+
await this.updateServerConnections(config.mcpServers)
684+
685+
vscode.window.showInformationMessage(`Updated timeout to ${timeout} seconds`)
686+
} catch (error) {
687+
console.error("Failed to update server timeout:", error)
688+
if (error instanceof Error) {
689+
console.error("Error details:", error.message, error.stack)
690+
}
691+
vscode.window.showErrorMessage(
692+
`Failed to update server timeout: ${error instanceof Error ? error.message : String(error)}`,
693+
)
694+
throw error
695+
}
696+
}
697+
666698
async dispose(): Promise<void> {
667699
this.removeAllFileWatchers()
668700
for (const connection of this.connections) {

src/shared/WebviewMessage.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export interface WebviewMessage {
5050
| "searchCommits"
5151
| "showMcpView"
5252
| "fetchLatestMcpServersFromHub"
53+
| "updateMcpTimeout"
5354
// | "relaunchChromeDebugMode"
5455
text?: string
5556
disabled?: boolean
@@ -63,6 +64,7 @@ export interface WebviewMessage {
6364
chatSettings?: ChatSettings
6465
chatContent?: ChatContent
6566
mcpId?: string
67+
timeout?: number // For updateMcpTimeout
6668

6769
// For toggleToolAutoApprove
6870
serverName?: string

src/shared/mcp.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
export const DEFAULT_MCP_TIMEOUT_SECONDS = 60 // matches Anthropic's default timeout in their MCP SDK
2+
13
export type McpMode = "full" | "server-use-only" | "off"
24

35
export type McpServer = {
@@ -9,6 +11,7 @@ export type McpServer = {
911
resources?: McpResource[]
1012
resourceTemplates?: McpResourceTemplate[]
1113
disabled?: boolean
14+
timeout?: number
1215
}
1316

1417
export type McpTool = {

src/utils/time.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function secondsToMs(seconds: number): number {
2+
return seconds * 1000
3+
}

webview-ui/src/components/mcp/McpView.tsx

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
1-
import { VSCodeButton, VSCodeLink, VSCodePanels, VSCodePanelTab, VSCodePanelView } from "@vscode/webview-ui-toolkit/react"
1+
import {
2+
VSCodeButton,
3+
VSCodeLink,
4+
VSCodePanels,
5+
VSCodePanelTab,
6+
VSCodePanelView,
7+
VSCodeDropdown,
8+
VSCodeOption,
9+
} from "@vscode/webview-ui-toolkit/react"
210
import { useEffect, useState } from "react"
311
import styled from "styled-components"
4-
import { McpServer } from "../../../../src/shared/mcp"
12+
import { DEFAULT_MCP_TIMEOUT_SECONDS, McpServer } from "../../../../src/shared/mcp"
513
import { useExtensionState } from "../../context/ExtensionStateContext"
614
import { getMcpServerDisplayName } from "../../utils/mcp"
715
import { vscode } from "../../utils/vscode"
@@ -210,6 +218,36 @@ const ServerRow = ({ server }: { server: McpServer }) => {
210218
}
211219
}
212220

221+
const [timeout, setTimeout] = useState<string>(() => {
222+
try {
223+
const config = JSON.parse(server.config)
224+
return config.timeout?.toString() || DEFAULT_MCP_TIMEOUT_SECONDS.toString()
225+
} catch {
226+
return DEFAULT_MCP_TIMEOUT_SECONDS.toString()
227+
}
228+
})
229+
230+
const timeoutOptions = [
231+
{ value: "30", label: "30 seconds" },
232+
{ value: "60", label: "1 minute" },
233+
{ value: "300", label: "5 minutes" },
234+
{ value: "600", label: "10 minutes" },
235+
{ value: "1800", label: "30 minutes" },
236+
{ value: "3600", label: "1 hour" },
237+
]
238+
239+
const handleTimeoutChange = (e: any) => {
240+
const select = e.target as HTMLSelectElement
241+
const value = select.value
242+
const num = parseInt(value)
243+
setTimeout(value)
244+
vscode.postMessage({
245+
type: "updateMcpTimeout",
246+
serverName: server.name,
247+
timeout: num,
248+
})
249+
}
250+
213251
const handleRestart = () => {
214252
vscode.postMessage({
215253
type: "restartMcpServer",
@@ -410,6 +448,16 @@ const ServerRow = ({ server }: { server: McpServer }) => {
410448
</VSCodePanelView>
411449
</VSCodePanels>
412450

451+
<div style={{ margin: "10px 7px" }}>
452+
<label style={{ display: "block", marginBottom: "4px", fontSize: "13px" }}>Request Timeout</label>
453+
<VSCodeDropdown style={{ width: "100%" }} value={timeout} onChange={handleTimeoutChange}>
454+
{timeoutOptions.map((option) => (
455+
<VSCodeOption key={option.value} value={option.value}>
456+
{option.label}
457+
</VSCodeOption>
458+
))}
459+
</VSCodeDropdown>
460+
</div>
413461
<VSCodeButton
414462
appearance="secondary"
415463
onClick={handleRestart}

0 commit comments

Comments
 (0)