A tool that calls fetch() with no timeout will hang indefinitely if the remote service is unresponsive — the MCP client freezes waiting for a result that never arrives. A tool that executes shell commands with no timeout can be kept running permanently by a hung subprocess, blocking the server for all clients. CWE-400 (uncontrolled resource consumption) is the mechanism; ISO 25010 performance-efficiency is the quality attribute violated. In agentic workflows where the AI chains multiple tool calls, a single hung tool halts the entire session.
Medium because missing timeouts cause hangs under degraded network or process conditions rather than immediate failures, but the impact is a frozen session with no recovery path short of restarting the server.
Add an AbortController timeout to every fetch() call and a timeout option to every shell command in tool handlers.
// src/tools/fetch.ts — 30-second timeout
server.tool('fetch_url', ..., async ({ url }) => {
const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), 30_000)
try {
const res = await fetch(url, { signal: controller.signal })
clearTimeout(timer)
return { content: [{ type: 'text', text: await res.text() }] }
} catch (error) {
clearTimeout(timer)
const msg = (error as Error).name === 'AbortError'
? `Request timed out after 30s: ${url}`
: `Fetch failed: ${(error as Error).message}`
return { content: [{ type: 'text', text: msg }], isError: true }
}
})
ID: mcp-server.error-resilience.timeout-handling
Severity: medium
What to look for: Count all long-running operations in tool handlers. Enumerate which have timeout limits vs. which could run indefinitely. Identify tools that perform potentially long-running operations: HTTP requests, database queries, file system operations on large datasets, shell command execution, API calls to external services. Check whether these operations have timeouts configured. Check for AbortController/AbortSignal usage, setTimeout wrappers, or timeout options on HTTP clients (fetch timeout, axios timeout). Check that timeout errors are caught and returned as isError: true with a descriptive message.
Pass criteria: Tools that make network requests, run queries, or execute commands have explicit timeouts. Timeout errors produce readable error messages (not raw timeout exceptions). At least 90% of tool handlers with external calls must have timeout limits of no more than 30 seconds.
Fail criteria: Network-calling tools have no timeout (will hang indefinitely if the remote service is unresponsive), or timeout errors are not caught (crash the server).
Skip (N/A) when: All tools are purely computational with no I/O operations. All checks skip when no MCP server is detected.
Cross-reference: For graceful shutdown, see graceful-shutdown.
Detail on fail: "Tool 'fetch_url' uses fetch() with no timeout — will hang indefinitely if the target server is unresponsive. The MCP client will appear frozen" or "Tool 'run_command' executes shell commands with no timeout — a hanging process will block the server permanently"
Remediation: Add timeouts to all I/O operations:
// src/tools/fetch.ts — timeout on external calls
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), 30000)
const response = await fetch(url, { signal: controller.signal })
server.tool('fetch_url', ..., async ({ url }) => {
try {
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), 30_000) // 30s timeout
const response = await fetch(url, { signal: controller.signal })
clearTimeout(timeout)
const text = await response.text()
return { content: [{ type: 'text', text }] }
} catch (error) {
if (error.name === 'AbortError') {
return { content: [{ type: 'text', text: `Request timed out after 30 seconds: ${url}` }], isError: true }
}
return { content: [{ type: 'text', text: `Fetch failed: ${error.message}` }], isError: true }
}
})