Skip to content

Commit 051cda5

Browse files
authored
Add Stderr() Method to StdioMCPClient (#72)
* for get subprocess stderr log message * add log fetch test
1 parent 2ea0c97 commit 051cda5

File tree

3 files changed

+60
-1
lines changed

3 files changed

+60
-1
lines changed

client/stdio.go

+16
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ type StdioMCPClient struct {
2323
cmd *exec.Cmd
2424
stdin io.WriteCloser
2525
stdout *bufio.Reader
26+
stderr io.ReadCloser
2627
requestID atomic.Int64
2728
responses map[int64]chan RPCResponse
2829
mu sync.RWMutex
@@ -58,9 +59,15 @@ func NewStdioMCPClient(
5859
return nil, fmt.Errorf("failed to create stdout pipe: %w", err)
5960
}
6061

62+
stderr, err := cmd.StderrPipe()
63+
if err != nil {
64+
return nil, fmt.Errorf("failed to create stderr pipe: %w", err)
65+
}
66+
6167
client := &StdioMCPClient{
6268
cmd: cmd,
6369
stdin: stdin,
70+
stderr: stderr,
6471
stdout: bufio.NewReader(stdout),
6572
responses: make(map[int64]chan RPCResponse),
6673
done: make(chan struct{}),
@@ -88,9 +95,18 @@ func (c *StdioMCPClient) Close() error {
8895
if err := c.stdin.Close(); err != nil {
8996
return fmt.Errorf("failed to close stdin: %w", err)
9097
}
98+
if err := c.stderr.Close(); err != nil {
99+
return fmt.Errorf("failed to close stderr: %w", err)
100+
}
91101
return c.cmd.Wait()
92102
}
93103

104+
// Stderr returns a reader for the stderr output of the subprocess.
105+
// This can be used to capture error messages or logs from the subprocess.
106+
func (c *StdioMCPClient) Stderr() io.Reader {
107+
return c.stderr
108+
}
109+
94110
// OnNotification registers a handler function to be called when notifications are received.
95111
// Multiple handlers can be registered and will be called in the order they were added.
96112
func (c *StdioMCPClient) OnNotification(

client/stdio_test.go

+41-1
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@ package client
22

33
import (
44
"context"
5+
"encoding/json"
56
"fmt"
7+
"log/slog"
68
"os"
79
"os/exec"
810
"path/filepath"
11+
"sync"
912
"testing"
1013
"time"
1114

@@ -38,7 +41,23 @@ func TestStdioMCPClient(t *testing.T) {
3841
if err != nil {
3942
t.Fatalf("Failed to create client: %v", err)
4043
}
41-
defer client.Close()
44+
var logRecords []map[string]any
45+
var logRecordsMu sync.RWMutex
46+
var wg sync.WaitGroup
47+
wg.Add(1)
48+
go func() {
49+
defer wg.Done()
50+
dec := json.NewDecoder(client.Stderr())
51+
for {
52+
var record map[string]any
53+
if err := dec.Decode(&record); err != nil {
54+
return
55+
}
56+
logRecordsMu.Lock()
57+
logRecords = append(logRecords, record)
58+
logRecordsMu.Unlock()
59+
}
60+
}()
4261

4362
t.Run("Initialize", func(t *testing.T) {
4463
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
@@ -238,4 +257,25 @@ func TestStdioMCPClient(t *testing.T) {
238257
)
239258
}
240259
})
260+
261+
client.Close()
262+
wg.Wait()
263+
264+
t.Run("CheckLogs", func(t *testing.T) {
265+
logRecordsMu.RLock()
266+
defer logRecordsMu.RUnlock()
267+
268+
if len(logRecords) != 1 {
269+
t.Errorf("Expected 1 log record, got %d", len(logRecords))
270+
return
271+
}
272+
273+
msg, ok := logRecords[0][slog.MessageKey].(string)
274+
if !ok {
275+
t.Errorf("Expected log record to have message key")
276+
}
277+
if msg != "launch successful" {
278+
t.Errorf("Expected log message 'launch successful', got '%s'", msg)
279+
}
280+
})
241281
}

testdata/mockstdio_server.go

+3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bufio"
55
"encoding/json"
66
"fmt"
7+
"log/slog"
78
"os"
89
)
910

@@ -25,6 +26,8 @@ type JSONRPCResponse struct {
2526
}
2627

2728
func main() {
29+
logger := slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{}))
30+
logger.Info("launch successful")
2831
scanner := bufio.NewScanner(os.Stdin)
2932
for scanner.Scan() {
3033
var request JSONRPCRequest

0 commit comments

Comments
 (0)