The stdio transport provides standard input/output based communication for MCP. It supports full bidirectional MCP features including notifications, making it ideal for subprocess communication and CLI tools.
import "github.com/metoro-io/mcp-golang/transport/stdio"type StdioServerTransport struct {
// Contains unexported fields
}Implements server-side transport for stdio communication. Reads JSON-RPC messages from stdin and writes responses to stdout.
Features:
func NewStdioServerTransport() *StdioServerTransportCreates a stdio transport using os.Stdin and os.Stdout for communication.
Returns: StdioServerTransport ready for use
Example:
import (
"github.com/metoro-io/mcp-golang/transport/stdio"
)
transport := stdio.NewStdioServerTransport()Use Case: Standard MCP server that communicates via stdin/stdout (e.g., for Claude Desktop)
func NewStdioServerTransportWithIO(in io.Reader, out io.Writer) *StdioServerTransportCreates a stdio transport with custom input and output streams.
Parameters:
in: Reader for incoming messages (typically stdin or pipe)out: Writer for outgoing messages (typically stdout or pipe)Returns: StdioServerTransport using the specified I/O streams
Example:
import (
"bytes"
"github.com/metoro-io/mcp-golang/transport/stdio"
)
// Custom I/O for testing or special scenarios
var inputBuffer bytes.Buffer
var outputBuffer bytes.Buffer
transport := stdio.NewStdioServerTransportWithIO(&inputBuffer, &outputBuffer)Use Case: Testing, subprocess communication with custom pipes, or non-standard I/O scenarios
StdioServerTransport implements the transport.Transport interface with full support for all methods.
func (s *StdioServerTransport) Start(ctx context.Context) errorBegins listening for messages on the input stream. Blocks until the context is cancelled or an error occurs.
Parameters:
ctx: Context for controlling the transport lifecycleReturns: Error if the transport fails to start or encounters a fatal error
Example:
ctx := context.Background()
transport := stdio.NewStdioServerTransport()
go func() {
if err := transport.Start(ctx); err != nil {
log.Fatalf("Transport error: %v", err)
}
}()func (s *StdioServerTransport) Send(ctx context.Context, message *transport.BaseJsonRpcMessage) errorSends a JSON-RPC message to the output stream.
Parameters:
ctx: Context for the send operationmessage: The JSON-RPC message to sendReturns: Error if sending fails
Example:
import "github.com/metoro-io/mcp-golang/transport"
response := &transport.BaseJSONRPCResponse{
Id: 1,
Jsonrpc: "2.0",
Result: json.RawMessage(`{"content": [{"type": "text", "text": "Hello"}]}`),
}
message := transport.NewBaseMessageResponse(response)
err := transport.Send(ctx, message)
if err != nil {
log.Printf("Failed to send message: %v", err)
}func (s *StdioServerTransport) Close() errorStops the transport and cleans up resources. Any blocking Start() call will return.
Returns: Error if cleanup fails
Example:
defer transport.Close()
// Later...
if err := transport.Close(); err != nil {
log.Printf("Error closing transport: %v", err)
}func (s *StdioServerTransport) SetCloseHandler(handler func())Sets a callback function invoked when the transport connection closes.
Parameters:
handler: Function to call when the connection closesExample:
transport.SetCloseHandler(func() {
log.Println("Stdio transport closed")
// Perform cleanup
})func (s *StdioServerTransport) SetErrorHandler(handler func(error))Sets a callback function invoked when transport errors occur.
Parameters:
handler: Function to call with error detailsExample:
transport.SetErrorHandler(func(err error) {
log.Printf("Transport error: %v", err)
// Handle error (retry, reconnect, etc.)
})func (s *StdioServerTransport) SetMessageHandler(handler func(ctx context.Context, message *transport.BaseJsonRpcMessage))Sets a callback function invoked when messages are received from the input stream.
Parameters:
handler: Function to call with received messagesExample:
transport.SetMessageHandler(func(ctx context.Context, message *transport.BaseJsonRpcMessage) {
switch message.Type {
case transport.BaseMessageTypeJSONRPCRequestType:
req := message.JsonRpcRequest
log.Printf("Received request: %s", req.Method)
// Handle request
case transport.BaseMessageTypeJSONRPCNotificationType:
notif := message.JsonRpcNotification
log.Printf("Received notification: %s", notif.Method)
// Handle notification
}
})package main
import (
"log"
mcp "github.com/metoro-io/mcp-golang"
"github.com/metoro-io/mcp-golang/transport/stdio"
)
type EchoArgs struct {
Message string `json:"message" jsonschema:"required,description=Message to echo"`
}
func main() {
// Create stdio transport
transport := stdio.NewStdioServerTransport()
// Create MCP server
server := mcp.NewServer(
transport,
mcp.WithName("echo-server"),
mcp.WithVersion("1.0.0"),
)
// Register a tool
server.RegisterTool("echo", "Echoes the input message",
func(args EchoArgs) (*mcp.ToolResponse, error) {
return mcp.NewToolResponse(
mcp.NewTextContent(args.Message),
), nil
})
// Start server (blocks)
log.Println("Starting echo server...")
if err := server.Serve(); err != nil {
log.Fatalf("Server error: %v", err)
}
}package main
import (
"context"
"log"
"os/exec"
mcp "github.com/metoro-io/mcp-golang"
"github.com/metoro-io/mcp-golang/transport/stdio"
)
func main() {
// Start server as subprocess
cmd := exec.Command("./mcp-server")
// Get stdin/stdout pipes
stdin, err := cmd.StdinPipe()
if err != nil {
log.Fatal(err)
}
stdout, err := cmd.StdoutPipe()
if err != nil {
log.Fatal(err)
}
// Start subprocess
if err := cmd.Start(); err != nil {
log.Fatal(err)
}
defer cmd.Process.Kill()
// Create transport with subprocess pipes
transport := stdio.NewStdioServerTransportWithIO(stdout, stdin)
// Create client
client := mcp.NewClient(transport)
// Initialize
ctx := context.Background()
_, err = client.Initialize(ctx)
if err != nil {
log.Fatalf("Failed to initialize: %v", err)
}
// Call tool
type EchoArgs struct {
Message string `json:"message"`
}
response, err := client.CallTool(ctx, "echo", EchoArgs{
Message: "Hello from client!",
})
if err != nil {
log.Fatalf("Failed to call tool: %v", err)
}
log.Printf("Response: %s", response.Content[0].TextContent.Text)
}package main
import (
"context"
"log"
"os"
"os/signal"
"syscall"
mcp "github.com/metoro-io/mcp-golang"
"github.com/metoro-io/mcp-golang/transport/stdio"
)
func main() {
transport := stdio.NewStdioServerTransport()
// Set up error handler
transport.SetErrorHandler(func(err error) {
log.Printf("Transport error: %v", err)
})
// Set up close handler
transport.SetCloseHandler(func() {
log.Println("Transport closed")
os.Exit(0)
})
// Create server
server := mcp.NewServer(
transport,
mcp.WithName("resilient-server"),
mcp.WithVersion("1.0.0"),
)
// Register tools, prompts, resources...
server.RegisterTool("status", "Returns server status",
func() (*mcp.ToolResponse, error) {
return mcp.NewToolResponse(
mcp.NewTextContent("Server is running"),
), nil
})
// Handle shutdown gracefully
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
go func() {
<-sigChan
log.Println("Shutdown signal received")
transport.Close()
os.Exit(0)
}()
// Start server
log.Println("Server starting...")
if err := server.Serve(); err != nil {
log.Fatalf("Server error: %v", err)
}
}package main
import (
"bytes"
"context"
"encoding/json"
"testing"
mcp "github.com/metoro-io/mcp-golang"
"github.com/metoro-io/mcp-golang/transport"
"github.com/metoro-io/mcp-golang/transport/stdio"
)
func TestToolExecution(t *testing.T) {
// Create buffers for I/O
var input bytes.Buffer
var output bytes.Buffer
// Create transport with test buffers
transport := stdio.NewStdioServerTransportWithIO(&input, &output)
// Create server
server := mcp.NewServer(transport)
server.RegisterTool("test", "Test tool",
func() (*mcp.ToolResponse, error) {
return mcp.NewToolResponse(
mcp.NewTextContent("test result"),
), nil
})
// Start server in background
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go server.Serve()
// Write request to input buffer
request := transport.BaseJSONRPCRequest{
Id: 1,
Jsonrpc: "2.0",
Method: "tools/call",
Params: json.RawMessage(`{"name": "test", "arguments": {}}`),
}
requestBytes, _ := json.Marshal(request)
input.Write(append(requestBytes, '\n'))
// Read response from output buffer
var response transport.BaseJSONRPCResponse
decoder := json.NewDecoder(&output)
if err := decoder.Decode(&response); err != nil {
t.Fatalf("Failed to decode response: %v", err)
}
// Verify response
if response.Id != 1 {
t.Errorf("Expected response ID 1, got %d", response.Id)
}
}The stdio transport is the standard way to integrate with Claude Desktop. Here's a complete example:
// main.go
package main
import (
"fmt"
"log"
mcp "github.com/metoro-io/mcp-golang"
"github.com/metoro-io/mcp-golang/transport/stdio"
)
type TimeArgs struct {
Timezone string `json:"timezone,omitempty" jsonschema:"description=Timezone (e.g. America/New_York)"`
}
func main() {
transport := stdio.NewStdioServerTransport()
server := mcp.NewServer(
transport,
mcp.WithName("time-server"),
mcp.WithVersion("1.0.0"),
mcp.WithInstructions("Provides current time information"),
)
server.RegisterTool("get-time", "Gets the current time",
func(args TimeArgs) (*mcp.ToolResponse, error) {
// Implementation would use time.Now() and format based on timezone
result := fmt.Sprintf("Current time: 2024-01-15 14:30:00")
return mcp.NewToolResponse(
mcp.NewTextContent(result),
), nil
})
log.Println("Time server starting...")
if err := server.Serve(); err != nil {
log.Fatalf("Server error: %v", err)
}
}Add to claude_desktop_config.json:
{
"mcpServers": {
"time-server": {
"command": "/path/to/time-server",
"args": [],
"env": {}
}
}
}Messages are exchanged as newline-delimited JSON:
{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}
{"jsonrpc":"2.0","id":1,"result":{"tools":[{"name":"echo","description":"Echoes input"}]}}Each message is a single line terminated by \n. The transport handles:
transport.SetErrorHandler(func(err error) {
log.Printf("Error: %v", err)
// Implement recovery logic if needed
})sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
go func() {
<-sigChan
transport.Close()
os.Exit(0)
}()ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go transport.Start(ctx)
// Cancel when done
cancel()// Server side: Use standard constructor
transport := stdio.NewStdioServerTransport()
// Client side: Connect to subprocess pipes
cmd := exec.Command("./server")
stdin, _ := cmd.StdinPipe()
stdout, _ := cmd.StdoutPipe()
cmd.Start()
transport := stdio.NewStdioServerTransportWithIO(stdout, stdin)StdioServerTransport is thread-safe:
Send()However, the underlying I/O streams (os.Stdin, os.Stdout) should not be used by other parts of the application while the transport is active.