Skip to content

Building MCP Clients

Learn how to build MCP clients that connect to and interact with MCP servers. This section covers client creation, operations, and transport-specific implementations.

Overview

MCP clients connect to servers to access tools, resources, and prompts. MCP-Go provides client implementations for all supported transports, making it easy to integrate MCP functionality into your applications.

What You'll Learn

Quick Example

Here's a complete example showing how to create and use an MCP client:

package main
 
import (
    "context"
    "fmt"
    "log"
 
    "github.com/mark3labs/mcp-go/client"
    "github.com/mark3labs/mcp-go/mcp"
)
 
func main() {
    // Create STDIO client
    c, err := client.NewStdioMCPClient(
        "go", []string{} , "run", "/path/to/server/main.go",
    )
    if err != nil {
        log.Fatal(err)
    }
    defer c.Close()
 
    ctx := context.Background()
 
    // Initialize the connection
    if err := c.Initialize(ctx, initRequest); err != nil {
        log.Fatal(err)
    }
 
    // Discover available capabilities
    if err := demonstrateClientOperations(ctx, c); err != nil {
        log.Fatal(err)
    }
}
 
func demonstrateClientOperations(ctx context.Context, c client.Client) error {
    // List available tools
    tools, err := c.ListTools(ctx)
    if err != nil {
        return fmt.Errorf("failed to list tools: %w", err)
    }
 
    fmt.Printf("Available tools: %d\n", len(tools.Tools))
    for _, tool := range tools.Tools {
        fmt.Printf("- %s: %s\n", tool.Name, tool.Description)
    }
 
    // List available resources
    resources, err := c.ListResources(ctx)
    if err != nil {
        return fmt.Errorf("failed to list resources: %w", err)
    }
 
    fmt.Printf("\nAvailable resources: %d\n", len(resources.Resources))
    for _, resource := range resources.Resources {
        fmt.Printf("- %s: %s\n", resource.URI, resource.Name)
    }
 
    // Call a tool if available
    if len(tools.Tools) > 0 {
        tool := tools.Tools[0]
        fmt.Printf("\nCalling tool: %s\n", tool.Name)
 
        result, err := c.CallTool(ctx, mcp.CallToolRequest{
            Params: mcp.CallToolRequestParams{
                Name: tool.Name,
                Arguments: map[string]interface{}{
                    "input": "example input",
                    "format": "text",
                },
            },
        })
        if err != nil {
            return fmt.Errorf("tool call failed: %w", err)
        }
 
        fmt.Printf("Tool result: %+v\n", result)
    }
 
    // Read a resource if available
    if len(resources.Resources) > 0 {
        resource := resources.Resources[0]
        fmt.Printf("\nReading resource: %s\n", resource.URI)
 
        content, err := c.ReadResource(ctx, mcp.ReadResourceRequest{
            Params: mcp.ReadResourceRequestParams{
                URI: resource.URI,
            },
        })
        if err != nil {
            return fmt.Errorf("resource read failed: %w", err)
        }
 
        fmt.Printf("Resource content: %+v\n", content)
    }
 
    return nil
}

Client Types by Transport

STDIO Client

Best for:
  • Command-line applications
  • Desktop software integration
  • Local development and testing
  • Single-server connections
// Create STDIO client
client, err := client.NewStdioMCPClient("server-command", "arg1", "arg2")

StreamableHTTP Client

Best for:
  • Web applications
  • Microservice architectures
  • Load-balanced deployments
  • REST-like interactions
// Create StreamableHTTP client
client := client.NewStreamableHttpClient("http://localhost:8080/mcp")

SSE Client

Best for:
  • Real-time web applications
  • Browser-based interfaces
  • Streaming data scenarios
  • Multi-client environments
// Create SSE client
client := client.NewSSEMCPClient("http://localhost:8080/mcp/sse")

In-Process Client

Best for:
  • Testing and development
  • Embedded scenarios
  • High-performance applications
  • Library integrations
// Create in-process client
client := client.NewInProcessClient(server)

Common Client Patterns

Connection Management

import (
    "context"
    "errors"
    "fmt"
    "log"
    "sync"
    "time"
 
    "github.com/mark3labs/mcp-go/client"
    "github.com/mark3labs/mcp-go/mcp"
)
 
type MCPClientManager struct {
    client client.Client
    ctx    context.Context
    cancel context.CancelFunc
}
 
func NewMCPClientManager(clientType, address string) (*MCPClientManager, error) {
    var c client.Client
    var err error
 
    switch clientType {
    case "stdio":
        c, err = client.NewStdioMCPClient("server-command")
    case "http":
        c = client.NewStreamableHttpClient(address)
    case "sse":
        c = client.NewSSEMCPClient(address)
    default:
        return nil, fmt.Errorf("unknown client type: %s", clientType)
    }
 
    if err != nil {
        return nil, err
    }
 
    ctx, cancel := context.WithCancel(context.Background())
 
    manager := &MCPClientManager{
        client: c,
        ctx:    ctx,
        cancel: cancel,
    }
 
    // Initialize connection
    if err := c.Initialize(ctx); err != nil {
        cancel()
        return nil, fmt.Errorf("failed to initialize client: %w", err)
    }
 
    return manager, nil
}
 
func (m *MCPClientManager) Close() error {
    m.cancel()
    return m.client.Close()
}

Error Handling

func handleClientErrors(ctx context.Context, c client.Client) {
    // Tool call with error handling
    result, err := c.CallTool(ctx, mcp.CallToolRequest{
        Params: mcp.CallToolRequestParams{
            Name: "example_tool",
            Arguments: map[string]interface{}{
                "param": "value",
            },
        },
    })
 
    if err != nil {
        switch {
        case errors.Is(err, client.ErrConnectionLost):
            log.Println("Connection lost, attempting reconnect...")
            // Implement reconnection logic
        case errors.Is(err, client.ErrToolNotFound):
            log.Printf("Tool not found: %v", err)
        case errors.Is(err, client.ErrInvalidArguments):
            log.Printf("Invalid arguments: %v", err)
        default:
            log.Printf("Unexpected error: %v", err)
        }
        return
    }
 
    // Process successful result
    processToolResult(result)
}

Retry Logic

func callToolWithRetry(ctx context.Context, c client.Client, req mcp.CallToolRequest, maxRetries int) (*mcp.CallToolResult, error) {
    var lastErr error
 
    for attempt := 0; attempt <= maxRetries; attempt++ {
        result, err := c.CallTool(ctx, req)
        if err == nil {
            return result, nil
        }
 
        lastErr = err
 
        // Don't retry certain errors
        if errors.Is(err, client.ErrInvalidArguments) ||
           errors.Is(err, client.ErrToolNotFound) {
            break
        }
 
        // Exponential backoff
        if attempt < maxRetries {
            backoff := time.Duration(1<<attempt) * time.Second
            log.Printf("Attempt %d failed, retrying in %v: %v", attempt+1, backoff, err)
            
            select {
            case <-time.After(backoff):
            case <-ctx.Done():
                return nil, ctx.Err()
            }
        }
    }
 
    return nil, fmt.Errorf("failed after %d attempts: %w", maxRetries+1, lastErr)
}

Integration Patterns

LLM Application Integration

type LLMApplication struct {
    mcpClient client.Client
    llmClient LLMClient
}
 
func NewLLMApplication(mcpAddress string) (*LLMApplication, error) {
    mcpClient := client.NewStreamableHttpClient(mcpAddress)
    
    ctx := context.Background()
    if err := mcpClient.Initialize(ctx); err != nil {
        return nil, err
    }
 
    return &LLMApplication{
        mcpClient: mcpClient,
        llmClient: NewLLMClient(),
    }, nil
}
 
func (app *LLMApplication) ProcessUserQuery(ctx context.Context, query string) (string, error) {
    // Get available tools for the LLM
    tools, err := app.mcpClient.ListTools(ctx)
    if err != nil {
        return "", err
    }
 
    // Send query to LLM with available tools
    response, toolCalls, err := app.llmClient.Chat(ctx, query, tools.Tools)
    if err != nil {
        return "", err
    }
 
    // Execute any tool calls
    for _, toolCall := range toolCalls {
        result, err := app.mcpClient.CallTool(ctx, mcp.CallToolRequest{
            Params: mcp.CallToolRequestParams{
                Name:      toolCall.Name,
                Arguments: toolCall.Arguments,
            },
        })
        if err != nil {
            return "", fmt.Errorf("tool call failed: %w", err)
        }
 
        // Send tool result back to LLM
        response, err = app.llmClient.ContinueWithToolResult(ctx, toolCall.ID, result)
        if err != nil {
            return "", err
        }
    }
 
    return response, nil
}

Multi-Server Client

type MultiServerClient struct {
    clients map[string]client.Client
    mutex   sync.RWMutex
}
 
func NewMultiServerClient() *MultiServerClient {
    return &MultiServerClient{
        clients: make(map[string]client.Client),
    }
}
 
func (msc *MultiServerClient) AddServer(name, address string, clientType string) error {
    msc.mutex.Lock()
    defer msc.mutex.Unlock()
 
    var c client.Client
    var err error
 
    switch clientType {
    case "http":
        c = client.NewStreamableHttpClient(address)
    case "sse":
        c = client.NewSSEMCPClient(address)
    default:
        return fmt.Errorf("unsupported client type: %s", clientType)
    }
 
    ctx := context.Background()
    if err := c.Initialize(ctx); err != nil {
        return fmt.Errorf("failed to initialize client for %s: %w", name, err)
    }
 
    msc.clients[name] = c
    return nil
}
 
func (msc *MultiServerClient) CallTool(ctx context.Context, serverName, toolName string, args map[string]interface{}) (*mcp.CallToolResult, error) {
    msc.mutex.RLock()
    c, exists := msc.clients[serverName]
    msc.mutex.RUnlock()
 
    if !exists {
        return nil, fmt.Errorf("server not found: %s", serverName)
    }
 
    return c.CallTool(ctx, mcp.CallToolRequest{
        Params: mcp.CallToolRequestParams{
            Name:      toolName,
            Arguments: args,
        },
    })
}
 
func (msc *MultiServerClient) GetAllTools(ctx context.Context) (map[string][]mcp.Tool, error) {
    msc.mutex.RLock()
    defer msc.mutex.RUnlock()
 
    allTools := make(map[string][]mcp.Tool)
 
    for serverName, c := range msc.clients {
        tools, err := c.ListTools(ctx)
        if err != nil {
            return nil, fmt.Errorf("failed to get tools from %s: %w", serverName, err)
        }
        allTools[serverName] = tools.Tools
    }
 
    return allTools, nil
}

Next Steps

Explore each client topic in detail: