Skip to content

Client Operations

Learn how to use MCP clients to interact with servers through tools, resources, prompts, and subscriptions.

Listing Resources

Resources provide read-only access to data. Before reading resources, you typically need to discover what's available.

Basic Resource Listing

import (
    "base64"
    "context"
    "encoding/json"
    "fmt"
    "log"
    "reflect"
    "regexp"
    "strings"
    "sync"
    "time"
 
    "github.com/mark3labs/mcp-go/client"
    "github.com/mark3labs/mcp-go/mcp"
)
 
func listResources(ctx context.Context, c client.Client) error {
    // List all available resources
    resources, err := c.ListResources(ctx)
    if err != nil {
        return fmt.Errorf("failed to list resources: %w", err)
    }
 
    fmt.Printf("Available resources: %d\n", len(resources.Resources))
    for _, resource := range resources.Resources {
        fmt.Printf("- %s (%s): %s\n", 
            resource.URI, 
            resource.MIMEType, 
            resource.Name)
        
        if resource.Description != "" {
            fmt.Printf("  Description: %s\n", resource.Description)
        }
    }
 
    return nil
}

Filtered Resource Listing

func listResourcesByType(ctx context.Context, c client.Client, mimeType string) ([]mcp.Resource, error) {
    resources, err := c.ListResources(ctx)
    if err != nil {
        return nil, err
    }
 
    var filtered []mcp.Resource
    for _, resource := range resources.Resources {
        if resource.MIMEType == mimeType {
            filtered = append(filtered, resource)
        }
    }
 
    return filtered, nil
}
 
func listResourcesByPattern(ctx context.Context, c client.Client, pattern string) ([]mcp.Resource, error) {
    resources, err := c.ListResources(ctx)
    if err != nil {
        return nil, err
    }
 
    regex, err := regexp.Compile(pattern)
    if err != nil {
        return nil, fmt.Errorf("invalid pattern: %w", err)
    }
 
    var filtered []mcp.Resource
    for _, resource := range resources.Resources {
        if regex.MatchString(resource.URI) {
            filtered = append(filtered, resource)
        }
    }
 
    return filtered, nil
}
 
// Usage examples
func demonstrateResourceFiltering(ctx context.Context, c client.Client) {
    // Find all JSON resources
    jsonResources, err := listResourcesByType(ctx, c, "application/json")
    if err != nil {
        log.Printf("Error listing JSON resources: %v", err)
    } else {
        fmt.Printf("Found %d JSON resources\n", len(jsonResources))
    }
 
    // Find all user-related resources
    userResources, err := listResourcesByPattern(ctx, c, `users?://.*`)
    if err != nil {
        log.Printf("Error listing user resources: %v", err)
    } else {
        fmt.Printf("Found %d user resources\n", len(userResources))
    }
}

Reading Resources

Once you know what resources are available, you can read their content.

Basic Resource Reading

func readResource(ctx context.Context, c client.Client, uri string) (*mcp.ReadResourceResult, error) {
    result, err := c.ReadResource(ctx, mcp.ReadResourceRequest{
        Params: mcp.ReadResourceRequestParams{
            URI: uri,
        },
    })
    if err != nil {
        return nil, fmt.Errorf("failed to read resource %s: %w", uri, err)
    }
 
    return result, nil
}
 
func demonstrateResourceReading(ctx context.Context, c client.Client) {
    // List resources first
    resources, err := c.ListResources(ctx)
    if err != nil {
        log.Printf("Failed to list resources: %v", err)
        return
    }
 
    // Read each resource
    for _, resource := range resources.Resources {
        fmt.Printf("\nReading resource: %s\n", resource.URI)
        
        result, err := readResource(ctx, c, resource.URI)
        if err != nil {
            log.Printf("Failed to read resource %s: %v", resource.URI, err)
            continue
        }
 
        // Process resource contents
        for i, content := range result.Contents {
            fmt.Printf("Content %d:\n", i+1)
            fmt.Printf("  URI: %s\n", content.URI)
            fmt.Printf("  MIME Type: %s\n", content.MIMEType)
            
            if content.Text != "" {
                fmt.Printf("  Text: %s\n", truncateString(content.Text, 100))
            }
            
            if content.Blob != "" {
                fmt.Printf("  Blob: %d bytes\n", len(content.Blob))
            }
        }
    }
}
 
func truncateString(s string, maxLen int) string {
    if len(s) <= maxLen {
        return s
    }
    return s[:maxLen] + "..."
}

Typed Resource Reading

// Helper functions for common resource types
func readJSONResource(ctx context.Context, c client.Client, uri string) (map[string]interface{}, error) {
    result, err := readResource(ctx, c, uri)
    if err != nil {
        return nil, err
    }
 
    if len(result.Contents) == 0 {
        return nil, fmt.Errorf("no content in resource")
    }
 
    content := result.Contents[0]
    if content.MIMEType != "application/json" {
        return nil, fmt.Errorf("expected JSON, got %s", content.MIMEType)
    }
 
    var data map[string]interface{}
    if err := json.Unmarshal([]byte(content.Text), &data); err != nil {
        return nil, fmt.Errorf("failed to parse JSON: %w", err)
    }
 
    return data, nil
}
 
func readTextResource(ctx context.Context, c client.Client, uri string) (string, error) {
    result, err := readResource(ctx, c, uri)
    if err != nil {
        return "", err
    }
 
    if len(result.Contents) == 0 {
        return "", fmt.Errorf("no content in resource")
    }
 
    content := result.Contents[0]
    if !strings.HasPrefix(content.MIMEType, "text/") {
        return "", fmt.Errorf("expected text, got %s", content.MIMEType)
    }
 
    return content.Text, nil
}
 
func readBinaryResource(ctx context.Context, c client.Client, uri string) ([]byte, error) {
    result, err := readResource(ctx, c, uri)
    if err != nil {
        return nil, err
    }
 
    if len(result.Contents) == 0 {
        return nil, fmt.Errorf("no content in resource")
    }
 
    content := result.Contents[0]
    if content.Blob == "" {
        return nil, fmt.Errorf("no binary data in resource")
    }
 
    data, err := base64.StdEncoding.DecodeString(content.Blob)
    if err != nil {
        return nil, fmt.Errorf("failed to decode binary data: %w", err)
    }
 
    return data, nil
}

Resource Caching

type ResourceCache struct {
    cache map[string]cacheEntry
    mutex sync.RWMutex
    ttl   time.Duration
}
 
type cacheEntry struct {
    result    *mcp.ReadResourceResult
    timestamp time.Time
}
 
func NewResourceCache(ttl time.Duration) *ResourceCache {
    return &ResourceCache{
        cache: make(map[string]cacheEntry),
        ttl:   ttl,
    }
}
 
func (rc *ResourceCache) Get(uri string) (*mcp.ReadResourceResult, bool) {
    rc.mutex.RLock()
    defer rc.mutex.RUnlock()
 
    entry, exists := rc.cache[uri]
    if !exists || time.Since(entry.timestamp) > rc.ttl {
        return nil, false
    }
 
    return entry.result, true
}
 
func (rc *ResourceCache) Set(uri string, result *mcp.ReadResourceResult) {
    rc.mutex.Lock()
    defer rc.mutex.Unlock()
 
    rc.cache[uri] = cacheEntry{
        result:    result,
        timestamp: time.Now(),
    }
}
 
func (rc *ResourceCache) ReadResource(ctx context.Context, c client.Client, uri string) (*mcp.ReadResourceResult, error) {
    // Check cache first
    if cached, found := rc.Get(uri); found {
        return cached, nil
    }
 
    // Read from server
    result, err := readResource(ctx, c, uri)
    if err != nil {
        return nil, err
    }
 
    // Cache the result
    rc.Set(uri, result)
    return result, nil
}

Calling Tools

Tools provide functionality that can be invoked with parameters.

Basic Tool Calling

func callTool(ctx context.Context, c client.Client, name string, args map[string]interface{}) (*mcp.CallToolResult, error) {
    result, err := c.CallTool(ctx, mcp.CallToolRequest{
        Params: mcp.CallToolRequestParams{
            Name:      name,
            Arguments: args,
        },
    })
    if err != nil {
        return nil, fmt.Errorf("tool call failed: %w", err)
    }
 
    return result, nil
}
 
func demonstrateToolCalling(ctx context.Context, c client.Client) {
    // List available tools
    tools, err := c.ListTools(ctx)
    if err != nil {
        log.Printf("Failed to list tools: %v", err)
        return
    }
 
    fmt.Printf("Available tools: %d\n", len(tools.Tools))
    for _, tool := range tools.Tools {
        fmt.Printf("- %s: %s\n", tool.Name, tool.Description)
    }
 
    // Call a specific tool
    if len(tools.Tools) > 0 {
        tool := tools.Tools[0]
        fmt.Printf("\nCalling tool: %s\n", tool.Name)
 
        result, err := callTool(ctx, c, tool.Name, map[string]interface{}{
            "input": "example input",
            "format": "text",
        })
        if err != nil {
            log.Printf("Tool call failed: %v", err)
            return
        }
 
        fmt.Printf("Tool result:\n")
        for i, content := range result.Content {
            fmt.Printf("Content %d (%s): %s\n", i+1, content.Type, content.Text)
        }
    }
}

Tool Schema Validation

func validateToolArguments(tool mcp.Tool, args map[string]interface{}) error {
    schema := tool.InputSchema
    
    // Check required properties
    if schema.Required != nil {
        for _, required := range schema.Required {
            if _, exists := args[required]; !exists {
                return fmt.Errorf("missing required argument: %s", required)
            }
        }
    }
 
    // Validate argument types
    if schema.Properties != nil {
        for name, value := range args {
            propSchema, exists := schema.Properties[name]
            if !exists {
                return fmt.Errorf("unknown argument: %s", name)
            }
 
            if err := validateValue(value, propSchema); err != nil {
                return fmt.Errorf("invalid argument %s: %w", name, err)
            }
        }
    }
 
    return nil
}
 
func validateValue(value interface{}, schema map[string]any) error {
    schemaType, ok := schema["type"].(string)
    if !ok {
        return fmt.Errorf("schema missing type")
    }
    
    switch schemaType {
    case "string":
        if _, ok := value.(string); !ok {
            return fmt.Errorf("expected string, got %T", value)
        }
    case "number":
        if _, ok := value.(float64); !ok {
            return fmt.Errorf("expected number, got %T", value)
        }
    case "integer":
        if _, ok := value.(float64); !ok {
            return fmt.Errorf("expected integer, got %T", value)
        }
    case "boolean":
        if _, ok := value.(bool); !ok {
            return fmt.Errorf("expected boolean, got %T", value)
        }
    case "array":
        if _, ok := value.([]interface{}); !ok {
            return fmt.Errorf("expected array, got %T", value)
        }
    case "object":
        if _, ok := value.(map[string]interface{}); !ok {
            return fmt.Errorf("expected object, got %T", value)
        }
    }
 
    return nil
}
 
func callToolWithValidation(ctx context.Context, c client.Client, toolName string, args map[string]interface{}) (*mcp.CallToolResult, error) {
    // Get tool schema
    tools, err := c.ListTools(ctx)
    if err != nil {
        return nil, fmt.Errorf("failed to list tools: %w", err)
    }
 
    var tool *mcp.Tool
    for _, t := range tools.Tools {
        if t.Name == toolName {
            tool = &t
            break
        }
    }
 
    if tool == nil {
        return nil, fmt.Errorf("tool not found: %s", toolName)
    }
 
    // Validate arguments
    if err := validateToolArguments(*tool, args); err != nil {
        return nil, fmt.Errorf("argument validation failed: %w", err)
    }
 
    // Call tool
    return callTool(ctx, c, toolName, args)
}

Batch Tool Operations

type ToolCall struct {
    Name      string
    Arguments map[string]interface{}
}
 
type ToolResult struct {
    Call   ToolCall
    Result *mcp.CallToolResult
    Error  error
}
 
func callToolsBatch(ctx context.Context, c client.Client, calls []ToolCall) []ToolResult {
    results := make([]ToolResult, len(calls))
    
    // Use goroutines for concurrent calls
    var wg sync.WaitGroup
    for i, call := range calls {
        wg.Add(1)
        go func(index int, toolCall ToolCall) {
            defer wg.Done()
            
            result, err := callTool(ctx, c, toolCall.Name, toolCall.Arguments)
            results[index] = ToolResult{
                Call:   toolCall,
                Result: result,
                Error:  err,
            }
        }(i, call)
    }
    
    wg.Wait()
    return results
}
 
func demonstrateBatchToolCalls(ctx context.Context, c client.Client) {
    calls := []ToolCall{
        {
            Name: "get_weather",
            Arguments: map[string]interface{}{
                "location": "New York",
            },
        },
        {
            Name: "get_weather",
            Arguments: map[string]interface{}{
                "location": "London",
            },
        },
        {
            Name: "calculate",
            Arguments: map[string]interface{}{
                "operation": "add",
                "x":         10,
                "y":         20,
            },
        },
    }
 
    results := callToolsBatch(ctx, c, calls)
    
    for i, result := range results {
        fmt.Printf("Call %d (%s):\n", i+1, result.Call.Name)
        if result.Error != nil {
            fmt.Printf("  Error: %v\n", result.Error)
        } else {
            fmt.Printf("  Success: %+v\n", result.Result)
        }
    }
}

Using Prompts

Prompts provide reusable templates for LLM interactions.

Basic Prompt Usage

func getPrompt(ctx context.Context, c client.Client, name string, args map[string]interface{}) (*mcp.GetPromptResult, error) {
    result, err := c.GetPrompt(ctx, mcp.GetPromptRequest{
        Params: mcp.GetPromptRequestParams{
            Name:      name,
            Arguments: args,
        },
    })
    if err != nil {
        return nil, fmt.Errorf("failed to get prompt: %w", err)
    }
 
    return result, nil
}
 
func demonstratePromptUsage(ctx context.Context, c client.Client) {
    // List available prompts
    prompts, err := c.ListPrompts(ctx)
    if err != nil {
        log.Printf("Failed to list prompts: %v", err)
        return
    }
 
    fmt.Printf("Available prompts: %d\n", len(prompts.Prompts))
    for _, prompt := range prompts.Prompts {
        fmt.Printf("- %s: %s\n", prompt.Name, prompt.Description)
        
        if len(prompt.Arguments) > 0 {
            fmt.Printf("  Arguments:\n")
            for _, arg := range prompt.Arguments {
                fmt.Printf("    - %s: %s\n", arg.Name, arg.Description)
            }
        }
    }
 
    // Use a specific prompt
    if len(prompts.Prompts) > 0 {
        prompt := prompts.Prompts[0]
        fmt.Printf("\nUsing prompt: %s\n", prompt.Name)
 
        result, err := getPrompt(ctx, c, prompt.Name, map[string]interface{}{
            // Add appropriate arguments based on prompt schema
        })
        if err != nil {
            log.Printf("Failed to get prompt: %v", err)
            return
        }
 
        fmt.Printf("Prompt result:\n")
        fmt.Printf("Description: %s\n", result.Description)
        fmt.Printf("Messages: %d\n", len(result.Messages))
        
        for i, message := range result.Messages {
            fmt.Printf("Message %d (%s): %s\n", i+1, message.Role, message.Content.Text)
        }
    }
}

Prompt Template Processing

type PromptProcessor struct {
    client client.Client
}
 
func NewPromptProcessor(c client.Client) *PromptProcessor {
    return &PromptProcessor{client: c}
}
 
func (pp *PromptProcessor) ProcessPrompt(ctx context.Context, name string, args map[string]interface{}) ([]mcp.PromptMessage, error) {
    result, err := pp.client.GetPrompt(ctx, mcp.GetPromptRequest{
        Params: mcp.GetPromptRequestParams{
            Name:      name,
            Arguments: args,
        },
    })
    if err != nil {
        return nil, err
    }
 
    return result.Messages, nil
}
 
func (pp *PromptProcessor) BuildConversation(ctx context.Context, promptName string, args map[string]interface{}, userMessage string) ([]mcp.PromptMessage, error) {
    // Get prompt template
    messages, err := pp.ProcessPrompt(ctx, promptName, args)
    if err != nil {
        return nil, err
    }
 
    // Add user message
    messages = append(messages, mcp.PromptMessage{
        Role: "user",
        Content: mcp.TextContent(userMessage),
    })
 
    return messages, nil
}
 
func (pp *PromptProcessor) FormatForLLM(messages []mcp.PromptMessage) []map[string]interface{} {
    formatted := make([]map[string]interface{}, len(messages))
    
    for i, message := range messages {
        formatted[i] = map[string]interface{}{
            "role":    message.Role,
            "content": message.Content.Text,
        }
    }
    
    return formatted
}

Dynamic Prompt Generation

func generateCodeReviewPrompt(ctx context.Context, c client.Client, code, language string) ([]mcp.PromptMessage, error) {
    processor := NewPromptProcessor(c)
    
    return processor.ProcessPrompt(ctx, "code_review", map[string]interface{}{
        "code":     code,
        "language": language,
        "focus":    "best-practices",
    })
}
 
func generateDataAnalysisPrompt(ctx context.Context, c client.Client, datasetURI string, analysisType string) ([]mcp.PromptMessage, error) {
    processor := NewPromptProcessor(c)
    
    return processor.ProcessPrompt(ctx, "analyze_data", map[string]interface{}{
        "dataset_uri":   datasetURI,
        "analysis_type": analysisType,
        "focus_areas":   []string{"trends", "outliers", "correlations"},
    })
}
 
func demonstrateDynamicPrompts(ctx context.Context, c client.Client) {
    // Generate code review prompt
    codeReviewMessages, err := generateCodeReviewPrompt(ctx, c, 
        "func main() { fmt.Println(\"Hello\") }", 
        "go")
    if err != nil {
        log.Printf("Failed to generate code review prompt: %v", err)
    } else {
        fmt.Printf("Code review prompt: %d messages\n", len(codeReviewMessages))
    }
 
    // Generate data analysis prompt
    analysisMessages, err := generateDataAnalysisPrompt(ctx, c, 
        "dataset://sales_data", 
        "exploratory")
    if err != nil {
        log.Printf("Failed to generate analysis prompt: %v", err)
    } else {
        fmt.Printf("Data analysis prompt: %d messages\n", len(analysisMessages))
    }
}

Subscriptions

Some transports support subscriptions for receiving real-time notifications.

Basic Subscription Handling

func handleSubscriptions(ctx context.Context, c client.Client) {
    // Check if client supports subscriptions
    subscriber, ok := c.(client.Subscriber)
    if !ok {
        log.Println("Client does not support subscriptions")
        return
    }
 
    // Subscribe to notifications
    notifications, err := subscriber.Subscribe(ctx)
    if err != nil {
        log.Printf("Failed to subscribe: %v", err)
        return
    }
 
    // Handle notifications
    for {
        select {
        case notification := <-notifications:
            handleNotification(notification)
        case <-ctx.Done():
            log.Println("Subscription cancelled")
            return
        }
    }
}
 
func handleNotification(notification mcp.Notification) {
    switch notification.Method {
    case "notifications/progress":
        handleProgressNotification(notification)
    case "notifications/message":
        handleMessageNotification(notification)
    case "notifications/resources/updated":
        handleResourceUpdateNotification(notification)
    case "notifications/tools/updated":
        handleToolUpdateNotification(notification)
    default:
        log.Printf("Unknown notification: %s", notification.Method)
    }
}
 
func handleProgressNotification(notification mcp.Notification) {
    var progress mcp.ProgressNotification
    if err := json.Unmarshal(notification.Params, &progress); err != nil {
        log.Printf("Failed to parse progress notification: %v", err)
        return
    }
 
    fmt.Printf("Progress: %d/%d - %s\n", 
        progress.Progress, 
        progress.Total, 
        progress.Message)
}
 
func handleMessageNotification(notification mcp.Notification) {
    var message mcp.MessageNotification
    if err := json.Unmarshal(notification.Params, &message); err != nil {
        log.Printf("Failed to parse message notification: %v", err)
        return
    }
 
    fmt.Printf("Server message: %s\n", message.Text)
}
 
func handleResourceUpdateNotification(notification mcp.Notification) {
    log.Println("Resources updated, refreshing cache...")
    // Invalidate resource cache or refresh resource list
}
 
func handleToolUpdateNotification(notification mcp.Notification) {
    log.Println("Tools updated, refreshing tool list...")
    // Refresh tool list
}

Advanced Subscription Management

type SubscriptionManager struct {
    client        client.Client
    subscriber    client.Subscriber
    notifications chan mcp.Notification
    handlers      map[string][]NotificationHandler
    ctx           context.Context
    cancel        context.CancelFunc
    wg            sync.WaitGroup
    mutex         sync.RWMutex
}
 
type NotificationHandler func(mcp.Notification) error
 
func NewSubscriptionManager(c client.Client) (*SubscriptionManager, error) {
    subscriber, ok := c.(client.Subscriber)
    if !ok {
        return nil, fmt.Errorf("client does not support subscriptions")
    }
 
    ctx, cancel := context.WithCancel(context.Background())
 
    sm := &SubscriptionManager{
        client:     c,
        subscriber: subscriber,
        handlers:   make(map[string][]NotificationHandler),
        ctx:        ctx,
        cancel:     cancel,
    }
 
    return sm, nil
}
 
func (sm *SubscriptionManager) Start() error {
    notifications, err := sm.subscriber.Subscribe(sm.ctx)
    if err != nil {
        return fmt.Errorf("failed to subscribe: %w", err)
    }
 
    sm.notifications = notifications
 
    sm.wg.Add(1)
    go sm.handleNotifications()
 
    return nil
}
 
func (sm *SubscriptionManager) Stop() {
    sm.cancel()
    sm.wg.Wait()
}
 
func (sm *SubscriptionManager) AddHandler(method string, handler NotificationHandler) {
    sm.mutex.Lock()
    defer sm.mutex.Unlock()
 
    sm.handlers[method] = append(sm.handlers[method], handler)
}
 
func (sm *SubscriptionManager) RemoveHandler(method string, handler NotificationHandler) {
    sm.mutex.Lock()
    defer sm.mutex.Unlock()
 
    handlers := sm.handlers[method]
    for i, h := range handlers {
        if reflect.ValueOf(h).Pointer() == reflect.ValueOf(handler).Pointer() {
            sm.handlers[method] = append(handlers[:i], handlers[i+1:]...)
            break
        }
    }
}
 
func (sm *SubscriptionManager) handleNotifications() {
    defer sm.wg.Done()
 
    for {
        select {
        case notification := <-sm.notifications:
            sm.processNotification(notification)
        case <-sm.ctx.Done():
            return
        }
    }
}
 
func (sm *SubscriptionManager) processNotification(notification mcp.Notification) {
    sm.mutex.RLock()
    handlers := sm.handlers[notification.Method]
    sm.mutex.RUnlock()
 
    for _, handler := range handlers {
        if err := handler(notification); err != nil {
            log.Printf("Handler error for %s: %v", notification.Method, err)
        }
    }
}
 
// Usage example
func demonstrateSubscriptionManager(c client.Client) {
    sm, err := NewSubscriptionManager(c)
    if err != nil {
        log.Printf("Failed to create subscription manager: %v", err)
        return
    }
 
    // Add handlers
    sm.AddHandler("notifications/progress", func(n mcp.Notification) error {
        log.Printf("Progress notification: %+v", n)
        return nil
    })
 
    sm.AddHandler("notifications/message", func(n mcp.Notification) error {
        log.Printf("Message notification: %+v", n)
        return nil
    })
 
    // Start handling
    if err := sm.Start(); err != nil {
        log.Printf("Failed to start subscription manager: %v", err)
        return
    }
 
    // Let it run for a while
    time.Sleep(30 * time.Second)
 
    // Stop
    sm.Stop()
}

Next Steps