Client Basics
Learn the fundamentals of creating and managing MCP clients, including lifecycle management, initialization, and error handling.
Creating Clients
MCP-Go provides client constructors for each supported transport. The choice of transport determines how your client communicates with the server.
Client Constructor Patterns
// STDIO client - for command-line tools
client, err := client.NewStdioMCPClient("command", "arg1", "arg2")
// StreamableHTTP client - for web services
client := client.NewStreamableHttpClient("http://localhost:8080/mcp")
// SSE client - for real-time web applications
client := client.NewSSEMCPClient("http://localhost:8080/mcp/sse")
// In-process client - for testing and embedded scenarios
client := client.NewInProcessClient(server)
STDIO Client Creation
package main
import (
"context"
"errors"
"fmt"
"log"
"math"
"net/http"
"sync"
"time"
"github.com/mark3labs/mcp-go/client"
"github.com/mark3labs/mcp-go/mcp"
)
func createStdioClient() (client.Client, error) {
// Create client that spawns a subprocess
c, err := client.NewStdioMCPClient(
"go", []string{}, "run", "/path/to/server/main.go",
)
if err != nil {
return nil, fmt.Errorf("failed to create STDIO client: %w", err)
}
return c, nil
}
// With custom environment variables
func createStdioClientWithEnv() (client.Client, error) {
env := []string{
"LOG_LEVEL=debug",
"DATABASE_URL=sqlite://test.db",
}
c, err := client.NewStdioMCPClient(
"go", env, "run", "/path/to/server/main.go",
)
if err != nil {
return nil, fmt.Errorf("failed to create STDIO client: %w", err)
}
return c, nil
}
StreamableHTTP Client Creation
func createStreamableHTTPClient() client.Client {
// Basic StreamableHTTP client
httpTransport, err := transport.NewStreamableHTTP(server.URL,
// Set timeout
transport.WithHTTPTimeout(30*time.Second),
// Set custom headers
transport.WithHTTPHeaders(map[string]string{
"X-Custom-Header": "custom-value",
"Y-Another-Header": "another-value",
}),
// With custom HTTP client
transport.WithHTTPBasicClient(&http.Client{}),
)
if err != nil {
log.Fatalf("Failed to create StreamableHTTP transport: %v", err)
}
c := client.NewClient(httpTransport)
return c
}
SSE Client Creation
func createSSEClient() client.Client {
// Basic SSE client
c, err := NewSSEMCPClient(testServer.URL+"/sse",
// Set custom headers
WithHeaders(map[string]string{
"X-Custom-Header": "custom-value",
"Y-Another-Header": "another-value",
}),
)
return c
}
Client Lifecycle
Understanding the client lifecycle is crucial for proper resource management and error handling.
Lifecycle Stages
- Creation - Instantiate the client
- Initialization - Establish connection and exchange capabilities
- Operation - Use tools, resources, and prompts
- Cleanup - Close connections and free resources
Complete Lifecycle Example
func demonstrateClientLifecycle() error {
// 1. Creation
c, err := client.NewSSEMCPClient("server-command")
if err != nil {
return fmt.Errorf("client creation failed: %w", err)
}
// Ensure cleanup happens
defer func() {
if closeErr := c.Close(); closeErr != nil {
log.Printf("Error closing client: %v", closeErr)
}
}()
ctx := context.Background()
// 2. Initialization
if err := c.Initialize(ctx); err != nil {
return fmt.Errorf("client initialization failed: %w", err)
}
// 3. Operation
if err := performClientOperations(ctx, c); err != nil {
return fmt.Errorf("client operations failed: %w", err)
}
// 4. Cleanup (handled by defer)
return nil
}
func performClientOperations(ctx context.Context, c client.Client) error {
// List available tools
tools, err := c.ListTools(ctx)
if err != nil {
return err
}
log.Printf("Found %d tools", len(tools.Tools))
// Use the tools
for _, tool := range tools.Tools {
result, err := c.CallTool(ctx, mcp.CallToolRequest{
Params: mcp.CallToolRequestParams{
Name: tool.Name,
Arguments: map[string]interface{}{
"input": "example input",
"format": "json",
},
},
})
if err != nil {
log.Printf("Tool %s failed: %v", tool.Name, err)
continue
}
log.Printf("Tool %s result: %+v", tool.Name, result)
}
return nil
}
Initialization Process
The initialization process establishes the MCP connection and exchanges capabilities:
func initializeClientWithDetails(ctx context.Context, c client.Client) error {
// Initialize with custom client info
initReq := mcp.InitializeRequest{
Params: mcp.InitializeRequestParams{
ProtocolVersion: "2024-11-05",
Capabilities: mcp.ClientCapabilities{
Tools: &mcp.ToolsCapability{},
Resources: &mcp.ResourcesCapability{},
Prompts: &mcp.PromptsCapability{},
},
ClientInfo: mcp.ClientInfo{
Name: "My Application",
Version: "1.0.0",
},
},
}
result, err := c.InitializeWithRequest(ctx, initReq)
if err != nil {
return fmt.Errorf("initialization failed: %w", err)
}
log.Printf("Connected to server: %s v%s",
result.ServerInfo.Name,
result.ServerInfo.Version)
log.Printf("Server capabilities: %+v", result.Capabilities)
return nil
}
Graceful Shutdown
type ManagedClient struct {
client client.Client
ctx context.Context
cancel context.CancelFunc
done chan struct{}
}
func NewManagedClient(clientType, address string) (*ManagedClient, error) {
var c client.Client
var err error
switch clientType {
case "stdio":
c, err = client.NewSSEMCPClient("server-command")
case "streamablehttp":
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())
mc := &ManagedClient{
client: c,
ctx: ctx,
cancel: cancel,
done: make(chan struct{}),
}
// Initialize in background
go func() {
defer close(mc.done)
if err := c.Initialize(ctx); err != nil {
log.Printf("Client initialization failed: %v", err)
}
}()
return mc, nil
}
func (mc *ManagedClient) WaitForReady(timeout time.Duration) error {
select {
case <-mc.done:
return nil
case <-time.After(timeout):
return fmt.Errorf("client initialization timeout")
case <-mc.ctx.Done():
return mc.ctx.Err()
}
}
func (mc *ManagedClient) Close() error {
mc.cancel()
// Wait for initialization to complete or timeout
select {
case <-mc.done:
case <-time.After(5 * time.Second):
log.Println("Timeout waiting for client shutdown")
}
return mc.client.Close()
}
Error Handling
Proper error handling is essential for robust client applications.
Error Types
// Connection errors
var (
ErrConnectionFailed = errors.New("connection failed")
ErrConnectionLost = errors.New("connection lost")
ErrTimeout = errors.New("operation timeout")
)
// Protocol errors
var (
ErrInvalidResponse = errors.New("invalid response")
ErrProtocolViolation = errors.New("protocol violation")
ErrUnsupportedVersion = errors.New("unsupported protocol version")
)
// Operation errors
var (
ErrToolNotFound = errors.New("tool not found")
ErrResourceNotFound = errors.New("resource not found")
ErrInvalidArguments = errors.New("invalid arguments")
ErrPermissionDenied = errors.New("permission denied")
)
Comprehensive Error Handling
func handleClientErrors(ctx context.Context, c client.Client) {
result, err := c.CallTool(ctx, mcp.CallToolRequest{
Params: mcp.CallToolRequestParams{
Name: "example_tool",
Arguments: map[string]interface{}{
"param": "value",
},
},
})
if err != nil {
switch {
// Connection errors - may be recoverable
case errors.Is(err, client.ErrConnectionLost):
log.Println("Connection lost, attempting reconnect...")
if reconnectErr := reconnectClient(c); reconnectErr != nil {
log.Printf("Reconnection failed: %v", reconnectErr)
return
}
// Retry the operation
return handleClientErrors(ctx, c)
case errors.Is(err, client.ErrTimeout):
log.Println("Operation timed out, retrying with longer timeout...")
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
return handleClientErrors(ctx, c)
// Protocol errors - usually not recoverable
case errors.Is(err, client.ErrProtocolViolation):
log.Printf("Protocol violation: %v", err)
return
case errors.Is(err, client.ErrUnsupportedVersion):
log.Printf("Unsupported protocol version: %v", err)
return
// Operation errors - check and fix request
case errors.Is(err, client.ErrToolNotFound):
log.Printf("Tool not found: %v", err)
// Maybe list available tools and suggest alternatives
suggestAlternativeTools(ctx, c)
return
case errors.Is(err, client.ErrInvalidArguments):
log.Printf("Invalid arguments: %v", err)
// Maybe get tool schema and show required parameters
showToolSchema(ctx, c, "example_tool")
return
case errors.Is(err, client.ErrPermissionDenied):
log.Printf("Permission denied: %v", err)
// Maybe prompt for authentication
return
// Unknown errors
default:
log.Printf("Unexpected error: %v", err)
return
}
}
// Process successful result
log.Printf("Tool result: %+v", result)
}
func reconnectClient(c client.Client) error {
// Close existing connection
if err := c.Close(); err != nil {
log.Printf("Error closing client: %v", err)
}
// Wait before reconnecting
time.Sleep(1 * time.Second)
// Reinitialize
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
return c.Initialize(ctx)
}
func suggestAlternativeTools(ctx context.Context, c client.Client) {
tools, err := c.ListTools(ctx)
if err != nil {
log.Printf("Failed to list tools: %v", err)
return
}
log.Println("Available tools:")
for _, tool := range tools.Tools {
log.Printf("- %s: %s", tool.Name, tool.Description)
}
}
func showToolSchema(ctx context.Context, c client.Client, toolName string) {
tools, err := c.ListTools(ctx)
if err != nil {
log.Printf("Failed to list tools: %v", err)
return
}
for _, tool := range tools.Tools {
if tool.Name == toolName {
log.Printf("Tool schema for %s:", toolName)
log.Printf("Description: %s", tool.Description)
log.Printf("Input schema: %+v", tool.InputSchema)
return
}
}
log.Printf("Tool %s not found", toolName)
}
Retry Logic with Exponential Backoff
type RetryConfig struct {
MaxRetries int
InitialDelay time.Duration
MaxDelay time.Duration
BackoffFactor float64
RetryableErrors []error
}
func DefaultRetryConfig() RetryConfig {
return RetryConfig{
MaxRetries: 3,
InitialDelay: 1 * time.Second,
MaxDelay: 30 * time.Second,
BackoffFactor: 2.0,
RetryableErrors: []error{
client.ErrConnectionLost,
client.ErrTimeout,
client.ErrConnectionFailed,
},
}
}
func (rc RetryConfig) IsRetryable(err error) bool {
for _, retryableErr := range rc.RetryableErrors {
if errors.Is(err, retryableErr) {
return true
}
}
return false
}
func WithRetry[T any](ctx context.Context, config RetryConfig, operation func() (T, error)) (T, error) {
var lastErr error
var zero T
for attempt := 0; attempt <= config.MaxRetries; attempt++ {
result, err := operation()
if err == nil {
return result, nil
}
lastErr = err
// Don't retry non-retryable errors
if !config.IsRetryable(err) {
break
}
// Don't retry on last attempt
if attempt == config.MaxRetries {
break
}
// Calculate delay with exponential backoff
delay := time.Duration(float64(config.InitialDelay) * math.Pow(config.BackoffFactor, float64(attempt)))
if delay > config.MaxDelay {
delay = config.MaxDelay
}
log.Printf("Attempt %d failed, retrying in %v: %v", attempt+1, delay, err)
// Wait with context cancellation support
select {
case <-time.After(delay):
case <-ctx.Done():
return zero, ctx.Err()
}
}
return zero, fmt.Errorf("failed after %d attempts: %w", config.MaxRetries+1, lastErr)
}
// Usage example
func callToolWithRetry(ctx context.Context, c client.Client, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
config := DefaultRetryConfig()
return WithRetry(ctx, config, func() (*mcp.CallToolResult, error) {
return c.CallTool(ctx, req)
})
}
Context and Timeout Management
func demonstrateContextUsage(c client.Client) {
// Operation with timeout
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
result, err := c.CallTool(ctx, mcp.CallToolRequest{
Params: mcp.CallToolRequestParams{
Name: "long_running_tool",
Arguments: map[string]interface{}{
"duration": 60, // seconds
},
},
})
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
log.Println("Tool call timed out")
} else {
log.Printf("Tool call failed: %v", err)
}
return
}
log.Printf("Tool completed: %+v", result)
}
func demonstrateCancellation(c client.Client) {
ctx, cancel := context.WithCancel(context.Background())
// Start operation in goroutine
go func() {
result, err := c.CallTool(ctx, mcp.CallToolRequest{
Params: mcp.CallToolRequestParams{
Name: "long_running_tool",
},
})
if err != nil {
if errors.Is(err, context.Canceled) {
log.Println("Tool call was cancelled")
} else {
log.Printf("Tool call failed: %v", err)
}
return
}
log.Printf("Tool completed: %+v", result)
}()
// Cancel after 5 seconds
time.Sleep(5 * time.Second)
cancel()
// Wait a bit to see the cancellation
time.Sleep(1 * time.Second)
}
Connection Monitoring
Health Checks
type ClientHealthMonitor struct {
client client.Client
interval time.Duration
timeout time.Duration
healthy bool
mutex sync.RWMutex
}
func NewClientHealthMonitor(c client.Client, interval, timeout time.Duration) *ClientHealthMonitor {
return &ClientHealthMonitor{
client: c,
interval: interval,
timeout: timeout,
healthy: false,
}
}
func (chm *ClientHealthMonitor) Start(ctx context.Context) {
ticker := time.NewTicker(chm.interval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
chm.checkHealth(ctx)
}
}
}
func (chm *ClientHealthMonitor) checkHealth(ctx context.Context) {
ctx, cancel := context.WithTimeout(ctx, chm.timeout)
defer cancel()
// Try to list tools as a health check
_, err := chm.client.ListTools(ctx)
chm.mutex.Lock()
chm.healthy = (err == nil)
chm.mutex.Unlock()
if err != nil {
log.Printf("Health check failed: %v", err)
}
}
func (chm *ClientHealthMonitor) IsHealthy() bool {
chm.mutex.RLock()
defer chm.mutex.RUnlock()
return chm.healthy
}
Connection Recovery
type ResilientClient struct {
factory func() (client.Client, error)
client client.Client
mutex sync.RWMutex
recovering bool
}
func NewResilientClient(factory func() (client.Client, error)) *ResilientClient {
return &ResilientClient{
factory: factory,
}
}
func (rc *ResilientClient) ensureConnected(ctx context.Context) error {
rc.mutex.RLock()
if rc.client != nil && !rc.recovering {
rc.mutex.RUnlock()
return nil
}
rc.mutex.RUnlock()
rc.mutex.Lock()
defer rc.mutex.Unlock()
// Double-check after acquiring write lock
if rc.client != nil && !rc.recovering {
return nil
}
rc.recovering = true
defer func() { rc.recovering = false }()
// Close existing client if any
if rc.client != nil {
rc.client.Close()
}
// Create new client
newClient, err := rc.factory()
if err != nil {
return fmt.Errorf("failed to create client: %w", err)
}
// Initialize new client
if err := newClient.Initialize(ctx); err != nil {
newClient.Close()
return fmt.Errorf("failed to initialize client: %w", err)
}
rc.client = newClient
return nil
}
func (rc *ResilientClient) CallTool(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
if err := rc.ensureConnected(ctx); err != nil {
return nil, err
}
rc.mutex.RLock()
client := rc.client
rc.mutex.RUnlock()
result, err := client.CallTool(ctx, req)
if err != nil && isConnectionError(err) {
// Mark for recovery and retry once
rc.mutex.Lock()
rc.recovering = true
rc.mutex.Unlock()
if retryErr := rc.ensureConnected(ctx); retryErr != nil {
return nil, fmt.Errorf("recovery failed: %w", retryErr)
}
rc.mutex.RLock()
client = rc.client
rc.mutex.RUnlock()
return client.CallTool(ctx, req)
}
return result, err
}
func isConnectionError(err error) bool {
return errors.Is(err, client.ErrConnectionLost) ||
errors.Is(err, client.ErrConnectionFailed)
}
Next Steps
- Client Operations - Learn to use tools, resources, and prompts
- Client Transports - Explore transport-specific features