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

  1. 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.
  2. 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).
  3. For http/sse: verify the URL is reachable and returns 2xx; confirm auth headers/tokens; ensure the server implements Streamable HTTP or SSE.
  4. Check the browser/runtime console: MCPServer logs 'mcp:client:start' / 'mcp:client:onUncaughtError' with the underlying error.
  5. 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

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


AI-assisted analysis of chatboxai/chatbox@81571269ad (2026-08-12). Data as JSON: /api/errors/18b762f4acaf509c. Report an issue: GitHub.