Spaces:
Runtime error
Runtime error
File size: 3,175 Bytes
857a91b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 | package mcp
import (
"bufio"
"context"
"encoding/json"
"fmt"
"io"
"os/exec"
"sync"
)
// Client is a JSON-RPC client speaking to an MCP server over the child
// process's stdin/stdout. A background reader dispatches responses to pending
// callers keyed by request id; server-initiated notifications are ignored.
type Client struct {
cmd *exec.Cmd
stdin io.WriteCloser
stdout *bufio.Reader
mu sync.Mutex
nextID int
pending map[int]chan response
encMu sync.Mutex // serialises writes to stdin
closeOnce sync.Once
readErr error
}
// newClient wires a client to an already-started command's pipes and starts
// the reader loop.
func newClient(cmd *exec.Cmd, stdin io.WriteCloser, stdout io.Reader) *Client {
c := &Client{
cmd: cmd,
stdin: stdin,
stdout: bufio.NewReaderSize(stdout, 1<<20),
pending: make(map[int]chan response),
}
go c.readLoop()
return c
}
func (c *Client) readLoop() {
for {
line, err := c.stdout.ReadBytes('\n')
if len(line) > 0 {
var msg response
if jErr := json.Unmarshal(line, &msg); jErr == nil {
c.dispatch(msg)
}
}
if err != nil {
c.mu.Lock()
c.readErr = err
for id, ch := range c.pending {
close(ch)
delete(c.pending, id)
}
c.mu.Unlock()
return
}
}
}
func (c *Client) dispatch(msg response) {
if msg.ID == nil {
// Server-initiated notification or request: ignored for the MVP.
return
}
c.mu.Lock()
ch, ok := c.pending[*msg.ID]
if ok {
delete(c.pending, *msg.ID)
}
c.mu.Unlock()
if ok {
ch <- msg
close(ch)
}
}
// call sends a request and waits for the matching response or ctx expiry.
func (c *Client) call(ctx context.Context, method string, params any) (json.RawMessage, error) {
c.mu.Lock()
if c.readErr != nil {
c.mu.Unlock()
return nil, fmt.Errorf("mcp server closed: %w", c.readErr)
}
c.nextID++
id := c.nextID
ch := make(chan response, 1)
c.pending[id] = ch
c.mu.Unlock()
if err := c.write(request{JSONRPC: "2.0", ID: &id, Method: method, Params: params}); err != nil {
c.mu.Lock()
delete(c.pending, id)
c.mu.Unlock()
return nil, err
}
select {
case <-ctx.Done():
c.mu.Lock()
delete(c.pending, id)
c.mu.Unlock()
return nil, fmt.Errorf("mcp %s timed out: %w", method, ctx.Err())
case msg, ok := <-ch:
if !ok {
return nil, fmt.Errorf("mcp %s: server closed before responding", method)
}
if msg.Error != nil {
return nil, fmt.Errorf("mcp %s error %d: %s", method, msg.Error.Code, msg.Error.Message)
}
return msg.Result, nil
}
}
// notify sends a notification (no id, no response expected).
func (c *Client) notify(method string, params any) error {
return c.write(request{JSONRPC: "2.0", Method: method, Params: params})
}
func (c *Client) write(r request) error {
b, err := json.Marshal(r)
if err != nil {
return err
}
b = append(b, '\n')
c.encMu.Lock()
defer c.encMu.Unlock()
_, err = c.stdin.Write(b)
return err
}
// Close terminates the underlying process and releases pipes.
func (c *Client) Close() {
c.closeOnce.Do(func() {
_ = c.stdin.Close()
if c.cmd != nil && c.cmd.Process != nil {
_ = c.cmd.Process.Kill()
_ = c.cmd.Wait()
}
})
}
|