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
- Client Basics - Creating and managing client lifecycle
- Client Operations - Using tools, resources, and prompts
- Client Transports - Transport-specific client implementations
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:
- Client Basics - Client lifecycle and error handling
- Client Operations - Tools, resources, and prompts
- Client Transports - Transport-specific implementations