Skip to content

Commit 090e9e3

Browse files
committed
feat(transport): Add the NewIO function.
This function allows creating a `*transport.Stdio` using provided `io.Reader` and `io.Writer`. This allows creating an MCP client to a server running in the same process, which significanly simplifies testing.
1 parent 9f39a43 commit 090e9e3

File tree

1 file changed

+40
-9
lines changed

1 file changed

+40
-9
lines changed

client/transport/stdio.go

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,20 @@ type Stdio struct {
3333
notifyMu sync.RWMutex
3434
}
3535

36+
// NewIO returns a new stdio-based transport using existing input, output, and
37+
// logging streams instead of spawning a subprocess.
38+
// This is useful for testing and simulating client behavior.
39+
func NewIO(input io.Reader, output io.WriteCloser, logging io.ReadCloser) *Stdio {
40+
return &Stdio{
41+
stdin: output,
42+
stdout: bufio.NewReader(input),
43+
stderr: logging,
44+
45+
responses: make(map[int64]chan *JSONRPCResponse),
46+
done: make(chan struct{}),
47+
}
48+
}
49+
3650
// NewStdio creates a new stdio transport to communicate with a subprocess.
3751
// It launches the specified command with given arguments and sets up stdin/stdout pipes for communication.
3852
// Returns an error if the subprocess cannot be started or the pipes cannot be created.
@@ -55,6 +69,26 @@ func NewStdio(
5569
}
5670

5771
func (c *Stdio) Start(ctx context.Context) error {
72+
if err := c.startProc(ctx); err != nil {
73+
return err
74+
}
75+
76+
ready := make(chan struct{})
77+
go func() {
78+
close(ready)
79+
c.readResponses()
80+
}()
81+
<-ready
82+
83+
return nil
84+
}
85+
86+
// startProc spawns a new process running c.command.
87+
func (c *Stdio) startProc(ctx context.Context) error {
88+
if c.command == "" {
89+
return nil
90+
}
91+
5892
cmd := exec.CommandContext(ctx, c.command, c.args...)
5993

6094
mergedEnv := os.Environ()
@@ -86,14 +120,6 @@ func (c *Stdio) Start(ctx context.Context) error {
86120
return fmt.Errorf("failed to start command: %w", err)
87121
}
88122

89-
// Start reading responses in a goroutine and wait for it to be ready
90-
ready := make(chan struct{})
91-
go func() {
92-
close(ready)
93-
c.readResponses()
94-
}()
95-
<-ready
96-
97123
return nil
98124
}
99125

@@ -107,7 +133,12 @@ func (c *Stdio) Close() error {
107133
if err := c.stderr.Close(); err != nil {
108134
return fmt.Errorf("failed to close stderr: %w", err)
109135
}
110-
return c.cmd.Wait()
136+
137+
if c.cmd != nil {
138+
return c.cmd.Wait()
139+
}
140+
141+
return nil
111142
}
112143

113144
// OnNotification registers a handler function to be called when notifications are received.

0 commit comments

Comments
 (0)