mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-09 01:49:49 -10:00
fix(mcp): give npx/uvx servers time for their first download
QA friction audit (codewhale-ops dd2c253d row 9): /mcp add recommended playwright wrote config, but the first connect failed because the cold npx @playwright/mcp download overran the 30 s connect budget. The error read "MCP SDK request failed before an admitted reply: mcp/open timed out after 30s; the host was told to cancel it". - A stdio server launched through npx, pnpx, bunx or uvx with no per-server connect_timeout now gets max(global, 120 s). An explicit per-server value still wins. - When initialize fails because the connect budget ran out, the error now leads with "MCP server '<name>' didn't start within Ns; retry with /mcp restart, or raise its connect_timeout", with the transport detail still in the chain. - docs/MCP.md states the floor. Verified on the shared tree (same code, pre-S19 layout): scripts/dev-test.sh tui mcp 584/584 passed. Ported to the S19 layout (crates/mcp/src/config.rs) by text; rustfmt clean. The live npx cold start was not re-run here. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
dc53e0ad47
commit
b8afed5f27
3 files changed
+40
-4
No files matched your search
@@ -203,13 +203,38 @@ pub struct McpServerOAuthConfig {
|
||||
pub client_metadata_url: Option<String>,
|
||||
}
|
||||
|
||||
const PACKAGE_RUNNER_CONNECT_TIMEOUT_SECS: u64 = 120;
|
||||
|
||||
fn default_enabled() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
impl McpServerConfig {
|
||||
pub fn effective_connect_timeout(&self, global: &McpTimeouts) -> u64 {
|
||||
self.connect_timeout.unwrap_or(global.connect_timeout)
|
||||
self.connect_timeout.unwrap_or_else(|| {
|
||||
if self.launches_package_runner() {
|
||||
global
|
||||
.connect_timeout
|
||||
.max(PACKAGE_RUNNER_CONNECT_TIMEOUT_SECS)
|
||||
} else {
|
||||
global.connect_timeout
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn launches_package_runner(&self) -> bool {
|
||||
self.url.is_none()
|
||||
&& self
|
||||
.command
|
||||
.as_deref()
|
||||
.and_then(|command| std::path::Path::new(command).file_stem())
|
||||
.and_then(|stem| stem.to_str())
|
||||
.is_some_and(|stem| {
|
||||
matches!(
|
||||
stem.to_ascii_lowercase().as_str(),
|
||||
"npx" | "pnpx" | "bunx" | "uvx"
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn effective_execute_timeout(&self, global: &McpTimeouts) -> u64 {
|
||||
|
||||
+13
-2
@@ -2077,9 +2077,20 @@ impl McpConnection {
|
||||
}
|
||||
|
||||
// Initialize with timeout
|
||||
tokio::time::timeout(Duration::from_secs(connect_timeout_secs), conn.initialize())
|
||||
let connect_deadline = Duration::from_secs(connect_timeout_secs);
|
||||
let initialize_started = std::time::Instant::now();
|
||||
let initialized = tokio::time::timeout(connect_deadline, conn.initialize())
|
||||
.await
|
||||
.with_context(|| format!("MCP server '{name}' initialization timed out"))??;
|
||||
.map_err(anyhow::Error::from)
|
||||
.and_then(|result| result);
|
||||
if let Err(error) = initialized {
|
||||
if initialize_started.elapsed() >= connect_deadline {
|
||||
return Err(error.context(format!(
|
||||
"MCP server '{name}' didn't start within {connect_timeout_secs}s; retry with /mcp restart, or raise its connect_timeout"
|
||||
)));
|
||||
}
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
conn.discover_all()
|
||||
.await
|
||||
|
||||
+1
-1
@@ -719,7 +719,7 @@ Per-server settings:
|
||||
- `args` (array of strings, optional)
|
||||
- `env` (object, optional)
|
||||
- `connect_timeout`, `execute_timeout`, `read_timeout` (seconds, optional). A per-server value overrides the global `timeouts` block, and each budget is independent of the others:
|
||||
- `connect_timeout` (default 30) covers spawn, `initialize` and the first `tools/list`, so a cold `uvx`/`npx` package download counts against it.
|
||||
- `connect_timeout` (default 30) covers spawn, `initialize` and the first `tools/list`, so a cold `uvx`/`npx` package download counts against it. A server launched through `npx`, `pnpx`, `bunx` or `uvx` without its own `connect_timeout` gets at least 120 seconds for that first download.
|
||||
- `execute_timeout` (default 1800) is the budget for each `tools/call` and `prompts/get`. An explicit shorter value is respected, and `read_timeout` never cuts a running tool short.
|
||||
- `read_timeout` (default 120) bounds each reply wait during the handshake and tool discovery, and is the budget for `resources/read`.
|
||||
- A request that runs out of budget fails with a timeout. The connection is kept, and a reply that arrives later is discarded instead of being handed to another call. Interrupting the turn stops the wait at once, but Codewhale does not send `notifications/cancelled` yet, so a server that handles one request at a time finishes the abandoned call before it answers the next one.
|
||||
|
||||
Reference in new issue
Block a user