mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
Everyone Gets a Timeout (#1889)
* timeouts for individual servers * changeset * remove logger * use const and descriptive function for time settings
This commit is contained in:
parent
0f1240063c
commit
87670a37b8
8 changed files with 135 additions and 31 deletions
5
.changeset/dull-planets-battle.md
Normal file
5
.changeset/dull-planets-battle.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Added timeout configuration for individual MCP servers.
|
||||
|
|
@ -759,8 +759,9 @@ IMPORTANT: Regardless of what else you see in the MCP settings file, you must de
|
|||
"args": ["/path/to/weather-server/build/index.js"],
|
||||
"env": {
|
||||
"OPENWEATHER_API_KEY": "user-provided-api-key"
|
||||
}
|
||||
},
|
||||
},
|
||||
"timeout": 60, // 60 seconds should be set as the default timeout for all MCP servers
|
||||
}
|
||||
}
|
||||
}
|
||||
\`\`\`
|
||||
|
|
|
|||
|
|
@ -821,6 +821,16 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
}
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import * as vscode from "vscode"
|
|||
import { z } from "zod"
|
||||
import { ClineProvider, GlobalFileNames } from "../../core/webview/ClineProvider"
|
||||
import {
|
||||
DEFAULT_MCP_TIMEOUT_SECONDS,
|
||||
McpMode,
|
||||
McpResource,
|
||||
McpResourceResponse,
|
||||
|
|
@ -26,7 +27,7 @@ import {
|
|||
} from "../../shared/mcp"
|
||||
import { fileExistsAtPath } from "../../utils/fs"
|
||||
import { arePathsEqual } from "../../utils/path"
|
||||
|
||||
import { secondsToMs } from "../../utils/time"
|
||||
export type McpConnection = {
|
||||
server: McpServer
|
||||
client: Client
|
||||
|
|
@ -42,6 +43,7 @@ const StdioConfigSchema = z.object({
|
|||
env: z.record(z.string()).optional(),
|
||||
autoApprove: AutoApproveSchema.optional(),
|
||||
disabled: z.boolean().optional(),
|
||||
timeout: z.number().min(1).max(3600).optional().default(DEFAULT_MCP_TIMEOUT_SECONDS),
|
||||
})
|
||||
|
||||
const McpSettingsSchema = z.object({
|
||||
|
|
@ -242,28 +244,6 @@ export class McpHub {
|
|||
}
|
||||
transport.start = async () => {} // No-op now, .connect() won't fail
|
||||
|
||||
// // Set up notification handlers
|
||||
// client.setNotificationHandler(
|
||||
// // @ts-ignore-next-line
|
||||
// { method: "notifications/tools/list_changed" },
|
||||
// async () => {
|
||||
// console.log(`Tools changed for server: ${name}`)
|
||||
// connection.server.tools = await this.fetchTools(name)
|
||||
// await this.notifyWebviewOfServerChanges()
|
||||
// },
|
||||
// )
|
||||
|
||||
// client.setNotificationHandler(
|
||||
// // @ts-ignore-next-line
|
||||
// { method: "notifications/resources/list_changed" },
|
||||
// async () => {
|
||||
// console.log(`Resources changed for server: ${name}`)
|
||||
// connection.server.resources = await this.fetchResources(name)
|
||||
// connection.server.resourceTemplates = await this.fetchResourceTemplates(name)
|
||||
// await this.notifyWebviewOfServerChanges()
|
||||
// },
|
||||
// )
|
||||
|
||||
// Connect
|
||||
await client.connect(transport)
|
||||
connection.server.status = "connected"
|
||||
|
|
@ -343,10 +323,6 @@ export class McpHub {
|
|||
const connection = this.connections.find((conn) => conn.server.name === name)
|
||||
if (connection) {
|
||||
try {
|
||||
// connection.client.removeNotificationHandler("notifications/tools/list_changed")
|
||||
// connection.client.removeNotificationHandler("notifications/resources/list_changed")
|
||||
// connection.client.removeNotificationHandler("notifications/stderr")
|
||||
// connection.client.removeNotificationHandler("notifications/stderr")
|
||||
await connection.transport.close()
|
||||
await connection.client.close()
|
||||
} catch (error) {
|
||||
|
|
@ -563,6 +539,7 @@ export class McpHub {
|
|||
if (connection.server.disabled) {
|
||||
throw new Error(`Server "${serverName}" is disabled`)
|
||||
}
|
||||
|
||||
return await connection.client.request(
|
||||
{
|
||||
method: "resources/read",
|
||||
|
|
@ -586,6 +563,17 @@ export class McpHub {
|
|||
throw new Error(`Server "${serverName}" is disabled and cannot be used`)
|
||||
}
|
||||
|
||||
let timeout = secondsToMs(DEFAULT_MCP_TIMEOUT_SECONDS) // sdk expects ms
|
||||
|
||||
try {
|
||||
const config = JSON.parse(connection.server.config)
|
||||
const parsedConfig = StdioConfigSchema.parse(config)
|
||||
timeout = secondsToMs(parsedConfig.timeout)
|
||||
} catch (error) {
|
||||
console.error(`Failed to parse timeout configuration for server ${serverName}: ${error}`)
|
||||
// Continue with default timeout
|
||||
}
|
||||
|
||||
return await connection.client.request(
|
||||
{
|
||||
method: "tools/call",
|
||||
|
|
@ -595,6 +583,9 @@ export class McpHub {
|
|||
},
|
||||
},
|
||||
CallToolResultSchema,
|
||||
{
|
||||
timeout,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -663,6 +654,47 @@ export class McpHub {
|
|||
}
|
||||
}
|
||||
|
||||
public async updateServerTimeout(serverName: string, timeout: number): Promise<void> {
|
||||
try {
|
||||
// Validate timeout against schema
|
||||
const setConfigResult = StdioConfigSchema.shape.timeout.safeParse(timeout)
|
||||
if (!setConfigResult.success) {
|
||||
throw new Error(`Invalid timeout value: ${timeout}. Must be between 1 and 3600 seconds.`)
|
||||
}
|
||||
|
||||
const settingsPath = await this.getMcpSettingsFilePath()
|
||||
const content = await fs.readFile(settingsPath, "utf-8")
|
||||
const config = JSON.parse(content)
|
||||
|
||||
if (!config.mcpServers?.[serverName]) {
|
||||
throw new Error(`Server "${serverName}" not found in settings`)
|
||||
}
|
||||
|
||||
// Update the timeout in the config
|
||||
config.mcpServers[serverName] = {
|
||||
...config.mcpServers[serverName],
|
||||
timeout,
|
||||
}
|
||||
|
||||
// Write updated config back to file
|
||||
await fs.writeFile(settingsPath, JSON.stringify(config, null, 2))
|
||||
|
||||
// Update server connections to apply the new timeout
|
||||
await this.updateServerConnections(config.mcpServers)
|
||||
|
||||
vscode.window.showInformationMessage(`Updated timeout to ${timeout} seconds`)
|
||||
} catch (error) {
|
||||
console.error("Failed to update server timeout:", error)
|
||||
if (error instanceof Error) {
|
||||
console.error("Error details:", error.message, error.stack)
|
||||
}
|
||||
vscode.window.showErrorMessage(
|
||||
`Failed to update server timeout: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async dispose(): Promise<void> {
|
||||
this.removeAllFileWatchers()
|
||||
for (const connection of this.connections) {
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ export interface WebviewMessage {
|
|||
| "searchCommits"
|
||||
| "showMcpView"
|
||||
| "fetchLatestMcpServersFromHub"
|
||||
| "updateMcpTimeout"
|
||||
// | "relaunchChromeDebugMode"
|
||||
text?: string
|
||||
disabled?: boolean
|
||||
|
|
@ -63,6 +64,7 @@ export interface WebviewMessage {
|
|||
chatSettings?: ChatSettings
|
||||
chatContent?: ChatContent
|
||||
mcpId?: string
|
||||
timeout?: number // For updateMcpTimeout
|
||||
|
||||
// For toggleToolAutoApprove
|
||||
serverName?: string
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
export const DEFAULT_MCP_TIMEOUT_SECONDS = 60 // matches Anthropic's default timeout in their MCP SDK
|
||||
|
||||
export type McpMode = "full" | "server-use-only" | "off"
|
||||
|
||||
export type McpServer = {
|
||||
|
|
@ -9,6 +11,7 @@ export type McpServer = {
|
|||
resources?: McpResource[]
|
||||
resourceTemplates?: McpResourceTemplate[]
|
||||
disabled?: boolean
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
export type McpTool = {
|
||||
|
|
|
|||
3
src/utils/time.ts
Normal file
3
src/utils/time.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export function secondsToMs(seconds: number): number {
|
||||
return seconds * 1000
|
||||
}
|
||||
|
|
@ -1,7 +1,15 @@
|
|||
import { VSCodeButton, VSCodeLink, VSCodePanels, VSCodePanelTab, VSCodePanelView } from "@vscode/webview-ui-toolkit/react"
|
||||
import {
|
||||
VSCodeButton,
|
||||
VSCodeLink,
|
||||
VSCodePanels,
|
||||
VSCodePanelTab,
|
||||
VSCodePanelView,
|
||||
VSCodeDropdown,
|
||||
VSCodeOption,
|
||||
} from "@vscode/webview-ui-toolkit/react"
|
||||
import { useEffect, useState } from "react"
|
||||
import styled from "styled-components"
|
||||
import { McpServer } from "../../../../src/shared/mcp"
|
||||
import { DEFAULT_MCP_TIMEOUT_SECONDS, McpServer } from "../../../../src/shared/mcp"
|
||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||
import { getMcpServerDisplayName } from "../../utils/mcp"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
|
|
@ -210,6 +218,36 @@ const ServerRow = ({ server }: { server: McpServer }) => {
|
|||
}
|
||||
}
|
||||
|
||||
const [timeout, setTimeout] = useState<string>(() => {
|
||||
try {
|
||||
const config = JSON.parse(server.config)
|
||||
return config.timeout?.toString() || DEFAULT_MCP_TIMEOUT_SECONDS.toString()
|
||||
} catch {
|
||||
return DEFAULT_MCP_TIMEOUT_SECONDS.toString()
|
||||
}
|
||||
})
|
||||
|
||||
const timeoutOptions = [
|
||||
{ value: "30", label: "30 seconds" },
|
||||
{ value: "60", label: "1 minute" },
|
||||
{ value: "300", label: "5 minutes" },
|
||||
{ value: "600", label: "10 minutes" },
|
||||
{ value: "1800", label: "30 minutes" },
|
||||
{ value: "3600", label: "1 hour" },
|
||||
]
|
||||
|
||||
const handleTimeoutChange = (e: any) => {
|
||||
const select = e.target as HTMLSelectElement
|
||||
const value = select.value
|
||||
const num = parseInt(value)
|
||||
setTimeout(value)
|
||||
vscode.postMessage({
|
||||
type: "updateMcpTimeout",
|
||||
serverName: server.name,
|
||||
timeout: num,
|
||||
})
|
||||
}
|
||||
|
||||
const handleRestart = () => {
|
||||
vscode.postMessage({
|
||||
type: "restartMcpServer",
|
||||
|
|
@ -410,6 +448,16 @@ const ServerRow = ({ server }: { server: McpServer }) => {
|
|||
</VSCodePanelView>
|
||||
</VSCodePanels>
|
||||
|
||||
<div style={{ margin: "10px 7px" }}>
|
||||
<label style={{ display: "block", marginBottom: "4px", fontSize: "13px" }}>Request Timeout</label>
|
||||
<VSCodeDropdown style={{ width: "100%" }} value={timeout} onChange={handleTimeoutChange}>
|
||||
{timeoutOptions.map((option) => (
|
||||
<VSCodeOption key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</VSCodeOption>
|
||||
))}
|
||||
</VSCodeDropdown>
|
||||
</div>
|
||||
<VSCodeButton
|
||||
appearance="secondary"
|
||||
onClick={handleRestart}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue