Skip to content

Commit f052cdf

Browse files
committed
feat(client): Add the NewStdioMCPClientWithIO function.
This function allows creating an `StdioMCPClient` using provided `io.Reader` and `io.Writer`. This allows creating an MCP server and client in the same process, which significanly simplifies testing.
1 parent 8cdb6c6 commit f052cdf

File tree

1 file changed

+30
-18
lines changed

1 file changed

+30
-18
lines changed

client/stdio.go

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,30 @@ type StdioMCPClient struct {
3434
capabilities mcp.ServerCapabilities
3535
}
3636

37+
// NewStdioMCPClientWithIO returns a new stdio-based MCP client using existing
38+
// input, output, and logging streams instead of spawning a subprocess.
39+
// This is useful for testing and simulating client behavior.
40+
func NewStdioMCPClientWithIO(input io.Reader, output io.WriteCloser, logging io.ReadCloser) *StdioMCPClient {
41+
client := &StdioMCPClient{
42+
cmd: nil,
43+
stdin: output,
44+
stdout: bufio.NewReader(input),
45+
stderr: logging,
46+
responses: make(map[int64]chan RPCResponse),
47+
done: make(chan struct{}),
48+
}
49+
50+
// Start reading responses in a goroutine and wait for it to be ready
51+
ready := make(chan struct{})
52+
go func() {
53+
close(ready)
54+
client.readResponses()
55+
}()
56+
<-ready
57+
58+
return client
59+
}
60+
3761
// NewStdioMCPClient creates a new stdio-based MCP client that communicates with a subprocess.
3862
// It launches the specified command with given arguments and sets up stdin/stdout pipes for communication.
3963
// Returns an error if the subprocess cannot be started or the pipes cannot be created.
@@ -64,28 +88,11 @@ func NewStdioMCPClient(
6488
return nil, fmt.Errorf("failed to create stderr pipe: %w", err)
6589
}
6690

67-
client := &StdioMCPClient{
68-
cmd: cmd,
69-
stdin: stdin,
70-
stderr: stderr,
71-
stdout: bufio.NewReader(stdout),
72-
responses: make(map[int64]chan RPCResponse),
73-
done: make(chan struct{}),
74-
}
75-
7691
if err := cmd.Start(); err != nil {
7792
return nil, fmt.Errorf("failed to start command: %w", err)
7893
}
7994

80-
// Start reading responses in a goroutine and wait for it to be ready
81-
ready := make(chan struct{})
82-
go func() {
83-
close(ready)
84-
client.readResponses()
85-
}()
86-
<-ready
87-
88-
return client, nil
95+
return NewStdioMCPClientWithIO(stdout, stdin, stderr), nil
8996
}
9097

9198
// Close shuts down the stdio client, closing the stdin pipe and waiting for the subprocess to exit.
@@ -98,6 +105,11 @@ func (c *StdioMCPClient) Close() error {
98105
if err := c.stderr.Close(); err != nil {
99106
return fmt.Errorf("failed to close stderr: %w", err)
100107
}
108+
109+
if c.cmd == nil {
110+
return nil
111+
}
112+
101113
return c.cmd.Wait()
102114
}
103115

0 commit comments

Comments
 (0)