Skip to main content

Model Context Protocol (MCP)

The Go-AI SDK supports connecting to Model Context Protocol (MCP) servers to access their tools, resources, and prompts. This enables your AI applications to discover and use capabilities across various services through a standardized interface.

Overview​

MCP (Model Context Protocol) is an open protocol that allows AI applications to:

  • Discover Tools: Automatically detect available functions from MCP servers
  • Execute Tools: Call remote functions and get structured results
  • Access Resources: Read data from various sources (files, databases, APIs)
  • Use Prompts: Retrieve reusable prompt templates

Installation​

The MCP package is included in the Go-AI SDK:

go get github.com/digitallysavvy/go-ai
import (
"github.com/digitallysavvy/go-ai/pkg/ai"
"github.com/digitallysavvy/go-ai/pkg/mcp"
)

Initializing an MCP Client​

We recommend using HTTP transport for production deployments. The stdio transport should only be used for connecting to local servers as it cannot be deployed to production environments.

For production deployments, use the HTTP transport:

import (
"context"
"github.com/digitallysavvy/go-ai/pkg/mcp"
)

ctx := context.Background()

// Create HTTP transport
transport := mcp.NewHTTPTransport(mcp.HTTPTransportConfig{
URL: "https://your-server.com/mcp",
TimeoutMS: 30000,

// Optional: Add custom headers (set on the base transport config)
Config: mcp.TransportConfig{
Headers: map[string]string{
"Authorization": "Bearer my-api-key",
},
},
})

// Create client
client := mcp.NewMCPClient(transport, mcp.MCPClientConfig{
ClientName: "my-go-app",
ClientVersion: "1.0.0",
})

// Connect
err := client.Connect(ctx)
if err != nil {
log.Fatal(err)
}
defer client.Close()

HTTP with OAuth Authentication​

For authenticated MCP servers:

transport := mcp.NewHTTPTransport(mcp.HTTPTransportConfig{
URL: "https://mcp.example.com",
TimeoutMS: 30000,
OAuth: &mcp.OAuthConfig{
ClientID: "your-client-id",
ClientSecret: "your-client-secret",
TokenURL: "https://auth.example.com/token",
},
})

client := mcp.NewMCPClient(transport, mcp.MCPClientConfig{
ClientName: "my-go-app",
ClientVersion: "1.0.0",
})

err := client.Connect(ctx)
if err != nil {
log.Fatal(err)
}
defer client.Close()

Custom OAuth Client Providers​

For full control over credential storage (e.g. a database-backed provider shared by multiple processes), implement mcp.OAuthClientProvider and drive the flow yourself with mcp.Auth.

If the server rotates its OAuth issuer, rediscovered authorization server metadata may no longer match the metadata that issued your stored credentials. mcp.Auth surfaces this as a permanent failure, detectable with mcp.IsAuthorizationServerMismatchError:

result, err := mcp.Auth(ctx, provider, mcp.AuthOptions{ServerURL: serverURL})
if mcp.IsAuthorizationServerMismatchError(err) {
// Treat as permanent: drop credentials and start a fresh authorization
// flow rather than retrying with the stale pin.
}

If your provider also implements mcp.OAuthTokenInvalidator, mcp.Auth identifies the exact token generation a failed refresh attempted, letting providers that share storage across clients delete only that stale generation instead of unconditionally clearing storage — so a concurrent refresh's newer tokens survive:

func (p *myOAuthProvider) InvalidateCredentialsForTokens(ctx context.Context, tokens mcp.OAuthTokens) error {
if p.tokens != nil && p.tokens.AccessToken == tokens.AccessToken && p.tokens.RefreshToken == tokens.RefreshToken {
p.tokens = nil
}
return nil
}

HTTP Error Handling​

HTTP transport failures return *mcp.MCPClientError with structured transport fields. Code remains the JSON-RPC application error code; StatusCode, URL, and ResponseBody describe the HTTP failure.

err := client.Connect(ctx)
if err != nil {
var clientErr *mcp.MCPClientError
if errors.As(err, &clientErr) && clientErr.StatusCode == http.StatusUnauthorized {
log.Printf("MCP auth failed for %s: %s", clientErr.URL, clientErr.ResponseBody)
return
}
log.Fatal(err)
}

Stdio Transport (Local Servers Only)​

note

The stdio transport should only be used for local development with MCP servers running as child processes.

// Create stdio transport for local Node.js MCP server
transport := mcp.NewStdioTransport(mcp.StdioTransportConfig{
Command: "node",
Args: []string{"server.mjs"},

// Optional: Set working directory
WorkingDir: "/path/to/mcp/server",

// Optional: Set environment variables
Env: []string{
"API_KEY=your-api-key",
},
})

client := mcp.NewMCPClient(transport, mcp.MCPClientConfig{
ClientName: "my-go-app",
ClientVersion: "1.0.0",
})

err := client.Connect(ctx)
if err != nil {
log.Fatal(err)
}
defer client.Close()

Closing the MCP Client​

After initialization, you should close the MCP client based on your usage pattern:

  • Short-lived usage: Close the client when the response is finished
  • Long-running clients: Keep the client open but ensure it's closed when the application terminates
// For short-lived usage
client := setupMCPClient()
defer client.Close()

result, err := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: model,
Tools: tools,
Prompt: "Your prompt here",
})

// Client closes automatically via defer
// For long-running applications
client := setupMCPClient()

// Register cleanup handler
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
<-c
client.Close()
os.Exit(0)
}()

// Use client throughout application lifecycle

Using MCP Tools​

The Go-AI SDK provides two approaches for working with MCP tools:

With schema discovery, all tools offered by the server are automatically listed and converted to Go-AI SDK tools:

import (
"context"
"os"

"github.com/digitallysavvy/go-ai/pkg/ai"
"github.com/digitallysavvy/go-ai/pkg/mcp"
"github.com/digitallysavvy/go-ai/pkg/providers/openai"
)

ctx := context.Background()

// Set up MCP client
transport := mcp.NewHTTPTransport(mcp.HTTPTransportConfig{
URL: "https://mcp.example.com",
})
client := mcp.NewMCPClient(transport, mcp.MCPClientConfig{
ClientName: "my-app",
ClientVersion: "1.0.0",
})

err := client.Connect(ctx)
if err != nil {
log.Fatal(err)
}
defer client.Close()

// Convert MCP tools to Go-AI SDK tools
converter := mcp.NewMCPToolConverter(client)
tools, err := converter.ConvertToGoAITools(ctx)
if err != nil {
log.Fatal(err)
}

// Use tools with AI model
openaiProvider := openai.New(openai.Config{APIKey: os.Getenv("OPENAI_API_KEY")})
model, _ := openaiProvider.LanguageModel("gpt-4")
result, err := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: model,
Prompt: "What's the weather in Brooklyn, NY?",
Tools: tools, // All MCP tools are available
// Allow a follow-up step so the model can turn the tool result into a
// final answer (GenerateText/StreamText default to a single step).
StopWhen: []ai.StopCondition{ai.IsStepCount(5)},
})

if err != nil {
log.Fatal(err)
}

fmt.Println(result.Text)

This approach is simpler and automatically stays in sync with server changes. However, you won't have compile-time type safety for tool parameters.

Approach 2: Schema Definition​

For better type safety and control, you can define specific tools and their schemas:

import (
"github.com/digitallysavvy/go-ai/pkg/ai"
"github.com/digitallysavvy/go-ai/pkg/provider/types"
)

// Define tool schema
weatherTool := types.Tool{
Name: "get-weather",
Description: "Get weather information for a location",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"location": map[string]string{
"type": "string",
"description": "City name",
},
"units": map[string]interface{}{
"type": "string",
"enum": []string{"celsius", "fahrenheit"},
"description": "Temperature units",
},
},
"required": []string{"location"},
},
Execute: func(ctx context.Context, args map[string]interface{}, _ types.ToolExecutionOptions) (interface{}, error) {
// Call MCP tool manually
result, err := client.CallTool(ctx, "get-weather", args)
if err != nil {
return nil, err
}

// Convert result
content, err := mcp.ConvertMCPContentToAISDK(result.Content)
if err != nil {
return nil, err
}
return content, nil
},
}

// Use with AI model
result, err := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: model,
Prompt: "What's the temperature in Paris?",
Tools: []types.Tool{weatherTool},
// Allow a follow-up step so the model can turn the tool result into a
// final answer (GenerateText/StreamText default to a single step).
StopWhen: []ai.StopCondition{ai.IsStepCount(5)},
})

This approach provides full type safety and lets you catch parameter mismatches during development.

Image Content Handling​

note

The Go-AI SDK automatically handles image content from MCP tools to prevent token explosions (200K+ tokens per image).

When MCP tools return images (screenshots, charts, diagrams), the SDK properly converts them to the AI SDK's native image format:

// MCP server returns tool result with image
// {
// "type": "image",
// "data": "iVBORw0KGgo...", // base64 data
// "mimeType": "image/png"
// }

// Automatically converted to ImageContent
// - Base64 decoded to bytes
// - Token usage: ~1K instead of 200K+
// - Cost savings: ~99% reduction

// Use tools that return images normally:
tools, _ := converter.ConvertToGoAITools(ctx)

result, err := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: model,
Prompt: "Create a sales chart and analyze it",
Tools: tools, // Tool may return image
StopWhen: []ai.StopCondition{ai.IsStepCount(5)},
})

// Images in results are properly handled
// No token explosion

Supported Image Formats​

The SDK handles all standard image formats:

  1. Base64 data: Raw base64-encoded image
  2. Data URLs: data:image/png;base64,...
  3. HTTP/HTTPS URLs: Remote image URLs

Performance Impact​

  • Without image handling: 200K+ tokens per image
  • With proper handling: ~1K tokens per image
  • Cost savings: ~$1.99 per image for GPT-4 Turbo (~99% reduction)

Using MCP Resources​

Resources are application-driven data sources that provide context to the model. Unlike tools (which are model-controlled), your application decides when to fetch and pass resources as context.

Listing Resources​

List all available resources from the MCP server:

resources, err := client.ListResources(ctx)
if err != nil {
log.Fatal(err)
}

for _, resource := range resources {
fmt.Printf("Resource: %s (%s)\n", resource.Name, resource.URI)
fmt.Printf(" Description: %s\n", resource.Description)
fmt.Printf(" MIME Type: %s\n", resource.MimeType)
}

Reading Resource Contents​

Read the contents of a specific resource by its URI:

resourceData, err := client.ReadResource(ctx, "file:///example/document.txt")
if err != nil {
log.Fatal(err)
}

// Use resource content in prompt
result, err := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: model,
Prompt: fmt.Sprintf("Summarize this document:\n\n%s",
resourceData.Contents[0].Text),
})

Listing Resource Templates​

Resource templates are dynamic URI patterns that allow flexible queries:

templates, err := client.ListResourceTemplates(ctx)
if err != nil {
log.Fatal(err)
}

for _, template := range templates.ResourceTemplates {
fmt.Printf("Template: %s\n", template.Name)
fmt.Printf(" URI Template: %s\n", template.URITemplate)
fmt.Printf(" Description: %s\n", template.Description)
}

Handling Elicitation Requests​

Some MCP servers pause a tool call to ask the client (your application) for additional user input mid-execution, using the elicitation/create request. Register a handler with OnElicitationRequest before calling Connect:

client.OnElicitationRequest(func(ctx context.Context, req mcp.ElicitationRequest) (mcp.ElicitResult, error) {
fmt.Println("Server asks:", req.Message)
// req.RequestedSchema describes the JSON Schema for the expected input.

answer := promptUserSomehow(req)
return mcp.ElicitResult{
Action: "accept", // or "decline" / "cancel"
Content: map[string]interface{}{"answer": answer},
}, nil
})

err := client.Connect(ctx)

If no handler is registered, the client rejects elicitation/create requests automatically. A handler error is reported through the client's OnError callback rather than propagating to the tool call.

Using MCP Prompts​

note

MCP Prompts is an experimental feature and may change in the future.

Prompts are user-controlled templates that servers expose for clients to list and retrieve with optional arguments.

Listing Prompts​

prompts, err := client.ListPrompts(ctx)
if err != nil {
log.Fatal(err)
}

for _, prompt := range prompts {
fmt.Printf("Prompt: %s\n", prompt.Name)
fmt.Printf(" Description: %s\n", prompt.Description)

if len(prompt.Arguments) > 0 {
fmt.Println(" Arguments:")
for _, arg := range prompt.Arguments {
fmt.Printf(" - %s (required: %v): %s\n",
arg.Name, arg.Required, arg.Description)
}
}
}

Getting a Prompt​

Retrieve prompt messages, optionally passing arguments:

prompt, err := client.GetPrompt(ctx, "code_review", map[string]interface{}{
"code": "func add(a, b int) int { return a + b }",
"language": "go",
})
if err != nil {
log.Fatal(err)
}

// Convert prompt messages to AI SDK messages
messages := make([]types.Message, len(prompt.Messages))
for i, msg := range prompt.Messages {
messages[i] = types.Message{
Role: types.MessageRole(msg.Role),
Content: []types.ContentPart{
types.TextContent{Text: msg.Content.Text},
},
}
}

// Use prompt messages with AI model
result, err := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: model,
Messages: messages,
})

Complete Example​

Here's a complete example that demonstrates all MCP features:

package main

import (
"context"
"fmt"
"log"
"os"
"os/signal"
"syscall"

"github.com/digitallysavvy/go-ai/pkg/ai"
"github.com/digitallysavvy/go-ai/pkg/mcp"
"github.com/digitallysavvy/go-ai/pkg/providers/openai"
)

func main() {
ctx := context.Background()

// 1. Set up MCP client with HTTP transport
transport := mcp.NewHTTPTransport(mcp.HTTPTransportConfig{
URL: os.Getenv("MCP_SERVER_URL"),
TimeoutMS: 30000,
Config: mcp.TransportConfig{
Headers: map[string]string{
"Authorization": "Bearer " + os.Getenv("MCP_API_KEY"),
},
},
})

client := mcp.NewMCPClient(transport, mcp.MCPClientConfig{
ClientName: "go-ai-example",
ClientVersion: "1.0.0",
})

err := client.Connect(ctx)
if err != nil {
log.Fatal(err)
}

// Ensure cleanup
defer client.Close()

// Handle interrupt signal
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
<-c
client.Close()
os.Exit(0)
}()

// 2. List available resources
resources, err := client.ListResources(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Println("Available resources:")
for _, r := range resources {
fmt.Printf(" - %s: %s\n", r.Name, r.Description)
}

// 3. Convert MCP tools to AI SDK tools
converter := mcp.NewMCPToolConverter(client)
tools, err := converter.ConvertToGoAITools(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("\nConverted %d MCP tools\n\n", len(tools))

// 4. Use tools with AI model
provider := openai.New(openai.Config{
APIKey: os.Getenv("OPENAI_API_KEY"),
})
model, err := provider.LanguageModel("gpt-4")
if err != nil {
log.Fatal(err)
}

maxSteps := 5 // Allow multiple tool calls
result, err := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: model,
Prompt: "Use the available tools to get the current weather " +
"in Brooklyn, New York and create a chart showing " +
"temperature trends",
Tools: tools,
MaxSteps: &maxSteps,
})

if err != nil {
log.Fatal(err)
}

// 5. Display results
fmt.Println("AI Response:")
fmt.Println(result.Text)

// Show tool calls made
if len(result.Steps) > 0 {
fmt.Println("\nTool Calls:")
for i, step := range result.Steps {
fmt.Printf(" Step %d:\n", i+1)
for _, call := range step.ToolCalls {
fmt.Printf(" - %s(%v)\n", call.ToolName, call.Arguments)
}
}
}

// Display usage statistics
fmt.Printf("\nUsage: %d input tokens, %d output tokens\n",
result.Usage.GetInputTokens(),
result.Usage.GetOutputTokens())
}

Error Handling​

Handle common MCP errors gracefully:

import (
"errors"

"github.com/digitallysavvy/go-ai/pkg/mcp"
)

// Connect with error handling
err := client.Connect(ctx)
if err != nil {
var transportErr *mcp.TransportError
if errors.As(err, &transportErr) {
log.Printf("Failed to connect to MCP server: %v", err)
// Retry or fallback logic
} else {
log.Fatal(err)
}
}

// Tool execution with error handling
result, err := client.CallTool(ctx, "get-weather", map[string]interface{}{
"location": "New York",
})

if err != nil {
var clientErr *mcp.MCPClientError
var timeoutErr *mcp.TimeoutError
if errors.As(err, &timeoutErr) {
log.Printf("Tool execution timed out: %v", err)
// Retry with longer timeout
} else if errors.As(err, &clientErr) {
log.Printf("Tool execution failed: %v", err)
// Handle tool failure
} else {
log.Fatal(err)
}
} else {
fmt.Printf("Tool result: %+v\n", result.Content)
}

Best Practices​

1. Use HTTP Transport in Production​

// ✅ Good: HTTP transport for production
transport := mcp.NewHTTPTransport(mcp.HTTPTransportConfig{
URL: "https://mcp.production.com",
})

// ❌ Avoid: Stdio transport in production (won't work)
transport := mcp.NewStdioTransport(mcp.StdioTransportConfig{
Command: "node", // Cannot deploy to cloud
})

2. Always Close the Client​

// ✅ Good: Use defer to ensure cleanup
client := setupMCPClient()
defer client.Close()

// ✅ Good: Handle signals for long-running apps
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
go func() {
<-c
client.Close()
os.Exit(0)
}()

3. Handle Context Cancellation​

// ✅ Good: Respect context cancellation
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

result, err := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: model,
Tools: tools,
Prompt: prompt,
})

if err != nil {
if ctx.Err() == context.DeadlineExceeded {
log.Println("Request timed out")
}
return err
}

4. Use Schema Discovery for Simplicity​

// ✅ Good: Simple schema discovery
converter := mcp.NewMCPToolConverter(client)
tools, err := converter.ConvertToGoAITools(ctx)

// Use all available tools
result, err := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: model,
Tools: tools,
StopWhen: []ai.StopCondition{ai.IsStepCount(5)},
Prompt: "Use whatever tools you need",
})

5. Monitor Token Usage​

// ✅ Good: Monitor usage to catch image token explosions
result, err := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: model,
Tools: tools,
StopWhen: []ai.StopCondition{ai.IsStepCount(5)},
Prompt: prompt,
})

if err != nil {
return err
}

// Check if image handling is working correctly
if result.Usage.GetInputTokens() > 100000 {
log.Printf("WARNING: High token usage (%d) - possible image handling issue",
result.Usage.GetInputTokens())
}

Testing​

The MCP package includes comprehensive tests:

# Run all MCP tests
go test ./pkg/mcp/... -v

# Run tests with coverage
go test ./pkg/mcp/... -cover

# Run specific test
go test ./pkg/mcp -run TestContentConversion -v

Troubleshooting​

Connection Failures​

If you cannot connect to an MCP server:

// Enable debug logging
transport := mcp.NewHTTPTransport(mcp.HTTPTransportConfig{
URL: "https://mcp.example.com",
Config: mcp.TransportConfig{
EnableLogging: true, // Enable verbose logging
},
})

// Check network connectivity
// - Verify the URL is correct
// - Check firewall settings
// - Test with curl: curl https://mcp.example.com/mcp

Tool Execution Failures​

If tools fail to execute:

// Check tool availability
tools, err := converter.ConvertToGoAITools(ctx)
if err != nil {
log.Fatal("Failed to convert tools:", err)
}

fmt.Printf("Available tools: %d\n", len(tools))
for _, tool := range tools {
fmt.Printf(" - %s: %s\n", tool.Name, tool.Description)
}

// Test tool manually
result, err := client.CallTool(ctx, "problem-tool", map[string]interface{}{})
if err != nil {
fmt.Printf("Tool error: %v\n", err)
} else {
fmt.Printf("Tool result: %+v\n", result.Content)
}

High Token Usage with Images​

If you're seeing unexpectedly high token usage:

// This should NOT happen with proper image handling
// but if you see 200K+ token usage, check:

// 1. Verify image conversion is working
result, err := client.CallTool(ctx, "create-chart", args)
if err != nil {
log.Fatal(err)
}

// 2. Inspect the result content
for _, content := range result.Content {
if content.Type == "image" {
fmt.Println("Image found - should be converted to bytes")
// If you see base64 text here, there's a problem
}
}

See Also​

May 2026 parity updates​

MCP JSON parsing uses secure parsing to avoid prototype-pollution style payloads when consuming remote server data. MCP clients expose server instructions and attach TypeScript-compatible MCP provider metadata to converted tools under the "mcp" key: clientName, toolName, optional title, and optional MCP Apps app metadata. The default client name is ai-sdk-mcp-client; ClientName overrides it, and Name remains as a deprecated alias.

MCP Apps hosts can advertise app-rendering support with mcp.MCPAppClientCapabilities(), inspect _meta.ui using GetMCPAppToolMeta, split tools with SplitMCPAppTools, and read ui:// app resources with ReadMCPAppResource. MCP tool and prompt content also accept resource_link parts, matching the current MCP schema.

Custom transports remain supported through the transport interfaces in pkg/mcp. Use explicit redirect handling for HTTP transports and prefer the default redirect error mode unless the server is trusted.

transport := mcp.NewHTTPTransport(mcp.HTTPTransportConfig{
URL: "https://mcp.example.com",
Config: mcp.TransportConfig{
Redirect: mcp.MCPRedirectError,
},
})