Skip to content

Implementing Resources

Resources expose data to LLMs in a read-only manner. Think of them as GET endpoints that provide access to files, databases, APIs, or any other data source.

Resource Fundamentals

Resources in MCP are identified by URIs and can be either static (fixed content) or dynamic (generated on-demand). They're perfect for giving LLMs access to documentation, configuration files, database records, or API responses.

Basic Resource Structure

// Create a simple resource
resource := mcp.NewResource(
    "docs://readme",           // URI - unique identifier
    "Project README",          // Name - human-readable
    mcp.WithResourceDescription("Main project documentation"),
    mcp.WithMIMEType("text/markdown"),
)

Static Resources

Static resources have fixed URIs and typically serve predetermined content.

File-Based Resources

Expose files from your filesystem:

func main() {
    s := server.NewMCPServer("File Server", "1.0.0",
        server.WithResourceCapabilities(true),
    )
 
    // Add a static file resource
    s.AddResource(
        mcp.NewResource(
            "file://README.md",
            "Project README",
            mcp.WithResourceDescription("Main project documentation"),
            mcp.WithMIMEType("text/markdown"),
        ),
        handleReadmeFile,
    )
 
    server.ServeStdio(s)
}
 
func handleReadmeFile(ctx context.Context, req mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
    content, err := os.ReadFile("README.md")
    if err != nil {
        return nil, fmt.Errorf("failed to read README: %w", err)
    }
 
    return &mcp.ReadResourceResult{
        Contents: []mcp.ResourceContent{
            {
                URI:      req.Params.URI,
                MIMEType: "text/markdown",
                Text:     string(content),
            },
        },
    }, nil
}

Configuration Resources

Expose application configuration:

// Configuration resource
s.AddResource(
    mcp.NewResource(
        "config://app",
        "Application Configuration", 
        mcp.WithResourceDescription("Current application settings"),
        mcp.WithMIMEType("application/json"),
    ),
    handleAppConfig,
)
 
func handleAppConfig(ctx context.Context, req mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
    config := map[string]interface{}{
        "database_url": os.Getenv("DATABASE_URL"),
        "debug_mode":   os.Getenv("DEBUG") == "true",
        "version":      "1.0.0",
        "features": []string{
            "authentication",
            "caching", 
            "logging",
        },
    }
 
    configJSON, err := json.Marshal(config)
    if err != nil {
        return nil, err
    }
    
    return &mcp.ReadResourceResult{
        Contents: []mcp.ResourceContent{
            mcp.TextResourceContent{
                URI:      req.Params.URI,
                MIMEType: "application/json",
                Text:     string(configJSON),
            },
        },
    }, nil
}

Dynamic Resources

Dynamic resources use URI templates with parameters, allowing for flexible, parameterized access to data.

URI Templates

Use {parameter} syntax for dynamic parts:

// User profile resource with dynamic user ID
s.AddResource(
    mcp.NewResource(
        "users://{user_id}",
        "User Profile",
        mcp.WithResourceDescription("User profile information"),
        mcp.WithMIMEType("application/json"),
    ),
    handleUserProfile,
)
 
func handleUserProfile(ctx context.Context, req mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
    // Extract user_id from URI
    userID := extractUserID(req.Params.URI) // "users://123" -> "123"
    
    // Fetch user data (from database, API, etc.)
    user, err := getUserFromDB(userID)
    if err != nil {
        return nil, fmt.Errorf("user not found: %w", err)
    }
 
    jsonData, err := json.Marshal(user)
    if err != nil {
        return nil, err
    }
    
    return []mcp.ResourceContents{
        mcp.TextResourceContents{
            URI:      req.Params.URI,
            MIMEType: "application/json",
            Text:     string(jsonData),
        },
    }, nil
}
 
func extractUserID(uri string) string {
    // Extract ID from "users://123" format
    parts := strings.Split(uri, "://")
    if len(parts) == 2 {
        return parts[1]
    }
    return ""
}

Database Resources

Expose database records dynamically:

import (
    "context"
    "database/sql"
    "encoding/json"
    "fmt"
 
    "github.com/mark3labs/mcp-go/mcp"
    "github.com/mark3labs/mcp-go/server"
)
 
// Database table resource
s.AddResource(
    mcp.NewResource(
        "db://{table}/{id}",
        "Database Record",
        mcp.WithResourceDescription("Access database records by table and ID"),
        mcp.WithMIMEType("application/json"),
    ),
    handleDatabaseRecord,
)
 
func handleDatabaseRecord(ctx context.Context, req mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
    table, id := parseDBURI(req.Params.URI) // "db://users/123" -> "users", "123"
    
    // Validate table name for security
    allowedTables := map[string]bool{
        "users":    true,
        "products": true,
        "orders":   true,
    }
    
    if !allowedTables[table] {
        return nil, fmt.Errorf("table not accessible: %s", table)
    }
 
    // Query database
    query := fmt.Sprintf("SELECT * FROM %s WHERE id = ?", table)
    row := db.QueryRowContext(ctx, query, id)
    
    var data map[string]interface{}
    if err := scanRowToMap(row, &data); err != nil {
        return nil, fmt.Errorf("record not found: %w", err)
    }
 
    jsonData, err := json.Marshal(data)
    if err != nil {
        return nil, err
    }
    
    return &mcp.ReadResourceResult{
        Contents: []mcp.ResourceContent{
            mcp.TextResourceContent{
                URI:      req.Params.URI,
                MIMEType: "application/json",
                Text:     string(jsonData),
            },
        },
    }, nil
}
}

API Resources

Proxy external APIs through resources:

// Weather API resource
s.AddResource(
    mcp.NewResource(
        "weather://{location}",
        "Weather Data",
        mcp.WithResourceDescription("Current weather for a location"),
        mcp.WithMIMEType("application/json"),
    ),
    handleWeatherData,
)
 
func handleWeatherData(ctx context.Context, req mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
    location := extractLocation(req.Params.URI)
    
    // Call external weather API
    apiURL := fmt.Sprintf("https://api.weather.com/v1/current?location=%s&key=%s", 
        url.QueryEscape(location), os.Getenv("WEATHER_API_KEY"))
    
    resp, err := http.Get(apiURL)
    if err != nil {
        return nil, fmt.Errorf("weather API error: %w", err)
    }
    defer resp.Body.Close()
 
    body, err := io.ReadAll(resp.Body)
    if err != nil {
        return nil, fmt.Errorf("failed to read response: %w", err)
    }
 
    return &mcp.ReadResourceResult{
        Contents: []mcp.ResourceContent{
            {
                URI:      req.Params.URI,
                MIMEType: "application/json",
                Text:     string(body),
            },
        },
    }, nil
}

Content Types

Resources can serve different types of content with appropriate MIME types.

Text Content

func handleTextResource(ctx context.Context, req mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
    content := "This is plain text content"
    
    return &mcp.ReadResourceResult{
        Contents: []mcp.ResourceContent{
            {
                URI:      req.Params.URI,
                MIMEType: "text/plain",
                Text:     content,
            },
        },
    }, nil
}

JSON Content

func handleJSONResource(ctx context.Context, req mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
    data := map[string]interface{}{
        "message": "Hello, World!",
        "timestamp": time.Now().Unix(),
        "status": "success",
    }
    
    jsonData, err := json.Marshal(data)
    if err != nil {
        return nil, err
    }
    
    return []mcp.ResourceContents{
        mcp.TextResourceContents{
            URI:      req.Params.URI,
            MIMEType: "application/json",
            Text:     string(jsonData),
        },
    }, nil
}

Binary Content

func handleImageResource(ctx context.Context, req mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
    imageData, err := os.ReadFile("logo.png")
    if err != nil {
        return nil, err
    }
    
    // Encode binary data as base64
    encoded := base64.StdEncoding.EncodeToString(imageData)
    
    return &mcp.ReadResourceResult{
        Contents: []mcp.ResourceContent{
            {
                URI:      req.Params.URI,
                MIMEType: "image/png",
                Blob:     encoded,
            },
        },
    }, nil
}

Multiple Content Types

A single resource can return multiple content representations:

func handleMultiFormatResource(ctx context.Context, req mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
    data := map[string]interface{}{
        "name": "John Doe",
        "age":  30,
        "city": "New York",
    }
    
    // JSON representation
    jsonData, _ := json.Marshal(data)
    
    // Text representation  
    textData := fmt.Sprintf("Name: %s\nAge: %d\nCity: %s", 
        data["name"], data["age"], data["city"])
    
    return &mcp.ReadResourceResult{
        Contents: []mcp.ResourceContent{
            {
                URI:      req.Params.URI,
                MIMEType: "application/json",
                Text:     string(jsonData),
            },
            {
                URI:      req.Params.URI,
                MIMEType: "text/plain", 
                Text:     textData,
            },
        },
    }, nil
}

Error Handling

Proper error handling ensures robust resource access:

Common Error Patterns

func handleResourceWithErrors(ctx context.Context, req mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
    // Validate URI format
    if !isValidURI(req.Params.URI) {
        return nil, fmt.Errorf("invalid URI format: %s", req.Params.URI)
    }
    
    // Check permissions
    if !hasPermission(ctx, req.Params.URI) {
        return nil, fmt.Errorf("access denied to resource: %s", req.Params.URI)
    }
    
    // Handle resource not found
    data, err := fetchResourceData(req.Params.URI)
    if err != nil {
        if errors.Is(err, ErrResourceNotFound) {
            return nil, fmt.Errorf("resource not found: %s", req.Params.URI)
        }
        return nil, fmt.Errorf("failed to fetch resource: %w", err)
    }
    
    jsonData, err := json.Marshal(data)
    if err != nil {
        return nil, err
    }
    
    return &mcp.ReadResourceResult{
        Contents: []mcp.ResourceContents{
            mcp.TextResourceContents{
                URI:      req.Params.URI,
                MIMEType: "application/json",
                Text:     string(jsonData),
            },
        },
    }, nil
}

Timeout Handling

func handleResourceWithTimeout(ctx context.Context, req mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
    // Create timeout context
    ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
    defer cancel()
    
    // Use context in operations
    data, err := fetchDataWithContext(ctx, req.Params.URI)
    if err != nil {
        if errors.Is(err, context.DeadlineExceeded) {
            return nil, fmt.Errorf("resource fetch timeout: %s", req.Params.URI)
        }
        return nil, err
    }
    
    jsonData, err := json.Marshal(data)
    if err != nil {
        return nil, err
    }
    
    return &mcp.ReadResourceResult{
        Contents: []mcp.ResourceContent{
            mcp.TextResourceContent{
                URI:      req.Params.URI,
                MIMEType: "application/json",
                Text:     string(jsonData),
            },
        },
    }, nil
}
}

Resource Listing

Implement resource discovery for clients:

func main() {
    s := server.NewMCPServer("Resource Server", "1.0.0",
        server.WithResourceCapabilities(true),
    )
 
    // Add multiple resources
    resources := []struct {
        uri         string
        name        string
        description string
        mimeType    string
        handler     server.ResourceHandler
    }{
        {"docs://readme", "README", "Project documentation", "text/markdown", handleReadme},
        {"config://app", "App Config", "Application settings", "application/json", handleConfig},
        {"users://{id}", "User Profile", "User information", "application/json", handleUser},
    }
 
    for _, r := range resources {
        s.AddResource(
            mcp.NewResource(r.uri, r.name,
                mcp.WithResourceDescription(r.description),
                mcp.WithMIMEType(r.mimeType),
            ),
            r.handler,
        )
    }
 
    server.ServeStdio(s)
}

Caching Resources

Implement caching for expensive resources:

type CachedResourceHandler struct {
    cache map[string]cacheEntry
    mutex sync.RWMutex
    ttl   time.Duration
}
 
type cacheEntry struct {
    data      *mcp.ReadResourceResult
    timestamp time.Time
}
 
func (h *CachedResourceHandler) HandleResource(ctx context.Context, req mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
    h.mutex.RLock()
    if entry, exists := h.cache[req.Params.URI]; exists {
        if time.Since(entry.timestamp) < h.ttl {
            h.mutex.RUnlock()
            return entry.data, nil
        }
    }
    h.mutex.RUnlock()
 
    // Fetch fresh data
    data, err := h.fetchFreshData(ctx, req)
    if err != nil {
        return nil, err
    }
 
    // Cache the result
    h.mutex.Lock()
    h.cache[req.Params.URI] = cacheEntry{
        data:      data,
        timestamp: time.Now(),
    }
    h.mutex.Unlock()
 
    return data, nil
}

Advanced Resource Patterns

Session-specific Resources

You can add resources to a specific client session, allowing different clients to see different resources or override global resources with session-specific implementations.

Using Helper Functions (Recommended)

The server provides convenient helper functions that mirror the session tool helpers:

// Add a single session resource
userResource := mcp.NewResource(
    "user://profile", 
    "User Profile",
    mcp.WithResourceDescription("Current user's profile data"),
)
 
err := s.AddSessionResource(
    sessionID,
    userResource,
    func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
        // This handler is only available to this specific session
        return []mcp.ResourceContents{
            mcp.TextResourceContents{
                URI:      request.Params.URI,
                MIMEType: "application/json",
                Text:     getUserProfile(sessionID),
            },
        }, nil
    },
)
if err != nil {
    log.Printf("Failed to add session resource: %v", err)
}
 
// Add multiple session resources at once
err = s.AddSessionResources(
    sessionID,
    server.ServerResource{
        Resource: mcp.NewResource("user://settings", "User Settings"),
        Handler: func(ctx context.Context, req mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
            return getUserSettings(sessionID)
        },
    },
    server.ServerResource{
        Resource: mcp.NewResource("user://history", "User History"),
        Handler: func(ctx context.Context, req mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
            return getUserHistory(sessionID)
        },
    },
)
if err != nil {
    log.Printf("Failed to add session resources: %v", err)
}
 
// Delete session resources when no longer needed
err = s.DeleteSessionResources(sessionID, "user://profile", "user://settings")
if err != nil {
    log.Printf("Failed to delete session resources: %v", err)
}

Direct Interface Usage

You can also work directly with the SessionWithResources interface:

sseServer := server.NewSSEServer(
    s,
    server.WithAppendQueryToMessageEndpoint(),
    server.WithSSEContextFunc(func(ctx context.Context, r *http.Request) context.Context {
        withNewResources := r.URL.Query().Get("withNewResources")
        if withNewResources != "1" {
            return ctx
        }
 
        session := server.ClientSessionFromContext(ctx)
        if sessionWithResources, ok := session.(server.SessionWithResources); ok {
            // Add the new resources
            sessionWithResources.SetSessionResources(map[string]server.ServerResource{
                myNewResource.URI: {
                    Resource: myNewResource,
                    Handler:  myNewResourceHandler,
                },
            })
        }
 
        return ctx
    }),
)

Important Notes

  • Session resources override global resources with the same URI
  • Notifications (resources/list_changed) are automatically sent when resources are added/removed
  • The server automatically registers resource capabilities when session resources are first added
  • Operations are thread-safe and can be called concurrently
  • Resources are only available to initialized sessions unless explicitly added before initialization

Next Steps

  • Tools - Learn to implement interactive functionality
  • Prompts - Create reusable interaction templates
  • Advanced Features - Explore hooks, middleware, and more