on main help sign in
fix(mcp): improve STDIO error handling (#1244)
3a99542Carlos Alexandro Becker
internal/llm/agent/mcp-tools.go+34
@@ -387,6 +387,7 @@ func createMCPSession(ctx context.Context, name string, m config.MCPConfig, reso
387
387
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)
392 cancel()
393 cancel()
@@ -398,6 +399,27 @@ func createMCPSession(ctx context.Context, name string, m config.MCPConfig, reso
398 return session, nil
399 return session, nil
399}
400}
400
401
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
406+// error.
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) {
411+ return err
412+ }
413+ ct, ok := transport.(*mcp.CommandTransport)
414+ if !ok {
415+ return err
416+ }
417+ if err2 := stdioMCPCheck(ct.Command); err2 != nil {
418+ err = errors.Join(err, err2)
419+ }
420+ return err
421+}
422+
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
467}
489}
490+
491+func stdioMCPCheck(old *exec.Cmd) error {
492+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
493+ defer cancel()
494+ cmd := exec.CommandContext(ctx, old.Path, old.Args...)
495+ cmd.Env = old.Env
496+ out, err := cmd.CombinedOutput()
497+ if err == nil || errors.Is(ctx.Err(), context.DeadlineExceeded) {
498+ return nil
499+ }
500+ return fmt.Errorf("%w: %s", err, string(out))
501+}