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"),
)Resource Icons
Resources can include icons for better visual identification:
resource := mcp.NewResource(
"docs://readme",
"Project README",
mcp.WithResourceDescription("Main project documentation"),
mcp.WithMIMEType("text/markdown"),
mcp.WithResourceIcons(
mcp.Icon{
Src: "https://example.com/icons/document.svg",
MIMEType: "image/svg+xml",
},
),
)Display Title
Resources and resource templates can carry a human-readable display title separate from the name. Clients show the title in file browsers and pickers; name remains a stable identifier suitable for programmatic lookup.
resource := mcp.NewResource(
"docs://readme",
"readme.md", // name (stable identifier)
mcp.WithResourceTitle("Project README"), // Shown in UI
mcp.WithResourceDescription("Main project documentation"),
mcp.WithMIMEType("text/markdown"),
)
// Same option exists on resource templates
template := mcp.NewResourceTemplate(
"users://{user_id}",
"user-profile",
mcp.WithTemplateTitle("User Profile"),
mcp.WithTemplateDescription("Profile information for a specific user"),
)If Title is empty, clients fall back to Name. The field serialises with omitempty, so resources that don't set a title are unchanged on the wire.
Resource Size
When the raw byte size of a resource is known in advance, set it so hosts can render file sizes and pre-budget context-window usage without fetching the body:
resource := mcp.NewResource(
"file:///var/log/app.log",
"app.log",
mcp.WithResourceTitle("Application Log"),
mcp.WithMIMEType("text/plain"),
mcp.WithResourceSize(2_048_576), // 2 MiB, measured before base64 encoding
)Size is exposed as *int64 on the Resource struct so an explicit zero-byte resource is distinguishable from an unknown size. Omit WithResourceSize entirely when the size is unknown.
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
}Resource Subscriptions
MCP lets clients subscribe to updates for individual resources via resources/subscribe, then cancel with resources/unsubscribe. The server later pushes notifications/resources/updated whenever a subscribed resource changes.
Enable the capability with the first argument to WithResourceCapabilities:
s := server.NewMCPServer("Resource Server", "1.0.0",
// (subscribe, listChanged)
server.WithResourceCapabilities(true, true),
)Once the subscribe capability is advertised, the server accepts resources/subscribe and resources/unsubscribe requests and acknowledges them with an empty result. If a client calls either method while the capability is disabled (or no resource capabilities are configured), the server responds with METHOD_NOT_FOUND.
Tracking subscriptions per session
The default handlers acknowledge the request but do not store any subscription state on their own. To remember which session is interested in which URI, implement the optional SessionWithResourceSubscriptions interface on your custom session type — the dispatcher will call into it automatically:
type mySession struct {
server.ClientSession
mu sync.Mutex
subs map[string]struct{}
}
func (s *mySession) SubscribeToResource(uri string) {
s.mu.Lock()
defer s.mu.Unlock()
if s.subs == nil {
s.subs = make(map[string]struct{})
}
s.subs[uri] = struct{}{}
}
func (s *mySession) UnsubscribeFromResource(uri string) {
s.mu.Lock()
defer s.mu.Unlock()
delete(s.subs, uri)
}
func (s *mySession) SubscribedResources() []string {
s.mu.Lock()
defer s.mu.Unlock()
out := make([]string, 0, len(s.subs))
for uri := range s.subs {
out = append(out, uri)
}
return out
}
func (s *mySession) IsSubscribedToResource(uri string) bool {
s.mu.Lock()
defer s.mu.Unlock()
_, ok := s.subs[uri]
return ok
}Duplicate subscribes and unsubscribes for unknown URIs are treated as no-ops, matching the spec.
Pushing resources/updated notifications
When a resource changes, deliver the update only to clients that asked for it. The simplest approach is to track sessions yourself via the OnRegisterSession / OnUnregisterSession hooks, then look up the ones subscribed to the URI you're updating and call SendNotificationToSpecificClient:
var (
sessionsMu sync.Mutex
sessions = map[string]server.ClientSession{}
)
hooks := &server.Hooks{}
hooks.AddOnRegisterSession(func(ctx context.Context, session server.ClientSession) {
sessionsMu.Lock()
defer sessionsMu.Unlock()
sessions[session.SessionID()] = session
})
hooks.AddOnUnregisterSession(func(ctx context.Context, session server.ClientSession) {
sessionsMu.Lock()
defer sessionsMu.Unlock()
delete(sessions, session.SessionID())
})
func notifyResourceUpdated(s *server.MCPServer, uri string) {
sessionsMu.Lock()
snapshot := make([]server.ClientSession, 0, len(sessions))
for _, sess := range sessions {
snapshot = append(snapshot, sess)
}
sessionsMu.Unlock()
for _, sess := range snapshot {
subs, ok := sess.(server.SessionWithResourceSubscriptions)
if !ok || !subs.IsSubscribedToResource(uri) {
continue
}
_ = s.SendNotificationToSpecificClient(
sess.SessionID(),
"notifications/resources/updated",
map[string]any{"uri": uri},
)
}
}Subscription hooks
If you only need to observe subscribe / unsubscribe traffic (for metrics, audit logging, or to drive an external pub/sub system) without maintaining per-session state, attach hooks:
hooks := &server.Hooks{}
hooks.AddBeforeSubscribe(func(ctx context.Context, id any, req *mcp.SubscribeRequest) {
log.Printf("subscribe: %s", req.Params.URI)
})
hooks.AddAfterUnsubscribe(func(ctx context.Context, id any, req *mcp.UnsubscribeRequest, _ *mcp.EmptyResult) {
log.Printf("unsubscribed: %s", req.Params.URI)
})
s := server.NewMCPServer("Resource Server", "1.0.0",
server.WithResourceCapabilities(true, true),
server.WithHooks(hooks),
)The matching AddAfterSubscribe and AddBeforeUnsubscribe hooks are also available. See Hooks for the broader hook contract.
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
