or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

client.mdcontent-types.mdindex.mdserver.mdtransport-http.mdtransport-stdio.mdtransport.md
tile.json

transport-stdio.mddocs/

Stdio Transport

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.

Package

import "github.com/metoro-io/mcp-golang/transport/stdio"

StdioServerTransport

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:

  • Full bidirectional communication
  • Supports notifications (server-to-client)
  • Suitable for subprocess communication
  • Ideal for Claude Desktop integration
  • Thread-safe message handling

Creating a Stdio Transport

NewStdioServerTransport

func NewStdioServerTransport() *StdioServerTransport

Creates 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)

NewStdioServerTransportWithIO

func NewStdioServerTransportWithIO(in io.Reader, out io.Writer) *StdioServerTransport

Creates 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

Transport Interface Methods

StdioServerTransport implements the transport.Transport interface with full support for all methods.

Start

func (s *StdioServerTransport) Start(ctx context.Context) error

Begins listening for messages on the input stream. Blocks until the context is cancelled or an error occurs.

Parameters:

  • ctx: Context for controlling the transport lifecycle

Returns: 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)
    }
}()

Send

func (s *StdioServerTransport) Send(ctx context.Context, message *transport.BaseJsonRpcMessage) error

Sends a JSON-RPC message to the output stream.

Parameters:

  • ctx: Context for the send operation
  • message: The JSON-RPC message to send

Returns: 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)
}

Close

func (s *StdioServerTransport) Close() error

Stops 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)
}

SetCloseHandler

func (s *StdioServerTransport) SetCloseHandler(handler func())

Sets a callback function invoked when the transport connection closes.

Parameters:

  • handler: Function to call when the connection closes

Example:

transport.SetCloseHandler(func() {
    log.Println("Stdio transport closed")
    // Perform cleanup
})

SetErrorHandler

func (s *StdioServerTransport) SetErrorHandler(handler func(error))

Sets a callback function invoked when transport errors occur.

Parameters:

  • handler: Function to call with error details

Example:

transport.SetErrorHandler(func(err error) {
    log.Printf("Transport error: %v", err)
    // Handle error (retry, reconnect, etc.)
})

SetMessageHandler

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 messages

Example:

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
    }
})

Usage Examples

Basic MCP Server with Stdio

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)
    }
}

MCP Client Communicating with Subprocess

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)
}

Server with Error Handling

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)
    }
}

Custom I/O for Testing

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)
    }
}

Claude Desktop Integration

The stdio transport is the standard way to integrate with Claude Desktop. Here's a complete example:

Server Code

// 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)
    }
}

Claude Desktop Configuration

Add to claude_desktop_config.json:

{
  "mcpServers": {
    "time-server": {
      "command": "/path/to/time-server",
      "args": [],
      "env": {}
    }
  }
}

Message Format

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:

  • Parsing incoming JSON-RPC messages
  • Validating message structure
  • Serializing outgoing messages
  • Managing the message stream

Best Practices

1. Always Set Error Handlers

transport.SetErrorHandler(func(err error) {
    log.Printf("Error: %v", err)
    // Implement recovery logic if needed
})

2. Handle Graceful Shutdown

sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)

go func() {
    <-sigChan
    transport.Close()
    os.Exit(0)
}()

3. Use Context for Cancellation

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

go transport.Start(ctx)

// Cancel when done
cancel()

4. Subprocess Communication Pattern

// 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)

Limitations

  • No connection multiplexing: One transport per subprocess
  • No automatic reconnection: Connection loss requires restart
  • Requires process management: Parent process must manage subprocess lifecycle
  • Not suitable for web scenarios: Use HTTP transport for web-based communication

Thread Safety

StdioServerTransport is thread-safe:

  • Multiple goroutines can safely call Send()
  • Message handlers can process messages concurrently
  • Close can be called from any goroutine

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.