Skip to content

STDIO Transport

STDIO (Standard Input/Output) transport is the most common MCP transport method, perfect for command-line tools, desktop applications, and local integrations.

Use Cases

STDIO transport excels in scenarios where:

  • Command-line tools: CLI utilities that LLMs can invoke
  • Desktop applications: IDE plugins, text editors, local tools
  • Subprocess communication: Parent processes managing MCP servers
  • Local development: Testing and debugging MCP implementations
  • Single-user scenarios: Personal productivity tools
Example applications:
  • File system browsers for IDEs
  • Local database query tools
  • Git repository analyzers
  • System monitoring utilities
  • Development workflow automation

Implementation

Basic STDIO Server

package main
 
import (
    "context"
    "fmt"
    "os"
    "path/filepath"
    "strings"
 
    "github.com/mark3labs/mcp-go/mcp"
    "github.com/mark3labs/mcp-go/server"
)
 
func main() {
    s := server.NewMCPServer("File Tools", "1.0.0",
        server.WithToolCapabilities(true),
        server.WithResourceCapabilities(true, true),
    )
 
    // Add file listing tool
    s.AddTool(
        mcp.NewTool("list_files",
            mcp.WithDescription("List files in a directory"),
            mcp.WithString("path", 
                mcp.Required(),
                mcp.Description("Directory path to list"),
            ),
            mcp.WithBoolean("recursive",
                mcp.DefaultBool(false),
                mcp.Description("List files recursively"),
            ),
        ),
        handleListFiles,
    )
 
    // Add file content resource
    s.AddResource(
        mcp.NewResource(
            "file://{path}",
            "File Content",
            mcp.WithResourceDescription("Read file contents"),
            mcp.WithMIMEType("text/plain"),
        ),
        handleFileContent,
    )
 
    // Start STDIO server
    if err := server.ServeStdio(s); err != nil {
        fmt.Fprintf(os.Stderr, "Server error: %v\n", err)
        os.Exit(1)
    }
}
 
func handleListFiles(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    path, err := req.RequireString("path")
    if err != nil {
        return mcp.NewToolResultError(err.Error()), nil
    }
    
    recursive, err := req.RequireBool("recursive")
    if err != nil {
        return mcp.NewToolResultError(err.Error()), nil
    }
 
    // Security: validate path
    if !isValidPath(path) {
        return mcp.NewToolResultError(fmt.Sprintf("invalid path: %s", path)), nil
    }
 
    files, err := listFiles(path, recursive)
    if err != nil {
        return mcp.NewToolResultError(fmt.Sprintf("failed to list files: %v", err)), nil
    }
 
    return mcp.NewToolResultText(fmt.Sprintf(`{"path":"%s","files":%v,"count":%d,"recursive":%t}`, 
        path, files, len(files), recursive)), nil
}
 
func handleFileContent(ctx context.Context, req mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
    // Extract path from URI: "file:///path/to/file" -> "/path/to/file"
    path := extractPathFromURI(req.Params.URI)
    
    if !isValidPath(path) {
        return nil, fmt.Errorf("invalid path: %s", path)
    }
 
    content, err := os.ReadFile(path)
    if err != nil {
        return nil, fmt.Errorf("failed to read file: %w", err)
    }
 
    return []mcp.ResourceContents{
        mcp.TextResourceContents{
            URI:      req.Params.URI,
            MIMEType: detectMIMEType(path),
            Text:     string(content),
        },
    }, nil
}
 
func isValidPath(path string) bool {
    // Clean the path to resolve any . or .. components
    clean := filepath.Clean(path)
    
    // Check for directory traversal patterns
    if strings.Contains(clean, "..") {
        return false
    }
    
    // For absolute paths, ensure they're within a safe base directory
    if filepath.IsAbs(clean) {
        // Define safe base directories (adjust as needed for your use case)
        safeBaseDirs := []string{
            "/tmp",
            "/var/tmp", 
            "/home",
            "/Users", // macOS
        }
        
        // Check if the path starts with any safe base directory
        for _, baseDir := range safeBaseDirs {
            if strings.HasPrefix(clean, baseDir) {
                return true
            }
        }
        return false
    }
    
    // For relative paths, ensure they don't escape the current directory
    return !strings.HasPrefix(clean, "..")
}
 
// Helper functions for the examples
func listFiles(path string, recursive bool) ([]string, error) {
    // Placeholder implementation
    return []string{"file1.txt", "file2.txt"}, nil
}
 
func extractPathFromURI(uri string) string {
    // Extract path from URI: "file:///path/to/file" -> "/path/to/file"
    if strings.HasPrefix(uri, "file://") {
        return strings.TrimPrefix(uri, "file://")
    }
    return uri
}
 
func detectMIMEType(path string) string {
    // Simple MIME type detection based on extension
    ext := filepath.Ext(path)
    switch ext {
    case ".txt":
        return "text/plain"
    case ".json":
        return "application/json"
    case ".html":
        return "text/html"
    default:
        return "application/octet-stream"
    }
}

Advanced STDIO Server

package main
import (
    "context"
    "fmt"
    "log"
    "os"
    "os/signal"
    "syscall"
    "time"
 
    "github.com/mark3labs/mcp-go/mcp"
    "github.com/mark3labs/mcp-go/server"
)
 
func main() {
    s := server.NewMCPServer("Advanced CLI Tool", "1.0.0",
        server.WithResourceCapabilities(true, true),
        server.WithPromptCapabilities(true),
        server.WithToolCapabilities(true),
        server.WithLogging(),
    )
 
    // Add comprehensive tools
    addSystemTools(s)
    addFileTools(s)
    addGitTools(s)
    addDatabaseTools(s)
 
    // Handle graceful shutdown
    setupGracefulShutdown(s)
 
    // Start with error handling
    if err := server.ServeStdio(s); err != nil {
        logError(fmt.Sprintf("Server error: %v", err))
        os.Exit(1)
    }
}
 
// Helper functions for the advanced example
func logToFile(message string) {
    // Placeholder implementation
    log.Println(message)
}
 
func logError(message string) {
    // Placeholder implementation
    log.Printf("ERROR: %s", message)
}
 
func addSystemTools(s *server.MCPServer) {
    // Placeholder implementation
}
 
func addFileTools(s *server.MCPServer) {
    // Placeholder implementation
}
 
func addGitTools(s *server.MCPServer) {
    // Placeholder implementation
}
 
func addDatabaseTools(s *server.MCPServer) {
    // Placeholder implementation
}
 
func setupGracefulShutdown(s *server.MCPServer) {
    c := make(chan os.Signal, 1)
    signal.Notify(c, os.Interrupt, syscall.SIGTERM)
    
    go func() {
        <-c
        logToFile("Received shutdown signal")
        
        ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
        defer cancel()
        
        if err := s.Shutdown(ctx); err != nil {
            logError(fmt.Sprintf("Shutdown error: %v", err))
        }
        
        os.Exit(0)
    }()
}

Client Integration

How LLM Applications Connect

LLM applications typically connect to STDIO MCP servers by:

  1. Spawning the process: Starting your server as a subprocess
  2. Pipe communication: Using stdin/stdout for JSON-RPC messages
  3. Lifecycle management: Handling process startup, shutdown, and errors

Claude Desktop Integration

Configure your STDIO server in Claude Desktop:

{
  "mcpServers": {
    "file-tools": {
      "command": "go",
      "args": ["run", "/path/to/your/server/main.go"],
      "env": {
        "LOG_LEVEL": "info"
      }
    }
  }
}

macOS: ~/Library/Application Support/Claude/claude_desktop_config.json Windows: %APPDATA%\Claude\claude_desktop_config.json

Custom Client Integration

package main
 
import (
    "context"
    "log"
 
    "github.com/mark3labs/mcp-go/client"
)
 
func main() {
    // Create STDIO client
    c, err := client.NewStdioClient(
        "go", "run", "/path/to/server/main.go",
    )
    if err != nil {
        log.Fatal(err)
    }
    defer c.Close()
 
    ctx := context.Background()
 
    // Initialize connection
    _, err = c.Initialize(ctx, mcp.InitializeRequest{
        Params: mcp.InitializeRequestParams{
            ProtocolVersion: "2024-11-05",
            Capabilities: mcp.ClientCapabilities{
                Tools: &mcp.ToolsCapability{},
            },
            ClientInfo: mcp.Implementation{
                Name:    "test-client",
                Version: "1.0.0",
            },
        },
    })
    if err != nil {
        log.Fatal(err)
    }
 
    // List available tools
    tools, err := c.ListTools(ctx, mcp.ListToolsRequest{})
    if err != nil {
        log.Fatal(err)
    }
 
    log.Printf("Available tools: %d", len(tools.Tools))
    for _, tool := range tools.Tools {
        log.Printf("- %s: %s", tool.Name, tool.Description)
    }
 
    // Call a tool
    result, err := c.CallTool(ctx, mcp.CallToolRequest{
        Params: mcp.CallToolParams{
            Name: "list_files",
            Arguments: map[string]interface{}{
                "path":      ".",
                "recursive": false,
            },
        },
    })
    if err != nil {
        log.Fatal(err)
    }
 
    log.Printf("Tool result: %+v", result)
}

Debugging

Command Line Testing

Test your STDIO server directly from the command line:

# Start your server
go run main.go
 
# Send initialization request
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{"tools":{}},"clientInfo":{"name":"test","version":"1.0.0"}}}' | go run main.go
 
# List tools
echo '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' | go run main.go
 
# Call a tool
echo '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"list_files","arguments":{"path":".","recursive":false}}}' | go run main.go

Interactive Testing Script

#!/bin/bash
 
# interactive_test.sh
SERVER_CMD="go run main.go"
 
echo "Starting MCP STDIO server test..."
 
# Function to send JSON-RPC request
send_request() {
    local request="$1"
    echo "Sending: $request"
    echo "$request" | $SERVER_CMD
    echo "---"
}
 
# Initialize
send_request '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{"tools":{}},"clientInfo":{"name":"test","version":"1.0.0"}}}'
 
# List tools
send_request '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'
 
# List resources
send_request '{"jsonrpc":"2.0","id":3,"method":"resources/list","params":{}}'
 
# Call tool
send_request '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"list_files","arguments":{"path":".","recursive":false}}}'
 
echo "Test completed."

Debug Logging

Add debug logging to your STDIO server:

func main() {
    // Setup debug logging
    logFile, err := os.OpenFile("mcp-server.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
    if err != nil {
        log.Fatal(err)
    }
    defer logFile.Close()
 
    logger := log.New(logFile, "[MCP] ", log.LstdFlags|log.Lshortfile)
 
    s := server.NewMCPServer("Debug Server", "1.0.0",
        server.WithToolCapabilities(true),
        server.WithLogging(),
    )
 
    // Add tools with debug logging
    s.AddTool(
        mcp.NewTool("debug_echo",
            mcp.WithDescription("Echo with debug logging"),
            mcp.WithString("message", mcp.Required()),
        ),
        func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
            message := req.GetString("message", "")
            logger.Printf("Echo tool called with message: %s", message)
            return mcp.NewToolResultText(fmt.Sprintf("Echo: %s", message)), nil
        },
    )
 
    logger.Println("Starting STDIO server...")
    if err := server.ServeStdio(s); err != nil {
        logger.Printf("Server error: %v", err)
    }
}

MCP Inspector Integration

Use the MCP Inspector for visual debugging:

# Install MCP Inspector
npm install -g @modelcontextprotocol/inspector
 
# Run your server with inspector
mcp-inspector go run main.go

This opens a web interface where you can:

  • View available tools and resources
  • Test tool calls interactively
  • Inspect request/response messages
  • Debug protocol issues

Error Handling

Robust Error Handling

func handleToolWithErrors(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    // Validate required parameters
    path, err := req.RequireString("path")
    if err != nil {
        return nil, fmt.Errorf("path parameter is required and must be a string")
    }
 
    // Validate path security
    if !isValidPath(path) {
        return nil, fmt.Errorf("invalid or unsafe path: %s", path)
    }
 
    // Check if path exists
    if _, err := os.Stat(path); os.IsNotExist(err) {
        return nil, fmt.Errorf("path does not exist: %s", path)
    }
 
    // Handle context cancellation
    select {
    case <-ctx.Done():
        return nil, ctx.Err()
    default:
    }
 
    // Perform operation with timeout
    ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
    defer cancel()
 
    result, err := performOperation(ctx, path)
    if err != nil {
        // Log error for debugging
        logError(fmt.Sprintf("Operation failed for path %s: %v", path, err))
        
        // Return user-friendly error
        if errors.Is(err, context.DeadlineExceeded) {
            return nil, fmt.Errorf("operation timed out")
        }
        
        return nil, fmt.Errorf("operation failed: %w", err)
    }
 
    return mcp.NewToolResultText(fmt.Sprintf("%v", result)), nil
}

Process Management

func main() {
    // Handle panics gracefully
    defer func() {
        if r := recover(); r != nil {
            logError(fmt.Sprintf("Server panic: %v", r))
            os.Exit(1)
        }
    }()
 
    s := server.NewMCPServer("Robust Server", "1.0.0",
        server.WithRecovery(), // Built-in panic recovery
    )
 
    // Setup signal handling
    setupSignalHandling()
 
    // Start server with retry logic
    for attempts := 0; attempts < 3; attempts++ {
        if err := server.ServeStdio(s); err != nil {
            logError(fmt.Sprintf("Server attempt %d failed: %v", attempts+1, err))
            if attempts == 2 {
                os.Exit(1)
            }
            time.Sleep(time.Second * time.Duration(attempts+1))
        } else {
            break
        }
    }
}
 
func setupSignalHandling() {
    c := make(chan os.Signal, 1)
    signal.Notify(c, os.Interrupt, syscall.SIGTERM)
    
    go func() {
        sig := <-c
        logToFile(fmt.Sprintf("Received signal: %v", sig))
        os.Exit(0)
    }()
}

Performance Optimization

Efficient Resource Usage

// Use connection pooling for database tools
var dbPool *sql.DB
 
func init() {
    var err error
    dbPool, err = sql.Open("sqlite3", "data.db")
    if err != nil {
        log.Fatal(err)
    }
    
    dbPool.SetMaxOpenConns(10)
    dbPool.SetMaxIdleConns(5)
    dbPool.SetConnMaxLifetime(time.Hour)
}
 
// Cache frequently accessed data
var fileCache = make(map[string]cacheEntry)
var cacheMutex sync.RWMutex
 
type cacheEntry struct {
    content   string
    timestamp time.Time
}
 
func getCachedFile(path string) (string, bool) {
    cacheMutex.RLock()
    defer cacheMutex.RUnlock()
    
    entry, exists := fileCache[path]
    if !exists || time.Since(entry.timestamp) > 5*time.Minute {
        return "", false
    }
    
    return entry.content, true
}

Memory Management

func handleLargeFile(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    path := req.GetString("path", "")
    
    // Stream large files instead of loading into memory
    file, err := os.Open(path)
    if err != nil {
        return nil, err
    }
    defer file.Close()
 
    // Process in chunks
    const chunkSize = 64 * 1024
    buffer := make([]byte, chunkSize)
    
    var result strings.Builder
    for {
        n, err := file.Read(buffer)
        if err == io.EOF {
            break
        }
        if err != nil {
            return nil, err
        }
        
        // Process chunk
        processed := processChunk(buffer[:n])
        result.WriteString(processed)
        
        // Check for cancellation
        select {
        case <-ctx.Done():
            return nil, ctx.Err()
        default:
        }
    }
 
    return mcp.NewToolResultText(result.String()), nil
}

Next Steps