@@ -387,6 +387,7 @@ func createMCPSession(ctx context.Context, name string, m config.MCPConfig, reso
388 session, err := client.Connect(mcpCtx, transport, nil)
388 session, err := client.Connect(mcpCtx, transport, nil)
389 if err != nil {
389 if err != nil {
390+ err = maybeStdioErr(err, transport)
390 updateMCPState(name, MCPStateError, maybeTimeoutErr(err, timeout), nil, 0)
391 updateMCPState(name, MCPStateError, maybeTimeoutErr(err, timeout), nil, 0)
391 slog.Error("error starting mcp client", "error", err, "name", name)
392 slog.Error("error starting mcp client", "error", err, "name", name)
@@ -398,6 +399,27 @@ func createMCPSession(ctx context.Context, name string, m config.MCPConfig, reso
398 return session, nil
399 return session, nil
402+// maybeStdioErr if a stdio mcp prints an error in non-json format, it'll fail
403+// to parse, and the cli will then close it, causing the EOF error.
404+// so, if we got an EOF err, and the transport is STDIO, we try to exec it
405+// again with a timeout and collect the output so we can add details to the
407+// this happens particularly when starting things with npx, e.g. if node can't
408+// be found or some other error like that.
409+func maybeStdioErr(err error, transport mcp.Transport) error {
410+ if !errors.Is(err, io.EOF) {
413+ ct, ok := transport.(*mcp.CommandTransport)
417+ if err2 := stdioMCPCheck(ct.Command); err2 != nil {
418+ err = errors.Join(err, err2)
401func maybeTimeoutErr(err error, timeout time.Duration) error {
423func maybeTimeoutErr(err error, timeout time.Duration) error {
402 if errors.Is(err, context.Canceled) {
424 if errors.Is(err, context.Canceled) {
403 return fmt.Errorf("timed out after %s", timeout)
425 return fmt.Errorf("timed out after %s", timeout)
@@ -465,3 +487,15 @@ func (rt headerRoundTripper) RoundTrip(req *http.Request) (*http.Response, error
465func mcpTimeout(m config.MCPConfig) time.Duration {
487func mcpTimeout(m config.MCPConfig) time.Duration {
466 return time.Duration(cmp.Or(m.Timeout, 15)) * time.Second
488 return time.Duration(cmp.Or(m.Timeout, 15)) * time.Second
491+func stdioMCPCheck(old *exec.Cmd) error {
492+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
494+ cmd := exec.CommandContext(ctx, old.Path, old.Args...)
496+ out, err := cmd.CombinedOutput()
497+ if err == nil || errors.Is(ctx.Err(), context.DeadlineExceeded) {
500+ return fmt.Errorf("%w: %s", err, string(out))