cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

main
012 / internal / cowork / attach.go
attach.go 3.1K
1package cowork23import (4 "bufio"5 "encoding/json"6 "errors"7 "fmt"8 "io"9 "net"10 "path/filepath"11)1213// Attach is 012 mcp --attach: it finds the session target names (see14// Pick), refusing a socket that isn't the user's alone, and carries the15// MCP client's messages from in to it and its answers to out until16// either side ends. The client's first message, its initialize request,17// names it to the others. When the session can't be reached, the18// initialize request is answered with the reason, which hosts show, and19// Attach returns it.20func Attach(target string, in io.Reader, out io.Writer) error {21 br := bufio.NewReader(in)22 first, err := br.ReadBytes('\n')23 if err != nil && len(first) == 0 {24 return err25 }26 c, err := dial(target, clientName(first))27 if err != nil {28 refuse(out, first, err)29 return err30 }31 defer c.conn.Close()32 if _, err := c.conn.Write(first); err != nil {33 return err34 }35 go func() {36 io.Copy(c.conn, br)37 if uc, ok := c.conn.(*net.UnixConn); ok {38 uc.CloseWrite()39 }40 }()41 _, err = io.Copy(out, c.r)42 return err43}4445// attached is a connection past the welcome.46type attached struct {47 conn net.Conn48 r *bufio.Reader49 w welcome50}5152// dial connects to the session target names, as client.53func dial(target, client string) (attached, error) {54 dir, err := SocketDir()55 if err != nil {56 return attached{}, err57 }58 eps, err := Endpoints(dir)59 if err != nil {60 return attached{}, err61 }62 ep, roomName, err := Pick(eps, target)63 if err != nil {64 return attached{}, err65 }66 if err := checkOwner(filepath.Dir(ep.Socket)); err != nil {67 return attached{}, err68 }69 if err := checkOwner(ep.Socket); err != nil {70 return attached{}, err71 }72 conn, err := net.Dial("unix", ep.Socket)73 if err != nil {74 return attached{}, fmt.Errorf("the session doesn't answer: %w", err)75 }76 if err := json.NewEncoder(conn).Encode(hello{Version: 1, Room: roomName, Client: client}); err != nil {77 conn.Close()78 return attached{}, err79 }80 a := attached{conn: conn, r: bufio.NewReader(conn)}81 line, err := a.r.ReadBytes('\n')82 if err == nil {83 err = json.Unmarshal(line, &a.w)84 }85 switch {86 case err != nil:87 conn.Close()88 return attached{}, fmt.Errorf("the session refused: %w", err)89 case a.w.Error != "":90 conn.Close()91 return attached{}, errors.New(a.w.Error)92 }93 return a, nil94}9596// clientName is the MCP client's name from its initialize request: its97// title, else its name.98func clientName(initialize []byte) string {99 var req struct {100 Params struct {101 ClientInfo struct {102 Name string `json:"name"`103 Title string `json:"title"`104 } `json:"clientInfo"`105 } `json:"params"`106 }107 json.Unmarshal(initialize, &req)108 if t := req.Params.ClientInfo.Title; t != "" {109 return t110 }111 return req.Params.ClientInfo.Name112}113114// refuse answers the client's first request with err, as a JSON-RPC115// error its host shows.116func refuse(out io.Writer, first []byte, err error) {117 var req struct {118 ID json.RawMessage `json:"id"`119 }120 if json.Unmarshal(first, &req) != nil || len(req.ID) == 0 {121 return122 }123 resp, _ := json.Marshal(map[string]any{"jsonrpc": "2.0", "id": req.ID, "error": map[string]any{"code": -32000, "message": "012: " + err.Error()}})124 out.Write(append(resp, '\n'))125}