// Package bridge implements `nexus connect`: a stdio<->HTTP shim so local // agents that only speak the MCP stdio transport (Claude Desktop, Cursor, …) // can attach to the Nexus Gateway's Streamable HTTP endpoint. Each line of // JSON-RPC on stdin is POSTed to the gateway; the response is written to stdout. package bridge import ( "bufio" "bytes" "context" "encoding/json" "fmt" "io" "net/http" "time" ) // Run bridges stdin/stdout to the MCP endpoint at url until stdin closes. func Run(ctx context.Context, url string, in io.Reader, out io.Writer) error { client := &http.Client{Timeout: 5 * time.Minute} scanner := bufio.NewScanner(in) scanner.Buffer(make([]byte, 0, 64*1024), 16<<20) enc := json.NewEncoder(out) for scanner.Scan() { line := bytes.TrimSpace(scanner.Bytes()) if len(line) == 0 { continue } // Preserve the raw frame; forward verbatim. frame := make([]byte, len(line)) copy(frame, line) respBody, isNotification, err := forward(ctx, client, url, frame) if err != nil { return fmt.Errorf("bridge forward: %w", err) } if isNotification || len(respBody) == 0 { continue // notifications produce no stdout frame } var msg json.RawMessage = respBody if err := enc.Encode(msg); err != nil { return err } } return scanner.Err() } func forward(ctx context.Context, client *http.Client, url string, frame []byte) (body []byte, notification bool, err error) { // Detect notification (no id) to avoid emitting an empty stdout frame. var probe struct { ID json.RawMessage `json:"id"` Method string `json:"method"` } _ = json.Unmarshal(frame, &probe) notification = probe.Method != "" && len(probe.ID) == 0 req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(frame)) if err != nil { return nil, notification, err } req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json, text/event-stream") resp, err := client.Do(req) if err != nil { return nil, notification, err } defer resp.Body.Close() b, err := io.ReadAll(io.LimitReader(resp.Body, 32<<20)) if err != nil { return nil, notification, err } if resp.StatusCode == http.StatusAccepted { return nil, true, nil } return bytes.TrimSpace(b), notification, nil }