chatboxai/chatbox · error · Error
Failed to start server: ${server.status.state}
Error message
Failed to start server: ${server.status.state} What it means
Thrown in ConfigModal.testConnection (src/renderer/components/settings/mcp/ConfigModal.tsx:85) during the MCP server connection test. After `await pTimeout(server.start(), { milliseconds: 5min, signal })`, if server.status.state is not 'running', it throws server.status.error (if present) else this template. This branch is reached because MCPServer.start() catches its own transport errors and parks in state 'idle' with an error message instead of rethrowing, so the throw here is the normal failure-reporting path: state is typically 'idle' and server.status.error carries the real cause (which is preferred when non-empty).
Source
Thrown at src/renderer/components/settings/mcp/ConfigModal.tsx:85
const testConnection = async () => {
if (formRef.current && !formRef.current.reportValidity()) {
return
}
const config = getConfigFromFormValues(form.getValues())
console.debug('Testing connection with config', config)
setTesting(true)
setTestingResult(null)
trackEvent('test_mcp_server_connection', { type: config.transport.type })
try {
const server = new MCPServer(config.transport)
testingAbortController.current = new AbortController()
await pTimeout(server.start(), {
milliseconds: 5 * 60_000,
signal: testingAbortController.current.signal,
})
if (server.status.state !== 'running') {
throw new Error(server.status.error || `Failed to start server: ${server.status.state}`)
}
const tools = await server.getAvailableTools()
setTestingResult({
config,
tools: Object.keys(tools).map((name) => ({ name, description: tools[name].description })),
})
await server.stop()
} catch (err) {
if (testingAbortController.current?.signal.aborted) {
return
}
setTestingResult({ config, error: err as Error, tools: [] })
} finally {
setTesting(false)
}
}
const handleSubmit = (values: typeof form.values) => {View on GitHub (pinned to 81571269ad)
Solutions
- Read the full message: server.status.error is used when present and usually names the real cause (e.g. ENOENT, 'MCP SSE Transport Error: 405'). The ConfigModal already shows an ENOENT hint for stdio.
- For stdio: verify the command is installed and on PATH in the runtime shell; test it in a terminal first (e.g. `npx -y <pkg>` or the absolute path).
- For http/sse: verify the URL is reachable and returns 2xx; confirm auth headers/tokens; ensure the server implements Streamable HTTP or SSE.
- Check the browser/runtime console: MCPServer logs 'mcp:client:start' / 'mcp:client:onUncaughtError' with the underlying error.
- If the server is slow, note the 5-minute pTimeout ceiling; a timeout surfaces as a TimeoutError, not this message.
Example fix
// before
await pTimeout(server.start(), { milliseconds: 5 * 60_000, signal })
if (server.status.state !== 'running') {
throw new Error(server.status.error || `Failed to start server: ${server.status.state}`)
}
// after (surface the real cause, keep the generic fallback)
await pTimeout(server.start(), { milliseconds: 5 * 60_000, signal })
if (server.status.state !== 'running') {
const reason = server.status.error || `Failed to start server: ${server.status.state}`
console.error('MCP test failed', { state: server.status.state, error: server.status.error })
throw new Error(reason)
} Defensive patterns
Strategy: try-catch
Type guard
// Narrow MCPServerStatus to extract the failure reason
function mcpStartFailure(
s: { state: string; error?: string }
): string | null {
return s.state === 'running' ? null : s.error || `Failed to start server: ${s.state}`
} Try / catch
// The test path already wraps this; mirror it for runtime starts
try {
await pTimeout(server.start(), { milliseconds: 5 * 60_000, signal })
if (server.status.state !== 'running') {
throw new Error(server.status.error || `Failed to start server: ${server.status.state}`)
}
} catch (e) {
showMcpError((e as Error).message) // server.status.error usually carries the real cause
} finally {
await server.stop().catch(() => {})
} Prevention
- Test the stdio command in a real terminal before saving the MCP config.
- For http/sse, curl the endpoint to confirm it speaks Streamable HTTP or SSE and returns 2xx.
- Always stop the server in a finally block to avoid orphaned stdio processes.
- Read the runtime console: MCPServer logs the underlying transport error at 'mcp:client:start'.
- Respect the 5-minute pTimeout; a slow server will throw TimeoutError, not this status message.
When it happens
Trigger: new MCPServer(config.transport).start() resolves without throwing but leaves state !== 'running' (i.e. 'idle' with error). Concrete causes: stdio transport where the command does not exist (ENOENT), command exits/crashes, missing args/env; http/sse transport with wrong URL, auth failure, 405/401 handshake, or both Streamable HTTP and legacy SSE fallback failing; start() aborted via the AbortController. The pTimeout itself throws a different error (TimeoutError) that is caught separately at line 93.
Common situations: User enters an MCP server config and clicks Test: mistyped command path (npx vs npx.exe, missing binary), command not installed, wrong/missing API URL or token for a remote server, server endpoint does not speak Streamable HTTP or SSE, slow server exceeding the 5-minute timeout, or a stdio server that prints errors to stderr and exits.
Related errors
- Streamable HTTP connection failed: ${streamableMessage}\nLeg
- Unknown transport type
- Custom provider "${providerId}" conflicts with a builtin pro
- Unsupported search provider: ${provider}
- third_party_parser_not_supported_in_chat
AI-assisted analysis of chatboxai/chatbox@81571269ad (2026-08-12).
Data as JSON: /api/errors/18b762f4acaf509c.
Report an issue: GitHub.