mcp-go Documentation

A Go implementation of the Model Context Protocol (MCP), enabling seamless integration between LLM applications and external data sources and tools.

9,172 stars Go Go 38 files · ~77,815 tokens
RAW Doc

Technical Documentation: mark3labs/mcp-go

ℹ️ Provenance: Multi-Tier Ingestion · CodeWiki Reference

Overview (`README.md`)

go
package main

import (
    "context"
    "fmt"

    "github.com/mark3labs/mcp-go/mcp"
    "github.com/mark3labs/mcp-go/server"
)

func main() {
    // Create a new MCP server
    s := server.NewMCPServer(
        "Demo 🚀",
        "1.0.0",
        server.WithToolCapabilities(false),
    )

    // Add tool
    tool := mcp.NewTool("hello_world",
        mcp.WithDescription("Say hello to someone"),
        mcp.WithString("name",
            mcp.Required(),
            mcp.Description("Name of the person to greet"),
        ),
    )

    // Add tool handler
    s.AddTool(tool, helloHandler)

    // Start the stdio server
    if err := server.ServeStdio(s); err != nil {
        fmt.Printf("Server error: %v\n", err)
    }
}

func helloHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    name, err := request.RequireString("name")
    if err != nil {
        return mcp.NewToolResultError(err.Error()), nil
    }

    return mcp.NewToolResultText(fmt.Sprintf("Hello, %s!", name)), nil
}

That's it!

MCP Go handles all the complex protocol details and server management, so you can focus on building great tools. It aims to be high-level and easy to use.

Key features:

  • Fast: High-level interface means less code and faster development
  • Simple: Build MCP servers with minimal boilerplate
  • Complete*: MCP Go aims to provide a full implementation of the core MCP specification

(*emphasis on aims)

🚨 🚧 🏗️ MCP Go is under active development, as is the MCP specification itself. Core features are working but some advanced capabilities are still in progress.

Table of Contents

Installation

bash
go get github.com/mark3labs/mcp-go

Quickstart

Let's create a simple MCP server that exposes a calculator tool and some data:

text
/* Detailed source-code truncated for AI context efficiency. */

What is MCP?

The Model Context Protocol (MCP) lets you build servers that expose data and functionality to LLM applications in a secure, standardized way. Think of it like a web API, but specifically designed for LLM interactions.

MCP servers can:

  • Expose data through Resources (think of these sort of like GET endpoints; they are used to load information into the LLM's context)
  • Provide functionality through Tools (sort of like POST endpoints; they are used to execute code or otherwise produce a side effect)
  • Define interaction patterns through Prompts (reusable templates for LLM interactions)
  • And more!

mcp-go implements the Model Context Protocol specification version 2025-11-25, with backward compatibility for versions 2025-06-18, 2025-03-26, and 2024-11-05.

Core Concepts

Server

Show Server Examples

The server is your core interface to the MCP protocol. It handles connection management, protocol compliance, and message routing:

go
// Create a basic server
s := server.NewMCPServer(
    "My Server",  // Server name
    "1.0.0",     // Version
)

// Start the server using stdio
if err := server.ServeStdio(s); err != nil {
    log.Fatalf("Server error: %v", err)
}

Resources

Show Resource Examples
Resources are how you expose data to LLMs. They can be anything - files, API responses, database queries, system information, etc. Resources can be:

  • Static (fixed URI)
  • Dynamic (using URI templates)

Here's a simple example of a static resource:

go
// Static resource example - exposing a README file
resource := mcp.NewResource(
    "docs://readme",
    "Project README",
    mcp.WithResourceDescription("The project's README file"), 
    mcp.WithMIMEType("text/markdown"),
)

// Add resource with its handler
s.AddResource(resource, func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
    content, err := os.ReadFile("README.md")
    if err != nil {
        return nil, err
    }
    
    return []mcp.ResourceContents{
        mcp.TextResourceContents{
            URI:      "docs://readme",
            MIMEType: "text/markdown",
            Text:     string(content),
        },
    }, nil
})

And here's an example of a dynamic resource using a template:

go
// Dynamic resource example - user profiles by ID
template := mcp.NewResourceTemplate(
    "users://{id}/profile",
    "User Profile",
    mcp.WithTemplateDescription("Returns user profile information"),
    mcp.WithTemplateMIMEType("application/json"),
)

// Add template with its handler
s.AddResourceTemplate(template, func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
    // Extract ID from the URI using regex matching
    // The server automatically matches URIs to templates
    userID := extractIDFromURI(request.Params.URI)
    
    profile, err := getUserProfile(userID)  // Your DB/API call here
    if err != nil {
        return nil, err
    }
    
    return []mcp.ResourceContents{
        mcp.TextResourceContents{
            URI:      request.Params.URI,
            MIMEType: "application/json",
            Text:     profile,
        },
    }, nil
})

The examples are simple but demonstrate the core concepts. Resources can be much more sophisticated - serving multiple contents, integrating with databases or external APIs, etc.

Tools

Show Tool Examples

Tools let LLMs take actions through your server. Unlike resources, tools are expected to perform computation and have side effects. They're similar to POST endpoints in a REST API.

Task-Augmented Tools

Task-augmented tools execute asynchronously and return results via polling. This is useful for long-running operations that would otherwise block or time out. Task tools support three modes:

  • TaskSupportForbidden (default): The tool cannot be invoked as a task
  • TaskSupportOptional: The tool can be invoked as a task or synchronously
  • TaskSupportRequired: The tool must be invoked as a task
text
/* Detailed source-code truncated for AI context efficiency. */

Task execution flow:

  1. Client calls tool with task parameter
  2. Server immediately returns task ID
  3. Tool executes asynchronously in the background
  4. Client polls tasks/result to retrieve the result
  5. Server sends task status notifications on completion

For optional task tools, the same tool can be called synchronously (without task parameter) or asynchronously (with task parameter):

go
// Tool with optional task support
analyzeTool := mcp.NewTool("analyze_data",
    mcp.WithDescription("Analyze data - can run sync or async"),
    mcp.WithTaskSupport(mcp.TaskSupportOptional),
    mcp.WithString("data", mcp.Required()),
)

// Use AddTaskTool for hybrid tools that support both modes
s.AddTaskTool(analyzeTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CreateTaskResult, error) {
    // This handler runs when called as a task
    data := request.GetString("data", "")
    result := analyzeData(data)
    
    return &mcp.CreateTaskResult{
        Task: mcp.Task{},
    }, nil
})

// The server automatically handles both sync and async invocations
// When called without task param: executes handler and returns immediately
// When called with task param: executes handler asynchronously

Limiting Concurrent Tasks

To prevent resource exhaustion, you can limit the number of concurrent running tasks:

go
s := server.NewMCPServer(
    "Task Server",
    "1.0.0",
    server.WithTaskCapabilities(true, true, true),
    server.WithMaxConcurrentTasks(10), // Allow up to 10 concurrent running tasks
)

When the limit is reached, new task creation requests will fail with an error. Completed, failed, or cancelled tasks don't count toward the limit - only tasks in "working" status. If WithMaxConcurrentTasks is not specified or set to 0, there is no limit on concurrent tasks.

For traditional synchronous tools that execute and return results immediately:

Simple calculation example:

go
calculatorTool := mcp.NewTool("calculate",
    mcp.WithDescription("Perform basic arithmetic calculations"),
    mcp.WithString("operation",
        mcp.Required(),
        mcp.Description("The arithmetic operation to perform"),
        mcp.Enum("add", "subtract", "multiply", "divide"),
    ),
    mcp.WithNumber("x",
        mcp.Required(),
        mcp.Description("First number"),
    ),
    mcp.WithNumber("y",
        mcp.Required(),
        mcp.Description("Second number"),
    ),
)

s.AddTool(calculatorTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    args := request.GetArguments()
    op := args["operation"].(string)
    x := args["x"].(float64)
    y := args["y"].(float64)

    var result float64
    switch op {
    case "add":
        result = x + y
    case "subtract":
        result = x - y
    case "multiply":
        result = x * y
    case "divide":
        if y == 0 {
            return mcp.NewToolResultError("cannot divide by zero"), nil
        }
        result = x / y
    }
    
    return mcp.FormatNumberResult(result), nil
})

HTTP request example:

text
/* Detailed source-code truncated for AI context efficiency. */

Tools can be used for any kind of computation or side effect:

  • Database queries
  • File operations
  • External API calls
  • Calculations
  • System operations

Each tool should:

  • Have a clear description
  • Validate inputs
  • Handle errors gracefully
  • Return structured responses
  • Use appropriate result types

Prompts

Show Prompt Examples

Prompts are reusable templates that help LLMs interact with your server effectively. They're like "best practices" encoded into your server. Here are some examples:

text
/* Detailed source-code truncated for AI context efficiency. */

Prompts can include:

  • System instructions
  • Required arguments
  • Embedded resources
  • Multiple messages
  • Different content types (text, images, etc.)
  • Custom URI schemes

Examples

For examples, see the examples/ directory.

Key examples include:

Extras

Transports

MCP-Go supports stdio, SSE and streamable-HTTP transport layers. For SSE transport, you can use SetConnectionLostHandler() to detect and handle disconnections for implementing reconnection logic.

Embedding StreamableHTTP in non-net/http frameworks

StreamableHTTPServer is an http.Handler, so it can be mounted in any
router that speaks net/http. To embed it in a framework that does not
go through net/http (e.g. fasthttp
or fiber) without buffering the response through an
adaptor, use the transport-agnostic Handle entry point:

go
func (s *StreamableHTTPServer) Handle(w HTTPResponseWriter, r *HTTPRequest)

HTTPRequest is a plain struct (Method, URL, Header, Body,
Context) and HTTPResponseWriter is a small interface (Header,
WriteHeader, Write, Flush, CanStream). Implementations whose
underlying transport cannot stream MUST return false from CanStream;
the server will then reject GET (SSE listening) with 405 Method Not Allowed and keep POST responses as buffered application/json instead of
upgrading to text/event-stream.

See the HTTP transport docs
for a full fasthttp/fiber adapter example. ServeHTTP is unchanged and
remains the conventional net/http entry point.

OAuth Protected Resource Metadata

Servers that require OAuth can advertise their authorization requirements
via the RFC 9728
/.well-known/oauth-protected-resource endpoint referenced by the
MCP authorization spec.
Use server.WithProtectedResourceMetadata (or
server.WithSSEProtectedResourceMetadata) to auto-mount the endpoint, or
server.NewProtectedResourceMetadataHandler to wire it into a custom router.
See the HTTP transport docs for examples.

go
httpServer := server.NewStreamableHTTPServer(mcpServer,
    server.WithProtectedResourceMetadata(server.ProtectedResourceMetadataConfig{
        Resource:             "https://my-mcp-server.com",
        AuthorizationServers: []string{"https://auth.example.com"},
        ScopesSupported:      []string{"mcp:read", "mcp:write"},
    }),
)

CORS for browser-based clients

Servers exposed to browser-based MCP clients can opt into Cross-Origin
Resource Sharing handling on either HTTP transport. CORS is disabled by
default; configure it explicitly via server.WithStreamableHTTPCORS or
server.WithSSECORS:

go
httpServer := server.NewStreamableHTTPServer(mcpServer,
    server.WithEndpointPath("/mcp"),
    server.WithStreamableHTTPCORS(
        server.WithCORSAllowedOrigins("https://my-ai-app.com", "http://localhost:3000"),
        server.WithCORSAllowCredentials(),
        server.WithCORSMaxAge(300),
    ),
)

The transport answers preflight (OPTIONS) requests directly and decorates
simple responses with the appropriate Access-Control-Allow-Origin,
Access-Control-Allow-Credentials, Access-Control-Expose-Headers and
Vary headers. Sensible defaults are used when the corresponding option is
omitted (GET, POST, DELETE, OPTIONS for methods; Content-Type, Mcp-Session-Id, Last-Event-ID, Authorization for request headers;
Mcp-Session-Id for exposed headers). Combining WithCORSAllowedOrigins("*")
with WithCORSAllowCredentials() echoes the request Origin to remain
spec-compliant.

DNS rebinding protection for localhost servers

Both HTTP transports automatically protect local servers against
DNS rebinding attacks:
requests arriving over a loopback connection (127.0.0.1, [::1]) whose
Host header is not a localhost value are rejected with 403 Forbidden.
The check is derived from the connection's local address at runtime, so it
applies whether the server listens on localhost or 0.0.0.0, and never
affects requests arriving via non-loopback addresses.

If a reverse proxy on the same host forwards requests via localhost while
preserving the original Host header, configure the proxy to rewrite the
Host header to localhost, or opt out explicitly:

go
httpServer := server.NewStreamableHTTPServer(mcpServer,
    // Or server.WithSSEDisableLocalhostProtection(true) on NewSSEServer.
    server.WithDisableLocalhostProtection(true),
)

See the HTTP transport docs
for details, including the caveat for the framework-agnostic Handle entry
point.

Session Management

MCP-Go provides a robust session management system that allows you to:

  • Maintain separate state for each connected client
  • Register and track client sessions
  • Send notifications to specific clients
  • Provide per-session tool customization

Show Session Management Examples

Basic Session Handling

go
// Create a server with session capabilities
s := server.NewMCPServer(
    "Session Demo",
    "1.0.0",
    server.WithToolCapabilities(true),
)

// Implement your own ClientSession
type MySession struct {
    id           string
    notifChannel chan mcp.JSONRPCNotification
    isInitialized bool
    // Add custom fields for your application
}

// Implement the ClientSession interface
func (s *MySession) SessionID() string {
    return s.id
}

func (s *MySession) NotificationChannel() chan<- mcp.JSONRPCNotification {
    return s.notifChannel
}

func (s *MySession) Initialize() {
    s.isInitialized = true
}

func (s *MySession) Initialized() bool {
    return s.isInitialized
}

// Register a session
session := &MySession{
    id:           "user-123",
    notifChannel: make(chan mcp.JSONRPCNotification, 10),
}
if err := s.RegisterSession(context.Background(), session); err != nil {
    log.Printf("Failed to register session: %v", err)
}

// Send notification to a specific client
err := s.SendNotificationToSpecificClient(
    session.SessionID(),
    "notification/update",
    map[string]any{"message": "New data available!"},
)
if err != nil {
    log.Printf("Failed to send notification: %v", err)
}

// Unregister session when done
s.UnregisterSession(context.Background(), session.SessionID())

Per-Session Tools

For more advanced use cases, you can implement the SessionWithTools interface to support per-session tool customization:

text
/* Detailed source-code truncated for AI context efficiency. */

Tool Filtering

You can also apply filters to control which tools are available to certain sessions:

go
// Add a tool filter that only shows tools with certain prefixes
s := server.NewMCPServer(
    "Tool Filtering Demo",
    "1.0.0",
    server.WithToolCapabilities(true),
    server.WithToolFilter(func(ctx context.Context, tools []mcp.Tool) []mcp.Tool {
        // Get session from context
        session := server.ClientSessionFromContext(ctx)
        if session == nil {
            return tools // Return all tools if no session
        }
        
        // Example: filter tools based on session ID prefix
        if strings.HasPrefix(session.SessionID(), "admin-") {
            // Admin users get all tools
            return tools
        } else {
            // Regular users only get tools with "public-" prefix
            var filteredTools []mcp.Tool
            for _, tool := range tools {
                if strings.HasPrefix(tool.Name, "public-") {
                    filteredTools = append(filteredTools, tool)
                }
            }
            return filteredTools
        }
    }),
)

Working with Context

The session context is automatically passed to tool and resource handlers:

go
s.AddTool(mcp.NewTool("session_aware"), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    // Get the current session from context
    session := server.ClientSessionFromContext(ctx)
    if session == nil {
        return mcp.NewToolResultError("No active session"), nil
    }
    
    return mcp.NewToolResultText("Hello, session " + session.SessionID()), nil
})

// When using handlers in HTTP/SSE servers, you need to pass the context with the session
httpHandler := func(w http.ResponseWriter, r *http.Request) {
    // Get session from somewhere (like a cookie or header)
    session := getSessionFromRequest(r)
    
    // Add session to context
    ctx := s.WithContext(r.Context(), session)
    
    // Use this context when handling requests
    // ...
}

Request Hooks

Hook into the request lifecycle by creating a Hooks object with your
selection among the possible callbacks. This enables telemetry across all
functionality, and observability of various facts, for example the ability
to count improperly-formatted requests, or to log the agent identity during
initialization.

Add the Hooks to the server at the time of creation using the
server.WithHooks option.

Tool Handler Middleware

Add middleware to tool call handlers using the server.WithToolHandlerMiddleware option. Middlewares can be registered on server creation and are applied on every tool call.

A recovery middleware option is available to recover from panics in a tool call and can be added to the server with the server.WithRecovery option.

Prompt Handler Middleware

Add middleware to prompt handlers using the server.WithPromptHandlerMiddleware option. Middlewares can be registered on server creation and are applied on every prompts/get call.

Prompt Filtering

Filter prompts based on context using the server.WithPromptFilter option. This works the same way as tool filtering but applies to prompts/list results.

Regenerating Server Code

Server hooks and request handlers are generated. Regenerate them by running:

bash
go generate ./...

You need go installed and the goimports tool available. The generator runs
goimports automatically to format and fix imports.

Auto-completions

When users are filling in argument values for a specific prompt (identified by name) or resource template (identified by URI), servers can provide contextual suggestions.
To enable completion support, use the server.WithCompletions() option when creating your server.

Completion Providers

You can provide completion logic for both prompt arguments and resource template arguments by implementing the respective interfaces and passing them to the server as options.

Show Completion Provider Examples

text
/* Detailed source-code truncated for AI context efficiency. */

Completion Context

For prompts or resource templates with multiple arguments, the CompleteContext parameter provides access to previously completed arguments. This allows you to provide contextual suggestions based on earlier choices.

Show Completion Context Example

go
func (p *MyProvider) CompleteResourceArgument(
    ctx context.Context,
    uri string,
    argument mcp.CompleteArgument,
    context mcp.CompleteContext,
) (*mcp.Completion, error) {
    // Access previously completed arguments
    if previousValue, ok := context.Arguments["previous_arg"]; ok {
        // Provide suggestions based on previous_arg value
        return getSuggestionsFor(argument.Value, previousValue), nil
    }
    
    return &mcp.Completion{Values: []string{}}, nil
}

Response Constraints

When returning completion results:

  • Maximum 100 items per response
  • Use Total to indicate the total number of available matches
  • Use HasMore to signal if additional results exist beyond the returned values

Overview (`otel/README.md`)

mcp-go OpenTelemetry adapter

OpenTelemetry tracing for mcp-go,
shipped as a separate Go module so the core has zero OpenTelemetry
dependencies.

go
import otelmcp "github.com/mark3labs/mcp-go/otel"

srv := server.NewMCPServer("svc", "1.0", otelmcp.WithServerTracing(tp.Tracer("mcp")))
cli := client.NewClient(t, otelmcp.WithClientTracing(tp.Tracer("mcp")))

WithServerTracing / WithClientTracing install both an adapter tracer and
the W3C TraceContext propagator. Use WithServerTracingPropagator /
WithClientTracingPropagator to supply a custom propagation.TextMapPropagator
(e.g. a composite of TraceContext{} and Baggage{}).

Spans:

Span name Kind Where
mcp.<method> Server dispatcher
mcp.<method> Client outgoing requests
tool.<name> Internal tool handler

Attributes (string-valued):

Attribute Where
mcp.method every span
mcp.tool.name tools/call only
mcp.session.id server spans
mcp.protocol.version server spans

See example/ for a runnable round-trip with stderr span output.


Overview (`www/README.md`)

This is a Vocs project bootstrapped with the Vocs CLI.


Overview (`examples/inprocess_sampling/README.md`)

InProcess Sampling Example

This example demonstrates how to use sampling with in-process MCP client/server communication.

Overview

The example shows:

  • Creating an MCP server with sampling enabled
  • Adding a tool that uses sampling to request LLM completions
  • Creating an in-process client with a sampling handler
  • Making tool calls that trigger sampling requests

Key Components

Server Side

  • mcpServer.EnableSampling() - Enables sampling capability
  • Tool handler calls mcpServer.RequestSampling() to request LLM completions
  • Sampling requests are handled directly by the client's sampling handler

Client Side

  • MockSamplingHandler - Implements the SamplingHandler interface
  • NewInProcessClientWithSamplingHandler() - Creates client with sampling support
  • The handler receives sampling requests and returns mock LLM responses

Running the Example

bash
go run main.go

Expected Output

text
Tool result: LLM Response (model: mock-llm-v1): Mock LLM response to: 'What is the capital of France?'

Real LLM Integration

To integrate with a real LLM service (OpenAI, Anthropic, etc.), replace the MockSamplingHandler with an implementation that calls your preferred LLM API. See the client sampling documentation for examples with real LLM providers.


Overview (`examples/oauth_client/README.md`)

OAuth Client Example

This example demonstrates how to use the OAuth capabilities of the MCP Go client to authenticate with an MCP server that requires OAuth authentication.

Features

  • OAuth 2.1 authentication with PKCE support
  • Dynamic client registration
  • Authorization code flow
  • Token refresh
  • Local callback server for handling OAuth redirects

Usage

bash
# Set environment variables (optional)
export MCP_CLIENT_ID=your_client_id
export MCP_CLIENT_SECRET=your_client_secret

# Run the example
go run main.go

How it Works

  1. The client attempts to initialize a connection to the MCP server
  2. If the server requires OAuth authentication, it will return a 401 Unauthorized response
  3. The client detects this and starts the OAuth flow:
    • Generates PKCE code verifier and challenge
    • Generates a state parameter for security
    • Opens a browser to the authorization URL
    • Starts a local server to handle the callback
  4. The user authorizes the application in their browser
  5. The authorization server redirects back to the local callback server
  6. The client exchanges the authorization code for an access token
  7. The client retries the initialization with the access token
  8. The client can now make authenticated requests to the MCP server

Configuration

Edit the following constants in main.go to match your environment:

go
const (
    // Replace with your MCP server URL
    serverURL = "https://api.example.com/v1/mcp"
    // Use a localhost redirect URI for this example
    redirectURI = "http://localhost:8085/oauth/callback"
)

OAuth Scopes

The example requests the following scopes:

  • mcp.read - Read access to MCP resources
  • mcp.write - Write access to MCP resources

You can modify the scopes in the oauthConfig to match the requirements of your MCP server.


Overview (`examples/sampling_client/README.md`)

MCP Sampling Example Client

This example demonstrates how to implement an MCP client that supports sampling requests from servers.

Features

  • Sampling Handler: Implements the SamplingHandler interface to process sampling requests
  • Mock LLM: Provides a mock LLM implementation for demonstration purposes
  • Capability Declaration: Automatically declares sampling capability when a handler is configured
  • Bidirectional Communication: Handles incoming requests from the server

Mock LLM Handler

The MockSamplingHandler simulates an LLM by:

  • Logging the received request parameters
  • Generating a mock response that echoes the input
  • Returning proper MCP sampling response format

In a real implementation, you would:

  • Integrate with actual LLM APIs (OpenAI, Anthropic, etc.)
  • Implement proper model selection based on preferences
  • Add human-in-the-loop approval mechanisms
  • Handle rate limiting and error cases

Usage

Build the client:

bash
go build -o sampling_client

Run with the sampling server:

bash
./sampling_client ../sampling_server/sampling_server

Or with any other MCP server that supports sampling:

bash
./sampling_client /path/to/your/mcp/server

Implementation Details

  1. Sampling Handler: Implements client.SamplingHandler interface
  2. Client Configuration: Uses client.WithSamplingHandler() to enable sampling
  3. Automatic Capability: Sampling capability is automatically declared during initialization
  4. Request Processing: Handles incoming sampling/createMessage requests from servers

Sample Output

text
Connected to server: sampling-example-server v1.0.0
Available tools:
  - ask_llm: Ask the LLM a question using sampling
  - greet: Greet the user

--- Testing greet tool ---
Greet result: Hello, Sampling Demo User! This server supports sampling - try using the ask_llm tool!

--- Testing ask_llm tool (with sampling) ---
Mock LLM received: What is the capital of France?
System prompt: You are a helpful geography assistant.
Max tokens: 1000
Temperature: 0.700000
Ask LLM result: LLM Response (model: mock-llm-v1): Mock LLM response to: 'What is the capital of France?'. This is a simulated response from a mock LLM handler.

Real LLM Integration

To integrate with a real LLM, replace the MockSamplingHandler with an implementation that:

go
type RealSamplingHandler struct {
    apiKey string
    client *openai.Client // or other LLM client
}

func (h *RealSamplingHandler) CreateMessage(ctx context.Context, request mcp.CreateMessageRequest) (*mcp.CreateMessageResult, error) {
    // Convert MCP request to LLM API format
    // Call LLM API
    // Convert response back to MCP format
    // Return result
}

Overview (`examples/sampling_http_client/README.md`)

HTTP Sampling Client Example

This example demonstrates how to create an MCP client using HTTP transport that supports sampling requests from the server.

Overview

This client:

  • Connects to an MCP server via HTTP/HTTPS transport
  • Declares sampling capability during initialization
  • Handles incoming sampling requests from the server
  • Uses a mock LLM to generate responses (replace with real LLM integration)

Usage

  1. Start an MCP server that supports sampling (e.g., using the sampling_server example)

  2. Update the server URL in main.go:

    go
    httpClient, err := client.NewStreamableHttpClient(
        "http://your-server:port", // Replace with your server URL
    )
  3. Run the client:

    bash
    go run main.go

Key Features

HTTP Transport with Sampling

The client creates the HTTP transport directly and then wraps it with a client that supports sampling:

go
httpTransport, err := transport.NewStreamableHTTP("http://localhost:8080")
mcpClient := client.NewClient(httpTransport, client.WithSamplingHandler(samplingHandler))

Sampling Handler

The MockSamplingHandler implements the client.SamplingHandler interface:

go
type MockSamplingHandler struct{}

func (h *MockSamplingHandler) CreateMessage(ctx context.Context, request mcp.CreateMessageRequest) (*mcp.CreateMessageResult, error) {
    // Process the sampling request and return LLM response
    // In production, integrate with OpenAI, Anthropic, or other LLM APIs
}

Client Configuration

The client is configured with sampling capabilities:

go
mcpClient := client.NewClient(
    httpTransport,
    client.WithSamplingHandler(samplingHandler),
)
// Sampling capability is automatically declared when a handler is provided

Real Implementation

For a production implementation, replace the MockSamplingHandler with a real LLM client:

go
type RealSamplingHandler struct {
    client *openai.Client // or other LLM client
}

func (h *RealSamplingHandler) CreateMessage(ctx context.Context, request mcp.CreateMessageRequest) (*mcp.CreateMessageResult, error) {
    // Convert MCP request to LLM API format
    // Call LLM API
    // Convert response back to MCP format
    // Return the result
}

HTTP-Specific Features

The HTTP transport supports:

  • Standard HTTP headers for authentication and customization
  • OAuth 2.0 authentication (using WithHTTPOAuth)
  • Custom headers (using WithHTTPHeaders)
  • Server-side events (SSE) for bidirectional communication
  • Proper error handling with HTTP status codes
  • Session management via HTTP headers

Testing

The implementation includes comprehensive tests in client/transport/streamable_http_sampling_test.go that verify:

  • Sampling request handling
  • Error scenarios
  • Bidirectional interface compliance
  • HTTP-specific error codes and responses

Overview (`examples/sampling_http_server/README.md`)

HTTP Sampling Server Example

This example demonstrates how to create an MCP server using HTTP transport that can send sampling requests to clients.

Overview

This server:

  • Runs on HTTP transport (port 8080 by default)
  • Declares sampling capability during initialization
  • Can send sampling requests to connected clients via Server-Sent Events (SSE)
  • Receives sampling responses from clients via HTTP POST
  • Includes tools that demonstrate sampling functionality

Usage

  1. Start the server:

    bash
    go run main.go
  2. The server will be available at: http://localhost:8080/mcp

  3. Connect with an HTTP client that supports sampling (like the sampling_http_client example)

Tools Available

`ask_llm`

Demonstrates server-initiated sampling:

  • Takes a question and optional system prompt
  • Sends sampling request to client
  • Returns the LLM's response

`echo`

Simple tool for testing basic functionality:

  • Echoes back the input message
  • Doesn't require sampling

How Sampling Works

Server → Client Flow

  1. Tool Invocation: Client calls ask_llm tool
  2. Sampling Request: Server creates sampling request with user's question
  3. SSE Transmission: Server sends JSON-RPC request to client via SSE stream
  4. Client Processing: Client's sampling handler processes the request
  5. HTTP Response: Client sends JSON-RPC response back via HTTP POST
  6. Tool Response: Server returns the LLM response to the original tool caller

Communication Architecture

text
Client (HTTP + SSE) ←→ Server (HTTP)
     │                       │
     ├─ POST: Tool Call ──→  │
     │                       │
     │  ←── SSE: Sampling ───┤ 
     │      Request          │
     │                       │
     ├─ POST: Sampling ───→  │
     │      Response         │
     │                       │
     │  ←── HTTP: Tool ──────┤
           Response

Key Features

Bidirectional Communication

  • SSE Stream: Server → Client requests (sampling, notifications)
  • HTTP POST: Client → Server responses and requests

Session Management

  • Session ID tracking for request/response correlation
  • Proper session lifecycle management
  • Session validation for security

Error Handling

  • JSON-RPC error codes for different failure scenarios
  • Timeout handling for sampling requests
  • Queue overflow protection

HTTP-Specific Features

  • Standard MCP headers (Mcp-Session-Id, Mcp-Protocol-Version)
  • Content-Type validation
  • Proper HTTP status codes
  • SSE event formatting

Testing

You can test the server using the sampling_http_client example:

  1. Start this server:

    bash
    go run examples/sampling_http_server/main.go
  2. In another terminal, start the client:

    bash
    go run examples/sampling_http_client/main.go
  3. The client will connect and be ready to handle sampling requests from the server.

Production Considerations

Security

  • Implement proper authentication/authorization
  • Use HTTPS in production
  • Validate all incoming data
  • Implement rate limiting

Scalability

  • Consider connection pooling for multiple clients
  • Implement proper session cleanup
  • Monitor memory usage for long-running sessions
  • Add metrics and monitoring

Reliability

  • Implement request retries
  • Add circuit breakers for failing clients
  • Implement graceful degradation when sampling is unavailable
  • Add comprehensive logging

Integration

This server can be integrated into existing HTTP infrastructure:

go
// Custom HTTP server integration
mux := http.NewServeMux()
mux.Handle("/mcp", httpServer)
mux.Handle("/health", healthHandler)

server := &http.Server{
    Addr:    ":8080",
    Handler: mux,
}

The sampling functionality works seamlessly with other MCP features like tools, resources, and prompts.


Overview (`examples/sampling_server/README.md`)

MCP Sampling Example Server

This example demonstrates how to implement an MCP server that uses sampling to request LLM completions from clients.

Features

  • Sampling Support: The server can request LLM completions from clients that support sampling
  • Tool Integration: Shows how to use sampling within tool implementations
  • Bidirectional Communication: Demonstrates server-to-client requests

Tools

`ask_llm`

Asks the LLM a question using sampling. This tool demonstrates how servers can leverage client-side LLM capabilities.

Parameters:

  • question (required): The question to ask the LLM
  • system_prompt (optional): System prompt to provide context

`greet`

A simple greeting tool that doesn't use sampling, for comparison.

Parameters:

  • name (required): Name of the person to greet

Usage

Build and run the server:

bash
go build -o sampling_server
./sampling_server

The server communicates via stdio and expects to be connected to an MCP client that supports sampling.

Implementation Details

  1. Enable Sampling: The server calls mcpServer.EnableSampling() to declare sampling capability
  2. Request Sampling: Tools use mcpServer.RequestSampling(ctx, request) to send sampling requests to the client
  3. Handle Responses: The server receives and processes the LLM responses from the client via bidirectional stdio communication
  4. Response Routing: Incoming responses are automatically routed to the correct pending request using request IDs

Testing

Use the companion sampling_client example to test this server:

bash
cd ../sampling_client
go build -o sampling_client
./sampling_client ../sampling_server/sampling_server

Overview (`examples/structured_input_and_output/README.md`)

Structured Content Example

This example shows how to return structuredContent in tool result with corresponding OutputSchema.

Defined in the MCP spec here: https://modelcontextprotocol.io/specification/2025-06-18/server/tools#structured-content

Usage

Define a struct for your input:

go
type WeatherRequest struct {
    Location string `json:"location" jsonschema:"City or location"`
    Units    string `json:"units,omitempty" jsonschema:"celsius or fahrenheit"`
}

A field is required unless its json tag contains omitempty (or omitzero).
Here location is required and units is optional. The jsonschema tag value
is used as the property description.

Define a struct for your output:

go
type WeatherResponse struct {
    Location    string  `json:"location" jsonschema:"The location"`
    Temperature float64 `json:"temperature" jsonschema:"Current temperature"`
    Conditions  string  `json:"conditions" jsonschema:"Weather conditions"`
}

Add it to your tool:

go
tool := mcp.NewTool("get_weather",
    mcp.WithDescription("Get weather information"),
    mcp.WithInputSchema[WeatherRequest](),
    mcp.WithOutputSchema[WeatherResponse](),
)

Return structured data in tool result:

go
func weatherHandler(ctx context.Context, request mcp.CallToolRequest, args WeatherRequest) (*mcp.CallToolResult, error) {
    response := WeatherResponse{
        Location:    args.Location,
        Temperature: 25.0,
        Conditions:  "Cloudy",
    }
    
    fallbackText := fmt.Sprintf("Weather in %s: %.1f°C, %s", 
        response.Location, response.Temperature, response.Conditions)
    
    return mcp.NewToolResultStructured(response, fallbackText), nil
}

Struct tag reference

Schemas are generated by google/jsonschema-go.
The tags below reflect its current behavior:

  • Description: use jsonschema:"some text". The older
    jsonschema_description:"..." tag is not read and is silently ignored.
  • Required: derived automatically — a field is required unless its json
    tag contains omitempty or omitzero. There is no json:"...,required"
    option, and the jsonschema:"required" tag is no longer supported.
  • Constraints (enum, minimum, maximum, ...): not supported as struct
    tags. A jsonschema tag value that begins with word= is rejected with
    tag must not begin with 'WORD='. To add constraints, use
    mcp.WithRawInputSchema() or the builder API (mcp.WithString,
    mcp.WithNumber, ... with options like mcp.Enum(...)).

See main.go for more examples.


Overview (`otel/example/README.md`)

OpenTelemetry Tracing

End-to-end OpenTelemetry tracing across an mcp-go server and client, wired
through the github.com/mark3labs/mcp-go/otel submodule.

What this demonstrates

  • otel.WithServerTracing(tracer) installs an OpenTelemetry-backed tracer on
    the server plus a W3C TraceContext propagator. The server emits
    mcp.<method> server spans for every dispatched JSON-RPC method, plus a
    tool.<name> child span around each tool invocation.
  • otel.WithClientTracing(tracer) installs the same on the client. Outgoing
    spans are mcp.<method> client-kind, and the client injects W3C
    traceparent into the request headers.
  • The streamable-HTTP transport carries the traceparent header so the
    server middleware extracts it and the server span chains as a child of the
    client span — same trace ID end-to-end.

Running

bash
go run ./otel/example

Spans are printed to stderr via a tiny inline SpanProcessor so the
example has no exporter dependency. To send spans to a real OTLP
collector, replace the processor with a batcher around the OTLP
exporter:

go
exp, err := otlptracegrpc.New(ctx)
if err != nil {
    log.Fatal(err)
}
tp := sdktrace.NewTracerProvider(sdktrace.WithBatcher(exp))

See https://opentelemetry.io/docs/languages/go/exporters/.

Span attributes

Attribute Where Description
mcp.method every span JSON-RPC method name
mcp.tool.name tools/call only tool being invoked
mcp.session.id server spans client session ID, when available
mcp.protocol.version server spans Mcp-Protocol-Version header value

Custom propagator

The default propagator is W3C TraceContext. Pass a composite (e.g.
TraceContext + Baggage) with otel.WithServerTracingPropagator /
otel.WithClientTracingPropagator.

Limitations

stdio transports do not carry trace context today. Spans created over stdio
start as roots — there is nowhere on the JSON-RPC envelope to attach
traceparent without a spec change.


Overview (`server/internal/gen/README.md`)

Readme for Codegen

This internal module contains code generation for producing a few repetitive
constructs, namely:

  • The switch statement that handles the request dispatch
  • The hook function types and the methods on the Hook struct

To invoke the code generation:

text
go generate ./...

Development

  • request_handler.go.tmpl generates server/request_handler.go, and
  • hooks.go.tmpl generates server/hooks.go

Inside of data.go there is a struct with the inputs to both templates.

Note that the driver in main.go generates code and also pipes it through
goimports for formatting and imports cleanup.


Www Docs Pages Getting Started (`www/docs/pages/getting-started.mdx`)

Getting Started

Introduction

What is MCP?

The Model Context Protocol (MCP) is an open standard that enables secure, controlled connections between AI applications and external data sources and tools. It provides a standardized way for Large Language Models (LLMs) to access and interact with external systems while maintaining security and user control.

Why MCP Go?

MCP-Go is designed to make building MCP servers in Go fast, simple, and complete:

  • Fast: Minimal overhead with efficient Go implementation
  • Simple: Clean, intuitive API with minimal boilerplate
  • Complete: Full support for the MCP specification including tools, resources, and prompts

Key Features

  • High-level interface: Focus on your business logic, not protocol details
  • Minimal boilerplate: Get started with just a few lines of code
  • Full MCP spec support: Tools, resources, prompts, and all transport methods
  • Type safety: Leverage Go's type system for robust MCP servers
  • Multiple transports: Stdio, StreamableHTTP, Server-Sent Events and In-Process support

Installation

Add MCP-Go to your Go project:

bash
go get github.com/mark3labs/mcp-go

MCP-Go makes it easy to build Model Context Protocol (MCP) servers in Go. This guide will help you create your first MCP server in just a few minutes.

Your First MCP Server

Let's create a simple MCP server with a "hello world" tool:

go
package main

import (
    "context"
    "fmt"

    "github.com/mark3labs/mcp-go/mcp"
    "github.com/mark3labs/mcp-go/server"
)

func main() {
    // Create a new MCP server
    s := server.NewMCPServer(
        "Demo 🚀",
        "1.0.0",
        server.WithToolCapabilities(false),
    )

    // Add tool
    tool := mcp.NewTool("hello_world",
        mcp.WithDescription("Say hello to someone"),
        mcp.WithString("name",
            mcp.Required(),
            mcp.Description("Name of the person to greet"),
        ),
    )

    // Add tool handler
    s.AddTool(tool, helloHandler)

    // Start the stdio server
    if err := server.ServeStdio(s); err != nil {
        fmt.Printf("Server error: %v\n", err)
    }
}

func helloHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    name, err := request.RequireString("name")
    if err != nil {
        return mcp.NewToolResultError(err.Error()), nil
    }

    return mcp.NewToolResultText(fmt.Sprintf("Hello, %s!", name)), nil
}

Running Your Server

  1. Save the code above to a file (e.g., main.go)
  2. Run it with:
    bash
    go run main.go

Your MCP server is now running and ready to accept connections via stdio!

What's Next?

Now that you have a basic server running, you can:

  • Add more tools - Create tools for calculations, file operations, API calls, etc.
  • Add resources - Expose data sources like files, databases, or APIs
  • Add prompts - Create reusable prompt templates for better LLM interactions
  • Explore examples - Check out the examples/ directory for more complex use cases

Key Concepts

Tools

Tools let LLMs take actions through your server. They're like functions that the LLM can call:

go
calculatorTool := mcp.NewTool("calculate",
    mcp.WithDescription("Perform basic arithmetic operations"),
    mcp.WithString("operation",
        mcp.Required(),
        mcp.Enum("add", "subtract", "multiply", "divide"),
    ),
    mcp.WithNumber("x", mcp.Required()),
    mcp.WithNumber("y", mcp.Required()),
)

Resources

Resources expose data to LLMs. They can be static files or dynamic data:

go
resource := mcp.NewResource(
    "docs://readme",
    "Project README",
    mcp.WithResourceDescription("The project's README file"),
    mcp.WithMIMEType("text/markdown"),
)

Server Options

Customize your server with various options:

go
s := server.NewMCPServer(
    "My Server",
    "1.0.0",
    server.WithToolCapabilities(true),
    server.WithRecovery(),
    server.WithHooks(myHooks),
)

Transport Options

MCP-Go supports multiple transport methods:

  • Stdio (most common): server.ServeStdio(s)
  • StreamableHTTP: server.NewStreamableHTTPServer(s).Start(":8080")
  • Server-Sent Events: server.ServeSSE(s, ":8080")
  • In-Process: client.NewInProcessClient(server)

Need Help?

  • Check out the examples for more complex use cases
  • Join the discussion on Discord
  • Read the full documentation in the README

Www Docs Pages Index (`www/docs/pages/index.mdx`)


layout: landing

import { HomePage } from 'vocs/components'

<HomePage.Root>
<HomePage.Logo />
<HomePage.Tagline>MCP-Go</HomePage.Tagline>
<HomePage.Description>
A Go implementation of the Model Context Protocol (MCP), enabling seamless integration between LLM applications and external data sources and tools. Build powerful MCP servers with minimal boilerplate and focus on creating great tools.
</HomePage.Description>
<HomePage.Buttons>
<HomePage.Button href="/getting-started" variant="accent">Get started</HomePage.Button>
<HomePage.Button href="https://github.com/mark3labs/mcp-go">GitHub</HomePage.Button>
</HomePage.Buttons>
</HomePage.Root>


Www Docs Pages Clients Index (`www/docs/pages/clients/index.mdx`)

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

Quick Example

Here's a complete example showing how to create and use an MCP client:

text
/* Detailed source-code truncated for AI context efficiency. */

Client Types by Transport

STDIO Client

Best for:

  • Command-line applications
  • Desktop software integration
  • Local development and testing
  • Single-server connections
go
// 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
go
// 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
go
// 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
go
// Create in-process client
client := client.NewInProcessClient(server)

Common Client Patterns

Connection Management

go
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

go
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

go
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

text
/* Detailed source-code truncated for AI context efficiency. */

Multi-Server Client

text
/* Detailed source-code truncated for AI context efficiency. */

Next Steps

Explore each client topic in detail:


Www Docs Pages Servers Index (`www/docs/pages/servers/index.mdx`)

Building MCP Servers

Learn how to build powerful MCP servers with MCP-Go. This section covers everything from basic server setup to advanced features like typed tools and session management.

Overview

MCP servers expose tools, resources, and prompts to LLM clients. MCP-Go makes it easy to build robust servers with minimal boilerplate while providing full control over advanced features.

What You'll Learn

Quick Example

Here's a complete MCP server that demonstrates the key concepts:

text
/* Detailed source-code truncated for AI context efficiency. */

Next Steps

Start with Server Basics to learn how to create and configure your first MCP server, then explore the other sections to add resources, tools, and advanced features.


Www Docs Pages Transports Index (`www/docs/pages/transports/index.mdx`)

Transport Options

MCP-Go supports multiple transport methods to fit different deployment scenarios and integration patterns. Choose the right transport based on your use case, performance requirements, and client capabilities.

Overview

Transport layers handle the communication between MCP clients and servers. Each transport has different characteristics and is optimized for specific scenarios:

  • STDIO - Standard input/output for command-line tools
  • SSE - Server-Sent Events for web applications
  • StreamableHTTP - Traditional HTTP for REST-like interactions
  • In-Process - Direct integration for embedded scenarios

Transport Comparison

Transport Use Case Pros Cons Sampling Support
STDIO CLI tools, desktop apps Simple, secure, no network Single client, local only ✅ Full support
SSE Web apps, real-time Multi-client, real-time, web-friendly HTTP overhead, one-way streaming ❌ Not supported
StreamableHTTP Web services, APIs Standard protocol, caching, load balancing No real-time, more complex ❌ Not supported
In-Process Embedded, testing No serialization, fastest Same process only ✅ Full support

Quick Example

The same server code works with any transport:

go
package main

import (
    "context"
    "fmt"
    "log"
    "os"

    "github.com/mark3labs/mcp-go/mcp"
    "github.com/mark3labs/mcp-go/server"
    "github.com/mark3labs/mcp-go/client"
)

func main() {
    // Create server (transport-agnostic)
    s := server.NewMCPServer("Multi-Transport Server", "1.0.0",
        server.WithToolCapabilities(true),
    )

    // Add a simple tool
    s.AddTool(
        mcp.NewTool("echo",
            mcp.WithDescription("Echo back the input"),
            mcp.WithString("message", mcp.Required()),
        ),
        handleEcho,
    )

    // Choose transport based on environment
    transport := os.Getenv("MCP_TRANSPORT")
    port := os.Getenv("PORT")
    if port == "" {
        port = "8080"
    }

    switch transport {
    case "sse":
        fmt.Printf("Starting SSE server on port %s\n", port)
        sseServer := server.NewSSEServer(s)
        sseServer.Start(":" + port)
    case "streamablehttp":
        fmt.Printf("Starting StreamableHTTP server on port %s\n", port)
        httpServer := server.NewStreamableHTTPServer(s)
        httpServer.Start(":" + port)
    default:
        fmt.Println("Starting STDIO server")
        server.ServeStdio(s)
    }
}

func handleEcho(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    message, err := req.RequireString("message")
    if err != nil {
        return nil, err
    }
    return mcp.NewToolResultText(fmt.Sprintf("Echo: %s", message)), nil
}

Choosing the Right Transport

STDIO Transport

Best for:

  • Command-line tools and utilities
  • Desktop application integrations
  • Local development and testing
  • Single-user scenarios

Example use cases:

  • File system tools for IDEs
  • Local database utilities
  • Development workflow automation
  • System administration tools

SSE Transport

Best for:

  • Web applications requiring real-time updates
  • Browser-based LLM interfaces
  • Multi-user collaborative tools
  • Dashboard and monitoring applications

Example use cases:

  • Web-based chat interfaces
  • Real-time data visualization
  • Collaborative document editing
  • Live system monitoring

StreamableHTTP Transport

Best for:

  • Traditional web services
  • REST API integrations
  • Load-balanced deployments
  • Stateless interactions

Example use cases:

  • Microservice architectures
  • Public API endpoints
  • Integration with existing HTTP infrastructure
  • Cached or rate-limited services

In-Process Transport

Best for:

  • Embedded MCP servers
  • Testing and development
  • High-performance scenarios
  • Library integrations
  • Sampling-enabled applications

Example use cases:

  • Testing MCP implementations
  • Embedded analytics engines
  • High-frequency trading systems
  • Real-time game servers
  • LLM-powered applications with bidirectional communication

Transport Configuration

Environment-Based Selection

go
func startServer(s *server.MCPServer) error {
    switch os.Getenv("MCP_TRANSPORT") {
    case "sse":
        sseServer := server.NewSSEServer(s)
        return sseServer.Start(getPort())
    case "streamablehttp":
        httpServer := server.NewStreamableHTTPServer(s)
        return httpServer.Start(getPort())
    case "inprocess":
        // Note: In-process transport doesn't use network ports
        // This would typically be used differently in practice
        client := client.NewInProcessClient(s)
        defer client.Close()
        // Keep the process running
        select {}
    default:
        return server.ServeStdio(s)
    }
}

func getPort() string {
    if port := os.Getenv("PORT"); port != "" {
        return ":" + port
    }
    return ":8080"
}

Multi-Transport Server

go
func main() {
    s := server.NewMCPServer("Multi-Transport", "1.0.0")
    
    // Add your tools, resources, prompts...
    setupServer(s)
    
    // Start multiple transports concurrently with proper error handling
    errChan := make(chan error, 3)
    
    go func() {
        log.Println("Starting STDIO server...")
        if err := server.ServeStdio(s); err != nil {
            log.Printf("STDIO server error: %v", err)
            errChan <- fmt.Errorf("STDIO server failed: %w", err)
        }
    }()
    
    go func() {
        log.Println("Starting SSE server on :8080...")
        sseServer := server.NewSSEServer(s)
        if err := sseServer.Start(":8080"); err != nil {
            log.Printf("SSE server error: %v", err)
            errChan <- fmt.Errorf("SSE server failed: %w", err)
        }
    }()
    
    log.Println("Starting StreamableHTTP server on :8081...")
    httpServer := server.NewStreamableHTTPServer(s)
    if err := httpServer.Start(":8081"); err != nil {
        log.Printf("StreamableHTTP server error: %v", err)
        errChan <- fmt.Errorf("StreamableHTTP server failed: %w", err)
    }
    
    // Wait for any server to fail
    select {
    case err := <-errChan:
        log.Printf("Server failed: %v", err)
        return
    }
}

// Helper function for the multi-transport example
func setupServer(s *server.MCPServer) {
    // Placeholder implementation - would add tools, resources, etc.
}

Performance Considerations

Latency Comparison

  • In-Process: ~1μs (no serialization)
  • STDIO: ~100μs (local pipes)
  • HTTP/SSE: ~1-10ms (network + HTTP overhead)

Throughput Comparison

  • In-Process: Limited by CPU/memory
  • STDIO: Limited by pipe buffers (~64KB)
  • HTTP/SSE: Limited by network bandwidth

Memory Usage

  • In-Process: Shared memory space
  • STDIO: Minimal overhead
  • HTTP/SSE: Connection pooling, request buffering

Security Considerations

STDIO Transport

  • Pros: No network exposure, process isolation
  • Cons: Inherits parent process permissions
  • Best practices: Validate all inputs, use least privilege

Network Transports (SSE/HTTP)

  • Authentication: Implement proper auth middleware
  • Authorization: Validate permissions per request
  • Rate limiting: Prevent abuse and DoS
  • HTTPS: Always use TLS in production
go
// Example with security middleware
s := server.NewMCPServer("Secure Server", "1.0.0",
    server.WithToolMiddleware(authMiddleware),
    server.WithToolMiddleware(rateLimitMiddleware),
    server.WithRecovery(),
)

Next Steps

Explore each transport in detail:


Www Docs Pages Core Concepts (`www/docs/pages/core-concepts.mdx`)

Core Concepts

Understanding the fundamental concepts of MCP and how MCP-Go implements them is essential for building effective MCP servers and clients.

MCP Protocol Fundamentals

The Model Context Protocol defines four core concepts that enable LLMs to interact with external systems safely and effectively.

Resources

Resources are like GET endpoints - they expose data to LLMs in a read-only manner. Think of them as files, database records, or API responses that an LLM can access.

Key characteristics:

  • Read-only: LLMs can fetch but not modify resources
  • URI-based: Each resource has a unique identifier
  • Typed content: Resources specify their MIME type (text, JSON, binary, etc.)
  • Dynamic or static: Can be pre-defined or generated on-demand

Example use cases:

  • File system access (file:///path/to/document.txt)
  • Database records (db://users/123)
  • API data (api://weather/current)
  • Configuration files (config://app.json)
go
// Static resource
resource := mcp.NewResource(
    "docs://readme",
    "Project README",
    mcp.WithResourceDescription("The project's main documentation"),
    mcp.WithMIMEType("text/markdown"),
)

// Dynamic resource with template
userResource := mcp.NewResource(
    "users://{user_id}",
    "User Profile",
    mcp.WithResourceDescription("User profile information"),
    mcp.WithMIMEType("application/json"),
)

Tools

Tools are like POST endpoints - they provide functionality that LLMs can invoke to take actions or perform computations.

Key characteristics:

  • Action-oriented: Tools do things rather than just return data
  • Parameterized: Accept structured input arguments
  • Typed schemas: Define expected parameter types and constraints
  • Return results: Provide structured output back to the LLM

Example use cases:

  • Calculations (calculate, convert_units)
  • File operations (create_file, search_files)
  • API calls (send_email, create_ticket)
  • System commands (run_command, check_status)
go
// Simple calculation tool
calcTool := mcp.NewTool("calculate",
    mcp.WithDescription("Perform arithmetic operations"),
    mcp.WithString("operation", 
        mcp.Required(),
        mcp.Enum("add", "subtract", "multiply", "divide"),
    ),
    mcp.WithNumber("x", mcp.Required()),
    mcp.WithNumber("y", mcp.Required()),
)

// File creation tool
fileTool := mcp.NewTool("create_file",
    mcp.WithDescription("Create a new file with content"),
    mcp.WithString("path", mcp.Required()),
    mcp.WithString("content", mcp.Required()),
    mcp.WithString("encoding", mcp.Default("utf-8")),
)

Prompts

Prompts are reusable interaction templates that help structure conversations between users and LLMs.

Key characteristics:

  • Template-based: Use placeholders for dynamic content
  • Reusable: Can be invoked multiple times with different arguments
  • Structured: Define clear input parameters and expected outputs
  • Context-aware: Can include relevant resources or tool suggestions

Example use cases:

  • Code review templates
  • Documentation generation
  • Data analysis workflows
  • Creative writing prompts
go
// Code review prompt
reviewPrompt := mcp.NewPrompt("code_review",
    mcp.WithPromptDescription("Review code for best practices and issues"),
    mcp.WithPromptArgument("code", 
        mcp.Required(),
        mcp.Description("The code to review"),
    ),
    mcp.WithPromptArgument("language",
        mcp.Description("Programming language"),
    ),
)

// Data analysis prompt
analysisPrompt := mcp.NewPrompt("analyze_data",
    mcp.WithPromptDescription("Analyze dataset and provide insights"),
    mcp.WithPromptArgument("dataset_uri", mcp.Required()),
    mcp.WithPromptArgument("focus_areas", 
        mcp.Description("Specific areas to focus analysis on"),
    ),
)

Transports

Transports define how MCP clients and servers communicate. MCP-Go supports multiple transport methods to fit different deployment scenarios.

Available transports:

  1. Stdio - Standard input/output (most common)

    • Best for: Local tools, CLI integration, desktop applications
    • Pros: Simple, secure, no network setup
    • Cons: Local only, single client
  2. Server-Sent Events (SSE) - HTTP-based streaming

    • Best for: Web applications, real-time updates
    • Pros: Web-friendly, real-time, multiple clients
    • Cons: HTTP overhead, one-way streaming
  3. HTTP - Traditional request/response

    • Best for: Web services, REST-like APIs
    • Pros: Standard protocol, caching, load balancing
    • Cons: No real-time updates, more complex
go
// Stdio transport (most common)
server.ServeStdio(s)

// HTTP transport
server.ServeHTTP(s, ":8080")

// SSE transport
server.ServeSSE(s, ":8080")

SDK Architecture

MCP-Go provides a clean architecture that abstracts the complexity of the MCP protocol while giving you full control when needed.

Server vs Client

Understanding when to build servers versus clients is crucial for effective MCP integration.

MCP Servers:

  • Purpose: Expose tools, resources, and prompts to LLMs
  • Use cases:
    • Database access layers
    • File system tools
    • API integrations
    • Custom business logic
  • Characteristics: Passive, respond to requests, stateful
go
// Server example - exposes functionality
s := server.NewMCPServer("Database Tools", "1.0.0")
s.AddTool(queryTool, handleQuery)
s.AddResource(tableResource, handleTableAccess)
server.ServeStdio(s)

MCP Clients:

  • Purpose: Connect to and use MCP servers
  • Use cases:
    • LLM applications
    • Orchestration tools
    • Testing and debugging
    • Server composition
  • Characteristics: Active, make requests, coordinate multiple servers
go
// Client example - uses functionality
client := client.NewStdioClient("database-server")
tools, _ := client.ListTools(ctx, mcp.ListToolsRequest{})
result, _ := client.CallTool(ctx, queryRequest)

Transport Layer

The transport layer abstracts communication protocols, allowing you to focus on business logic rather than protocol details.

Key benefits:

  • Protocol agnostic: Same server code works with any transport
  • Automatic serialization: JSON marshaling/unmarshaling handled automatically
  • Error handling: Transport-specific errors are normalized
  • Connection management: Automatic reconnection and cleanup
go
// Same server works with any transport
s := server.NewMCPServer("My Server", "1.0.0")

// Choose transport at runtime
switch transport {
case "stdio":
    server.ServeStdio(s)
case "http":
    server.ServeHTTP(s, ":8080")
case "sse":
    server.ServeSSE(s, ":8080")
}

Session Management

MCP-Go handles session management automatically, supporting multiple concurrent clients with proper isolation.

Features:

  • Multi-client support: Multiple LLMs can connect simultaneously
  • Session isolation: Each client has independent state
  • Resource cleanup: Automatic cleanup when clients disconnect
  • Concurrent safety: Thread-safe operations across all sessions

Session lifecycle:

  1. Initialize: Client connects and exchanges capabilities
  2. Active: Client makes requests, server responds
  3. Cleanup: Connection closes, resources are freed
go
// Server automatically handles multiple sessions
s := server.NewMCPServer("Multi-Client Server", "1.0.0",
    server.WithHooks(&server.Hooks{
        OnSessionStart: func(sessionID string) {
            log.Printf("Client %s connected", sessionID)
        },
        OnSessionEnd: func(sessionID string) {
            log.Printf("Client %s disconnected", sessionID)
        },
    }),
)

State management patterns:

go
// Per-session state
type SessionState struct {
    UserID   string
    Settings map[string]interface{}
}

var sessions = make(map[string]*SessionState)

func toolHandler(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    sessionID := server.GetSessionID(ctx)
    state := sessions[sessionID]
    
    // Use session-specific state
    return processWithState(state, req)
}

Www Docs Pages Protocol 2026 07 28 (`www/docs/pages/protocol-2026-07-28.mdx`)

Protocol Version 2026-07-28

The 2026-07-28 revision turns MCP from a stateful, bidirectional protocol into
a stateless request/response one. Every request is self-describing, so any
request can land on any server instance behind a plain round-robin load
balancer.

mcp-go speaks this revision and every earlier one, deciding which to use per
connection. Existing code keeps working unchanged: if you do nothing, your
server continues to serve older clients exactly as before, and gains support
for newer ones.

What changed

Before 2026-07-28 From 2026-07-28
Connecting initialize / notifications/initialized handshake No handshake. Each request carries its version, identity, and capabilities in _meta.
Discovery The initialize response The server/discover RPC, which servers must implement and clients may skip
Sessions Mcp-Session-Id header, terminated with DELETE Removed. Servers ignore the header and answer GET and DELETE with 405.
Asking the user something Server-initiated requests over a held-open stream Multi round-trip requests
Change notifications A standalone GET SSE stream subscriptions/listen, with opt-in filters
Resumability Last-Event-ID replay Removed. Re-issue the request with a new ID.
Routing Parse the JSON body The Mcp-Method and Mcp-Name headers
Caching — ttlMs and cacheScope on list and read results
Removed RPCs — ping, logging/setLevel, resources/subscribe, resources/unsubscribe

Servers

A server built the usual way serves both eras on one endpoint:

go
srv := server.NewMCPServer("my-server", "1.0.0",
    server.WithToolCapabilities(true),
)

http.ListenAndServe(":8080", server.NewStreamableHTTPServer(srv))

The era is decided per request, from the protocol version the request declares.
Nothing about how you register tools, prompts, or resources changes.

Pinning to earlier revisions

Deployments that depend on protocol-level session state — per-session tools, for
example — can advertise only the revisions that provide it:

go
server.NewStreamableHTTPServer(srv,
    server.WithStreamableHTTPProtocolVersions(mcp.LegacyProtocolVersions()...),
)

Modern clients then receive an UnsupportedProtocolVersionError naming the
versions you do support. Configure the client explicitly for a legacy revision
to use the initialization handshake:

go
client.NewClient(httpTransport, client.WithProtocolVersion(mcp.ProtocolVersion20251125))

Cache hints

List and read results carry a freshness hint from this revision on. The default
asks clients to revalidate every time, which preserves earlier behaviour:

go
server.NewMCPServer("my-server", "1.0.0",
    // Clients may reuse tool and prompt catalogs for a minute.
    server.WithCacheHints(60_000, mcp.CacheScopePublic),
    // Resource contents are user-specific, so no shared cache may hold them.
    server.WithMethodCacheHints(mcp.MethodResourcesRead, 5_000, mcp.CacheScopePrivate),
)

Hints are only emitted to clients using 2026-07-28 or later.

Multi round-trip requests

A tool that needs something from the user mid-call used to send the client a
request over a held-open stream. That cannot work on a stateless protocol, so
the server now says what it needs and the client retries the call with the
answers attached.

Write the handler once, and it works for clients of either era:

go
srv.AddTool(mcp.NewTool("deploy"), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    // On the retry, the client's answer is waiting for us.
    if answer := server.ElicitationResponse(req.Params.InputResponses, "confirm"); answer != nil {
        if answer.Action != mcp.ElicitationResponseActionAccept {
            return mcp.NewToolResultText("deployment cancelled"), nil
        }
        return mcp.NewToolResultText("deployed"), nil
    }

    // First call: ask, and hand back whatever state we need to resume.
    return server.NewInputRequestBuilder("awaiting-confirmation").
        Elicit("confirm", mcp.ElicitationParams{
            Mode:    mcp.ElicitationModeForm,
            Message: "Deploy to production?",
            RequestedSchema: map[string]any{
                "type":       "object",
                "properties": map[string]any{"ok": map[string]any{"type": "boolean"}},
            },
        }).
        ToolResult(), nil
})

requestState is opaque to the client and echoed back verbatim, so use it to
carry whatever your handler needs to pick up where it left off. It is the right
place for a continuation token; it is not a place for secrets, since the client
can read it.

Against a client using an earlier revision, mcp-go issues the
elicitation/create that client understands and re-invokes your handler with
the answer. You do not write that path.

RequestSampling, RequestElicitation, and RequestRoots still exist, but
return an error when the client speaks 2026-07-28 or later, where the pattern
was removed. In-process servers are unaffected, since they call the handler
directly rather than sending a message.

Subscriptions

resources/subscribe and the standalone GET stream are replaced by a single
long-lived request. Clients opt in to specific notification types, and the
server intersects that with what it can actually serve:

go
srv := server.NewMCPServer("my-server", "1.0.0",
    server.WithToolCapabilities(true),          // enables toolsListChanged
    server.WithResourceCapabilities(true, true), // enables subscriptions and resourcesListChanged
)

Your notification-sending code is unchanged; delivery is filtered for sessions
that opened a subscription stream.

Logging

logging/setLevel is gone. Clients name a level per request, and a server must
not emit log notifications for a request that did not ask for one.
SendLogMessageToClient applies that rule automatically.

Clients

Initialize probes with server/discover and falls back to the handshake when
the probe fails without identifying a modern server. Successful discovery with
no compatible modern version returns an error. A version error with a supported
list allows at most one retry at a mutually supported modern version;
if negotiation fails, the client returns the error instead of switching eras.

go
c := client.NewClient(httpTransport)
if err := c.Start(ctx); err != nil {
    return err
}

var req mcp.InitializeRequest
req.Params.ClientInfo = mcp.Implementation{Name: "my-client", Version: "1.0.0"}

result, err := c.Initialize(ctx, req)  // result.ProtocolVersion tells you which era won

On a modern connection the client adds the required _meta and headers to every
request, and neither sends nor stores a session ID.

Multi round-trip requests

Register the handlers you already would, and CallTool, GetPrompt, and
ReadResource fulfil the server's requests and retry for you:

go
c := client.NewClient(httpTransport,
    client.WithElicitationHandler(myHandler),
    client.WithMaxInputRoundTrips(5),  // optional; the default is 10
)

// Returns the final result. Any input the server needed was gathered
// and resubmitted along the way.
result, err := c.CallTool(ctx, req)

To drive the exchange yourself, disable nothing — just check
result.NeedsInput() and pass InputResponses and RequestState on the next
call.

Subscriptions

go
stop, err := c.ListenAsync(ctx, mcp.SubscriptionFilter{
    ToolsListChanged:      true,
    ResourceSubscriptions: []string{"file:///config.json"},
}, func(err error) {
    log.Printf("subscription stream ended: %v", err)
})
defer stop()

Notifications reach the handlers registered with OnNotification as before.

Pinning to an earlier revision

go
c := client.NewClient(httpTransport,
    client.WithProtocolVersion(mcp.ProtocolVersion20251125),
)

A transport configured with WithContinuousListening pins itself
automatically, since that option depends on the GET stream this revision
removed.

Migrating

Nothing is required. When you are ready to adopt the new capabilities:

  1. Rewrite server-initiated requests as multi round-trip handlers. Replace
    RequestSampling, RequestElicitation, and RequestRoots calls with
    NewInputRequestBuilder. The result works for old and new clients alike.
  2. Stop relying on protocol-level session state. Mint an explicit handle
    from a tool and have the model pass it back as an argument. This works
    better than state hidden in the transport, because the model can see the
    handle and thread it between tools.
  3. Replace Subscribe with Listen on clients that consume notifications.
  4. Set cache hints for catalogs that do not change often.
  5. Move off WithContinuousListening, Ping, and SetLevel, all of which
    degrade to no-ops or local state on a modern connection.

Deprecated features remain functional for at least twelve months under the MCP
feature lifecycle policy.

Reference

Spec: 2026-07-28 changelog.
Key proposals: SEP-2575 (stateless core), SEP-2322 (multi round-trip),
SEP-2243 (header routing), SEP-2549 (cacheable results), SEP-2567 (session
removal), SEP-2577 (roots, sampling, and logging deprecation).


Www Docs Pages Quick Start (`www/docs/pages/quick-start.mdx`)

Quick Start

Get up and running with MCP-Go in minutes. This guide walks you through creating your first MCP server and client.

Hello World Server

Let's start with the simplest possible MCP server - a "hello world" tool:

go
package main

import (
    "context"
    "fmt"

    "github.com/mark3labs/mcp-go/mcp"
    "github.com/mark3labs/mcp-go/server"
)

func main() {
    // Create a new MCP server
    s := server.NewMCPServer(
        "Hello World Server",
        "1.0.0",
        server.WithToolCapabilities(true),
    )

    // Define a simple tool
    tool := mcp.NewTool("hello_world",
        mcp.WithDescription("Say hello to someone"),
        mcp.WithString("name",
            mcp.Required(),
            mcp.Description("Name of the person to greet"),
        ),
    )

    // Add tool handler
    s.AddTool(tool, helloHandler)

    // Start the stdio server
    if err := server.ServeStdio(s); err != nil {
        fmt.Printf("Server error: %v\n", err)
    }
}

func helloHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    arguments := request.GetArguments()
    name, ok := arguments["name"].(string)
    if !ok {
        return &mcp.CallToolResult{
            Content: []mcp.Content{
                mcp.TextContent{
                    Type: "text",
                    Text: "Error: name parameter is required and must be a string",
                },
            },
            IsError: true,
        }, nil
    }

    return &mcp.CallToolResult{
        Content: []mcp.Content{
            mcp.TextContent{
                Type: "text",
                Text: fmt.Sprintf("Hello, %s! 👋", name),
            },
        },
    }, nil
}

Save this as hello-server/main.go and run:

bash
cd hello-server
go mod init hello-server
go get github.com/mark3labs/mcp-go
go run main.go

Running Your First Server

Testing with Claude Desktop

  1. Install Claude Desktop from Anthropic's website

  2. Configure your server by editing Claude's config file:

    macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
    Windows: %APPDATA%\Claude\claude_desktop_config.json

    json
    {
      "mcpServers": {
        "hello-world": {
          "command": "go",
          "args": ["run", "/path/to/your/hello-server/main.go"]
        }
      }
    }
  3. Restart Claude Desktop and look for the 🔌 icon indicating MCP connection

  4. Test your tool by asking Claude: "Use the hello_world tool to greet Alice"

Testing with MCP Inspector

For debugging and development, use the MCP Inspector:

bash
# Install the MCP Inspector
npm install @modelcontextprotocol/inspector

# Run your server with the inspector
mcp-inspector go run main.go

This opens a web interface where you can test your tools interactively.

Basic Client Example

You can also create MCP clients to connect to other servers:

text
/* Detailed source-code truncated for AI context efficiency. */

StreamableHTTP Client Example

For StreamableHTTP-based servers, use the StreamableHTTP client:

text
/* Detailed source-code truncated for AI context efficiency. */

What's Next?

Now that you have a working MCP server and client:

  • Learn about Tools - Create powerful tool interfaces
  • Add Resources - Expose data sources to LLMs
  • Create Prompts - Build reusable prompt templates
  • Explore Advanced Features - Production-ready features

Common Issues

Server Won't Start

  • Check that the port isn't already in use
  • Verify Go module dependencies are installed
  • Ensure proper file permissions

Client Connection Failed

  • Verify the server is running and accessible
  • Check network connectivity for StreamableHTTP clients
  • Validate stdio command paths for stdio clients

Tool Calls Failing

  • Verify tool parameter types match the schema
  • Check error handling in your tool functions
  • Use the MCP Inspector for debugging

Www Docs Pages Clients Advanced Sampling (`www/docs/pages/clients/advanced-sampling.mdx`)

Sampling

Learn how to implement MCP clients that can handle sampling requests from servers, enabling bidirectional communication where clients provide LLM capabilities to servers.

Overview

Sampling allows MCP clients to respond to LLM completion requests from servers. When a server needs to generate content, answer questions, or perform reasoning tasks, it can send a sampling request to the client, which then processes it using an LLM and returns the result.

:::danger[Critical Security Requirement]
Per the MCP specification, sampling implementations SHOULD always include a human in the loop with the ability to deny sampling requests.

You MUST implement approval flows that:

  • Present each sampling request to the user for review before execution
  • Allow users to view and edit prompts before sending to the LLM
  • Display generated responses for user approval before returning to the server
  • Provide clear UI to accept or reject requests at each stage

Without human approval, your implementation:

  • Allows servers to make unauthorized LLM requests without user consent
  • May expose sensitive information through unreviewed prompts
  • Creates uncontrolled API costs from automated sampling
  • Violates user trust and security best practices

The examples below show basic handler implementation. You must add approval logic before using in production.
:::

Implementing a Sampling Handler

Create a sampling handler by implementing the SamplingHandler interface:

go
package main

import (
    "context"
    "fmt"
    
    "github.com/mark3labs/mcp-go/client"
    "github.com/mark3labs/mcp-go/client/transport"
    "github.com/mark3labs/mcp-go/mcp"
)

type MySamplingHandler struct {
    // Add fields for your LLM client (OpenAI, Anthropic, etc.)
}

func (h *MySamplingHandler) CreateMessage(ctx context.Context, request mcp.CreateMessageRequest) (*mcp.CreateMessageResult, error) {
    // Extract request parameters
    messages := request.Messages
    systemPrompt := request.SystemPrompt
    maxTokens := request.MaxTokens
    temperature := request.Temperature
    
    // Process with your LLM
    response, err := h.callLLM(ctx, messages, systemPrompt, maxTokens, temperature)
    if err != nil {
        return nil, fmt.Errorf("LLM call failed: %w", err)
    }
    
    // Return MCP-formatted result
    return &mcp.CreateMessageResult{
        Model: "your-model-name",
        Role:  mcp.RoleAssistant,
        Content: mcp.TextContent{
            Type: "text",
            Text: response,
        },
        StopReason: "endTurn",
    }, nil
}

func (h *MySamplingHandler) callLLM(ctx context.Context, messages []mcp.SamplingMessage, systemPrompt string, maxTokens int, temperature float64) (string, error) {
    // Implement your LLM integration here
    // This is where you'd call OpenAI, Anthropic, or other LLM APIs
    return "Your LLM response here", nil
}

Configuring the Client

Enable sampling by providing a handler when creating the client:

go
func main() {
    // Create sampling handler
    samplingHandler := &MySamplingHandler{}
    
    // Create stdio transport
    stdioTransport := transport.NewStdio("/path/to/mcp/server", nil)
    
    // Create client with sampling support
    mcpClient := client.NewClient(stdioTransport, client.WithSamplingHandler(samplingHandler))
    
    // Start the client
    ctx := context.Background()
    if err := mcpClient.Start(ctx); err != nil {
        log.Fatalf("Failed to start client: %v", err)
    }
    defer mcpClient.Close()
    if err := mcpClient.Connect(ctx); err != nil {
        log.Fatalf("Failed to connect: %v", err)
    }
    
    // The client will now automatically handle sampling requests
    // from the server using your handler
}

Mock Implementation Example

Here's a complete mock implementation for testing:

text
/* Detailed source-code truncated for AI context efficiency. */

Real LLM Integration

OpenAI Integration

text
/* Detailed source-code truncated for AI context efficiency. */

Anthropic Integration

text
/* Detailed source-code truncated for AI context efficiency. */

Automatic Capability Declaration

When you provide a sampling handler, the client automatically declares the sampling capability during initialization:

go
// This automatically adds sampling capability
stdioTransport := transport.NewStdio(serverPath, nil)
mcpClient := client.NewClient(stdioTransport, client.WithSamplingHandler(handler))

The client will include this in the initialization request:

json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2024-11-05",
    "capabilities": {
      "sampling": {}
    },
    "clientInfo": {
      "name": "your-client",
      "version": "1.0.0"
    }
  }
}

Sampling Sub-Capabilities (2025-11-25)

The 2025-11-25 protocol revision splits sampling into a baseline capability (the empty "sampling": {} object above) plus two opt-in sub-capabilities that a client uses to advertise which optional CreateMessageParams fields it actually understands:

go
type SamplingCapability struct {
    // Context advertises that the client honours CreateMessageParams.IncludeContext.
    // If nil, servers SHOULD only send IncludeContext "none" (or omit it).
    Context *struct{} `json:"context,omitempty"`
    // Tools advertises that the client honours CreateMessageParams.Tools and
    // CreateMessageParams.ToolChoice (sampling with tools).
    Tools   *struct{} `json:"tools,omitempty"`
}

The corresponding wire shape when both sub-capabilities are declared:

json
{
  "capabilities": {
    "sampling": { "context": {}, "tools": {} }
  }
}

client.WithSamplingHandler advertises only the baseline ("sampling": {}). Advertising the optional Context / Tools sub-capabilities through the high-level client is not yet exposed — the Initialize flow overrides the Sampling field on the supplied request when a sampling handler is registered. Until a dedicated option is added, code that needs the sub-capabilities on the wire must declare them at a lower layer (for example by sending its own initialize request through the transport). The type is in place so that:

  • Servers can inspect what their connected client advertised via session.GetClientCapabilities().Sampling (see Capability Gating on the server page).
  • Custom client integrations that build initialize requests directly can populate the sub-capabilities today.

:::warning[Compile-time break]
ClientCapabilities.Sampling and ServerCapabilities.Sampling are now *mcp.SamplingCapability (previously *struct{}). Code that constructed the capability with a &struct{}{} literal must be updated to &mcp.SamplingCapability{}. The JSON wire format for the baseline case is unchanged.
:::

Handling Sampling-With-Tools Requests

If your handler declared Sampling.Tools, incoming mcp.CreateMessageRequest values may include request.Tools (a list of tools the model is allowed to call) and request.ToolChoice (controls how aggressively the model should reach for them). Inspect them in your handler and surface them to your LLM:

go
func (h *MySamplingHandler) CreateMessage(ctx context.Context, request mcp.CreateMessageRequest) (*mcp.CreateMessageResult, error) {
    var allowedTools []mcp.Tool
    if len(request.Tools) > 0 {
        allowedTools = request.Tools
    }

    mode := mcp.ToolChoiceModeAuto // spec default when ToolChoice is omitted
    if request.ToolChoice != nil && request.ToolChoice.Mode != "" {
        mode = request.ToolChoice.Mode
    }

    switch mode {
    case mcp.ToolChoiceModeNone:
        // Tool use is disabled for this request.
    case mcp.ToolChoiceModeRequired:
        // Model must call at least one tool.
    case mcp.ToolChoiceModeAuto:
        // Model decides.
    }

    // ... forward `allowedTools` and `mode` to your LLM client and return the
    // response as a SamplingMessage (which may carry ToolUseContent /
    // ToolResultContent in addition to TextContent / ImageContent / AudioContent).
    return nil, nil
}

The constructors mcp.NewToolUseContent and mcp.NewToolResultContent build the corresponding SamplingMessage.Content values without manually setting the Type discriminator.

Error Handling

Handle errors gracefully in your sampling handler:

go
func (h *MySamplingHandler) CreateMessage(ctx context.Context, request mcp.CreateMessageRequest) (*mcp.CreateMessageResult, error) {
    // Validate request
    if len(request.Messages) == 0 {
        return nil, fmt.Errorf("no messages provided")
    }
    
    // Check for context cancellation
    if err := ctx.Err(); err != nil {
        return nil, fmt.Errorf("request cancelled: %w", err)
    }
    
    // Call LLM with error handling
    response, err := h.callLLM(ctx, request)
    if err != nil {
        // Log error for debugging
        log.Printf("LLM call failed: %v", err)
        
        // Return appropriate error
        if strings.Contains(err.Error(), "rate limit") {
            return nil, fmt.Errorf("rate limit exceeded, please try again later")
        }
        return nil, fmt.Errorf("LLM service unavailable: %w", err)
    }
    
    return response, nil
}

Best Practices

  1. Implement Proper Error Handling: Always handle LLM API errors gracefully
  2. Respect Rate Limits: Implement rate limiting and backoff strategies
  3. Validate Inputs: Check message content and parameters before processing
  4. Use Context: Respect context cancellation and timeouts
  5. Log Appropriately: Log requests for debugging but avoid logging sensitive data
  6. Model Selection: Allow configuration of which LLM model to use
  7. Content Filtering: Implement content filtering if required by your use case

Testing Your Implementation

Test your sampling handler with the sampling server example:

bash
# Build the sampling server
cd examples/sampling_server
go build -o sampling_server

# Build your client
go build -o my_client

# Test the integration
./my_client ./sampling_server

Transport Support

Sampling is available on the following transports:

STDIO Transport

For STDIO clients, create the transport and client separately:

go
stdioTransport := transport.NewStdio("/path/to/server", nil)
mcpClient := client.NewClient(stdioTransport, client.WithSamplingHandler(&MySamplingHandler{}))

In-Process Transport

For in-process clients, use the dedicated constructor:

go
mcpClient, err := client.NewInProcessClientWithSamplingHandler(
    mcpServer, 
    &MySamplingHandler{},
)

In-process sampling uses direct method calls instead of JSON-RPC serialization.

Unsupported Transports

SSE and StreamableHTTP transports do not support sampling due to their one-way or stateless nature.

Next Steps


Www Docs Pages Clients Basics (`www/docs/pages/clients/basics.mdx`)

Client Basics

Learn the fundamentals of creating and managing MCP clients, including lifecycle management, initialization, and error handling.

Creating Clients

MCP-Go provides client constructors for each supported transport. The choice of transport determines how your client communicates with the server.

Client Constructor Patterns

go
// STDIO client - for command-line tools
client, err := client.NewStdioMCPClient("command", "arg1", "arg2")

// StreamableHTTP client - for web services
client := client.NewStreamableHttpClient("http://localhost:8080/mcp")

// SSE client - for real-time web applications
client := client.NewSSEMCPClient("http://localhost:8080/mcp/sse")

// In-process client - for testing and embedded scenarios
client := client.NewInProcessClient(server)

STDIO Client Creation

go
package main

import (
    "context"
    "errors"
    "fmt"
    "log"
    "math"
    "net/http"
    "sync"
    "time"

    "github.com/mark3labs/mcp-go/client"
    "github.com/mark3labs/mcp-go/mcp"
)

func createStdioClient() (client.Client, error) {
    // Create client that spawns a subprocess
    c, err := client.NewStdioMCPClient(
        "go", []string{}, "run", "/path/to/server/main.go",
    )
    if err != nil {
        return nil, fmt.Errorf("failed to create STDIO client: %w", err)
    }

    return c, nil
}

// With custom environment variables
func createStdioClientWithEnv() (client.Client, error) {
    env := []string{
        "LOG_LEVEL=debug",
        "DATABASE_URL=sqlite://test.db",
    }
    c, err := client.NewStdioMCPClient(
        "go", env, "run", "/path/to/server/main.go",
    )
    if err != nil {
        return nil, fmt.Errorf("failed to create STDIO client: %w", err)
    }

    return c, nil
}

StreamableHTTP Client Creation

go
func createStreamableHTTPClient() client.Client {
    // Basic StreamableHTTP client
	httpTransport, err := transport.NewStreamableHTTP(server.URL,
		// Set timeout
		transport.WithHTTPTimeout(30*time.Second),
		// Set custom headers
		transport.WithHTTPHeaders(map[string]string{
			"X-Custom-Header": "custom-value",
			"Y-Another-Header": "another-value",
		}),
		// With custom HTTP client
		transport.WithHTTPBasicClient(&http.Client{}),
	)
    if err != nil {
        log.Fatalf("Failed to create StreamableHTTP transport: %v", err)
    }
    c := client.NewClient(httpTransport)
    return c
}

SSE Client Creation

go
func createSSEClient() client.Client {
    // Basic SSE client
	c, err := NewSSEMCPClient(testServer.URL+"/sse",
		// Set custom headers
		WithHeaders(map[string]string{
			"X-Custom-Header": "custom-value",
			"Y-Another-Header": "another-value",
		}),
	)
    return c
}

Client Lifecycle

Understanding the client lifecycle is crucial for proper resource management and error handling.

Lifecycle Stages

  1. Creation - Instantiate the client
  2. Initialization - Establish connection and exchange capabilities
  3. Operation - Use tools, resources, and prompts
  4. Cleanup - Close connections and free resources

Complete Lifecycle Example

text
/* Detailed source-code truncated for AI context efficiency. */

Initialization Process

The initialization process establishes the MCP connection and exchanges capabilities:

go
func initializeClientWithDetails(ctx context.Context, c client.Client) error {
    // Initialize with custom client info
    initReq := mcp.InitializeRequest{
        Params: mcp.InitializeRequestParams{
            ProtocolVersion: "2024-11-05",
            Capabilities: mcp.ClientCapabilities{
                Tools:     &mcp.ToolsCapability{},
                Resources: &mcp.ResourcesCapability{},
                Prompts:   &mcp.PromptsCapability{},
            },
            ClientInfo: mcp.ClientInfo{
                Name:    "My Application",
                Version: "1.0.0",
            },
        },
    }

    result, err := c.InitializeWithRequest(ctx, initReq)
    if err != nil {
        return fmt.Errorf("initialization failed: %w", err)
    }

    log.Printf("Connected to server: %s v%s", 
        result.ServerInfo.Name, 
        result.ServerInfo.Version)
    
    log.Printf("Server capabilities: %+v", result.Capabilities)

    return nil
}

Graceful Shutdown

text
/* Detailed source-code truncated for AI context efficiency. */

Error Handling

Proper error handling is essential for robust client applications.

Error Types

go
// Connection errors
var (
    ErrConnectionFailed = errors.New("connection failed")
    ErrConnectionLost   = errors.New("connection lost")
    ErrTimeout          = errors.New("operation timeout")
)

// Protocol errors
var (
    ErrInvalidResponse    = errors.New("invalid response")
    ErrProtocolViolation  = errors.New("protocol violation")
    ErrUnsupportedVersion = errors.New("unsupported protocol version")
)

// Operation errors
var (
    ErrToolNotFound       = errors.New("tool not found")
    ErrResourceNotFound   = errors.New("resource not found")
    ErrInvalidArguments   = errors.New("invalid arguments")
    ErrPermissionDenied   = errors.New("permission denied")
)

Comprehensive Error Handling

text
/* Detailed source-code truncated for AI context efficiency. */

Retry Logic with Exponential Backoff

text
/* Detailed source-code truncated for AI context efficiency. */

Context and Timeout Management

text
/* Detailed source-code truncated for AI context efficiency. */

Connection Monitoring

Health Checks

go
type ClientHealthMonitor struct {
    client   client.Client
    interval time.Duration
    timeout  time.Duration
    healthy  bool
    mutex    sync.RWMutex
}

func NewClientHealthMonitor(c client.Client, interval, timeout time.Duration) *ClientHealthMonitor {
    return &ClientHealthMonitor{
        client:   c,
        interval: interval,
        timeout:  timeout,
        healthy:  false,
    }
}

func (chm *ClientHealthMonitor) Start(ctx context.Context) {
    ticker := time.NewTicker(chm.interval)
    defer ticker.Stop()

    for {
        select {
        case <-ctx.Done():
            return
        case <-ticker.C:
            chm.checkHealth(ctx)
        }
    }
}

func (chm *ClientHealthMonitor) checkHealth(ctx context.Context) {
    ctx, cancel := context.WithTimeout(ctx, chm.timeout)
    defer cancel()

    // Try to list tools as a health check
    _, err := chm.client.ListTools(ctx, mcp.ListToolsRequest{})
    
    chm.mutex.Lock()
    chm.healthy = (err == nil)
    chm.mutex.Unlock()

    if err != nil {
        log.Printf("Health check failed: %v", err)
    }
}

func (chm *ClientHealthMonitor) IsHealthy() bool {
    chm.mutex.RLock()
    defer chm.mutex.RUnlock()
    return chm.healthy
}

Connection Recovery

text
/* Detailed source-code truncated for AI context efficiency. */

Next Steps


Www Docs Pages Clients Operations (`www/docs/pages/clients/operations.mdx`)

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

go
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, mcp.ListResourcesRequest{})
    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

text
/* Detailed source-code truncated for AI context efficiency. */

Reading Resources

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

Basic Resource Reading

text
/* Detailed source-code truncated for AI context efficiency. */

Typed Resource Reading

text
/* Detailed source-code truncated for AI context efficiency. */

Resource Caching

go
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

go
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, mcp.ListToolsRequest{})
    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

text
/* Detailed source-code truncated for AI context efficiency. */

Batch Tool Operations

text
/* Detailed source-code truncated for AI context efficiency. */

Using Prompts

Prompts provide reusable templates for LLM interactions.

Basic Prompt Usage

text
/* Detailed source-code truncated for AI context efficiency. */

Prompt Template Processing

go
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

text
/* Detailed source-code truncated for AI context efficiency. */

Subscriptions

Some transports support subscriptions for receiving real-time notifications.

Basic Subscription Handling

text
/* Detailed source-code truncated for AI context efficiency. */

Advanced Subscription Management

text
/* Detailed source-code truncated for AI context efficiency. */

Advanced: Sampling Support

Sampling is an advanced feature that allows clients to respond to LLM completion requests from servers. This enables servers to leverage client-side LLM capabilities for content generation and reasoning.

Note: Sampling is an advanced feature that most clients don't need. Only implement sampling if you're building a client that provides LLM capabilities to servers.

When to Implement Sampling

Consider implementing sampling when your client:

  • Has access to LLM APIs (OpenAI, Anthropic, etc.)
  • Wants to provide LLM capabilities to servers
  • Needs to support servers that generate dynamic content

Basic Implementation

go
import "github.com/mark3labs/mcp-go/client"

// Implement the SamplingHandler interface
type MySamplingHandler struct {
    // Add your LLM client here
}

func (h *MySamplingHandler) CreateMessage(ctx context.Context, request mcp.CreateMessageRequest) (*mcp.CreateMessageResult, error) {
    // Process the request with your LLM
    // Return the result in MCP format
    return &mcp.CreateMessageResult{
        Model: "your-model",
        Role:  mcp.RoleAssistant,
        Content: mcp.TextContent{
            Type: "text",
            Text: "Your LLM response here",
        },
        StopReason: "endTurn",
    }, nil
}

// Create client with sampling support
mcpClient, err := client.NewStdioClient(
    "/path/to/server",
    client.WithSamplingHandler(&MySamplingHandler{}),
)

For complete sampling documentation, see Client Sampling Guide.

Streaming Pagination with Iterators

MCP servers may return large result sets (tools, resources, resource templates,
or prompts) across multiple pages. The standard ListTools / ListResources /
ListPrompts / ListResourceTemplates methods auto-fetch every page and return
an aggregated slice — convenient, but wasteful when you only need a few items
or when the result set is very large.

For these cases, the client also exposes Go 1.23 iterator methods that lazily
fetch pages on demand:

  • IterTools(ctx, mcp.ListToolsRequest) iter.Seq2[mcp.Tool, error]
  • IterResources(ctx, mcp.ListResourcesRequest) iter.Seq2[mcp.Resource, error]
  • IterResourceTemplates(ctx, mcp.ListResourceTemplatesRequest) iter.Seq2[mcp.ResourceTemplate, error]
  • IterPrompts(ctx, mcp.ListPromptsRequest) iter.Seq2[mcp.Prompt, error]

Each iterator yields one item at a time and only fetches the next page when the
current one is exhausted. Breaking out of the range stops further requests,
so early-exit is cheap.

:::note
These methods are defined on the concrete *client.Client type, not on the
MCPClient interface. The examples below use c *client.Client rather than
the c client.Client you'll see elsewhere in this guide. If you're holding
an MCPClient interface value, use the two-value type assertion so a
mismatched implementation doesn't panic:

go
cc, ok := c.(*client.Client)
if !ok {
    return fmt.Errorf("iterators require *client.Client, got %T", c)
}
// safe to call cc.IterTools / cc.IterResources / ... here

Throughout the rest of this section, c refers to a *client.Client directly
— substitute cc (or whatever you named the asserted value) when you arrive
via the interface.
:::

Basic Usage

go
func printToolNames(ctx context.Context, c *client.Client) error {
    for tool, err := range c.IterTools(ctx, mcp.ListToolsRequest{}) {
        if err != nil {
            return fmt.Errorf("list tools: %w", err)
        }
        fmt.Println(tool.Name)
    }
    return nil
}

Early Exit

The iterator stops fetching pages as soon as the loop terminates, which is
useful for searches or first-match lookups:

go
func findTool(ctx context.Context, c *client.Client, name string) (*mcp.Tool, error) {
    for tool, err := range c.IterTools(ctx, mcp.ListToolsRequest{}) {
        if err != nil {
            return nil, err
        }
        if tool.Name == name {
            return &tool, nil // no further pages are fetched
        }
    }
    return nil, fmt.Errorf("tool %q not found", name)
}

Error Handling and Cancellation

If a page fetch fails, or the context is cancelled between pages, the iterator
yields a single (zero-value, err) pair and then stops. Always check err on
each iteration:

go
func processResources(ctx context.Context, c *client.Client) error {
    for resource, err := range c.IterResources(ctx, mcp.ListResourcesRequest{}) {
        if err != nil {
            // ctx.Err() or a transport / server error — iterator is now done.
            return err
        }
        process(resource)
    }
    return nil
}

When to Use Which

Method Behavior Best for
ListTools / ListResources / ... Auto-fetches all pages, returns an aggregated slice. Small result sets, when you need the full list.
ListToolsByPage / ListResourcesByPage / ... Fetches a single page; caller manages cursors. Custom pagination UI, manual cursor control.
IterTools / IterResources / ... Lazily fetches pages as the iterator is consumed. Large result sets, searches, early-exit lookups.

As mentioned above, the iterator methods live on the concrete *client.Client
type rather than the MCPClient interface, so the interface remains
backwards-compatible for existing implementers.

Next Steps


Www Docs Pages Clients Transports (`www/docs/pages/clients/transports.mdx`)

Client Transports

Learn about transport-specific client implementations and how to choose the right transport for your use case.

Transport Overview

MCP-Go provides client implementations for all supported transports. Each transport has different characteristics and is optimized for specific scenarios.

Transport Best For Connection Real-time Multi-client
STDIO CLI tools, desktop apps Process pipes No No
StreamableHTTP Web services, APIs HTTP requests No Yes
SSE Web apps, real-time HTTP + EventSource Yes Yes
In-Process Testing, embedded Direct calls Yes No

STDIO Client

STDIO clients communicate with servers through standard input/output, typically by spawning a subprocess.

Basic STDIO Client

go
package main

import (
    "context"
    "crypto/tls"
    "errors"
    "fmt"
    "log"
    "net/http"
    "os"
    "sync"
    "time"

    "github.com/mark3labs/mcp-go/client"
    "github.com/mark3labs/mcp-go/mcp"
    "github.com/mark3labs/mcp-go/server"
)

func createStdioClient() {
    // Create client that spawns a subprocess
    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 connection
    if err := c.Initialize(ctx, initRequest); err != nil {
        log.Fatal(err)
    }

    // Use the client
    tools, err := c.ListTools(ctx, mcp.ListToolsRequest{})
    if err != nil {
        log.Fatal(err)
    }

    log.Printf("Available tools: %d", len(tools.Tools))
}

STDIO Client with Custom Configuration

go
func createCustomStdioClient() {
    // Create custom logger for debugging
    logger := myCustomLogger{}

    // Create STDIO client with custom options
    c, err := client.NewStdioMCPClientWithOptions(
        "go",
        []string{"GOCACHE=/tmp/gocache"}, // Custom environment
        []string{"run", "/path/to/server/main.go"},
        transport.WithCommandLogger(logger),
        transport.WithCommandFunc(func(ctx context.Context, command string, args []string, env []string) (*exec.Cmd, error) {
            cmd := exec.CommandContext(ctx, command, args...)
            cmd.Env = append(os.Environ(), env...)
            cmd.Dir = "/path/to/working/directory"
            return cmd, nil
        }),
    )
    if err != nil {
        log.Fatal(err)
    }
    defer c.Close()

    ctx := context.Background()

    // Initialize connection
    if err := c.Initialize(ctx); err != nil {
        log.Fatal(err)
    }

    // Use the client...
}

STDIO Error Handling

text
/* Detailed source-code truncated for AI context efficiency. */

STDIO Process Management

text
/* Detailed source-code truncated for AI context efficiency. */
go
// Define connection error constants
var (
    ErrConnectionLost   = errors.New("connection lost")
    ErrConnectionFailed = errors.New("connection failed")
    ErrUnauthorized     = errors.New("unauthorized")
    ErrForbidden        = errors.New("forbidden")
)

StreamableHTTP Client

StreamableHTTP clients communicate with servers using traditional HTTP requests.

Automatic Fallback to Legacy SSE

When connecting to a server that only supports the legacy SSE transport, the StreamableHTTP client will receive a 4xx error during initialization. Use the ErrLegacySSEServer sentinel error to detect this and fall back automatically:

go
import "github.com/mark3labs/mcp-go/client/transport"

func connectWithFallback(ctx context.Context, url string) (*client.Client, error) {
    // Try StreamableHTTP first
    streamableTransport, err := transport.NewStreamableHTTP(url)
    if err != nil {
        return nil, err
    }

    c := client.New(streamableTransport)
    if err := c.Start(ctx); err != nil {
        if errors.Is(err, transport.ErrLegacySSEServer) {
            // Fall back to legacy SSE transport
            sseTransport, err := transport.NewSSE(url)
            if err != nil {
                return nil, err
            }
            c = client.New(sseTransport)
            if err := c.Start(ctx); err != nil {
                return nil, err
            }
            return c, nil
        }
        return nil, err
    }
    return c, nil
}

Basic StreamableHTTP Client

go
func createStreamableHTTPClient() {
    // Create StreamableHTTP client
    c := client.NewStreamableHttpClient("http://localhost:8080/mcp")
    defer c.Close()

    ctx := context.Background()

    // Initialize
    if err := c.Initialize(ctx); err != nil {
        log.Fatal(err)
    }

    // Use client
    tools, err := c.ListTools(ctx, mcp.ListToolsRequest{})
    if err != nil {
        log.Fatal(err)
    }

    log.Printf("Available tools: %d", len(tools.Tools))
}

StreamableHTTP Client with Custom Configuration

go
func createCustomStreamableHTTPClient() {
    // Create custom logger for debugging
    logger := myCustomLogger{}

    // Create StreamableHTTP client with options
    c := client.NewStreamableHttpClient("https://api.example.com/mcp",
        transport.WithLogger(logger),
        transport.WithHTTPTimeout(30*time.Second),
        transport.WithHTTPHeaders(map[string]string{
            "User-Agent": "MyApp/1.0",
            "Accept":     "application/json",
        }),
        transport.WithHTTPBasicClient(&http.Client{
            Transport: &http.Transport{
                MaxIdleConns:        100,
                MaxIdleConnsPerHost: 10,
                IdleConnTimeout:     90 * time.Second,
                TLSClientConfig: &tls.Config{
                    InsecureSkipVerify: false,
                },
            },
        }),
    )
    defer c.Close()

    ctx := context.Background()

    if err := c.Initialize(ctx); err != nil {
        log.Fatal(err)
    }

    // Use client...
}

Persisting OAuth issuer bindings

Save handler.GetCredentialsIssuer() alongside GetClientID(),
GetClientSecret() and GetTokenEndpointAuthMethod(). Restore that exact issuer
in OAuthConfig.CredentialsIssuer. Custom TokenStore implementations must
also preserve Token.Issuer; it is trusted local provenance stamped by the
handler, not an issuer supplied by the token endpoint.

Configured credentials (including restored dynamic registrations) are never
silently replaced when discovery changes issuer. Handle the inspectable error
and explicitly register replacement credentials if appropriate:

go
if errors.Is(err, client.ErrAuthorizationServerChanged) {
    // Verify the new authorization server before opting into registration.
    if err := handler.RegisterClient(ctx, "My MCP client"); err != nil {
        return err
    }
    // Start a fresh authorization flow; do not reuse the old code or token.
}

Registrations created by the current handler automatically re-register on
GetAuthorizationURL when the issuer changes, retaining the original client
name. A failed migration never falls back to the old credentials. Issuers are
compared exactly, including paths and trailing slashes; changing endpoints
without changing issuer does not require registration. SkipIssuerMetadataValidation
does not disable credential binding. Leave AuthServerMetadataURL unset to follow
protected-resource authorization server discovery. Setting it explicitly pins
discovery to that metadata URL and bypasses protected-resource discovery.

For compatibility, configured credentials with no CredentialsIssuer bind on
first authorization use. This trust-on-first-use behavior cannot protect a
restart that loses provenance: persist the issuer for restart safety. Legacy
unexpired access tokens remain usable until a known issuer change, but expired
tokens without Token.Issuer require fresh authorization, not inferred refresh.
RefreshToken accepts raw tokens only with matching stored token provenance or
an explicitly configured CredentialsIssuer; a newly learned binding is not
proof of a raw refresh token's origin.

StreamableHTTP Authentication

go
func createAuthenticatedStreamableHTTPClient() (*client.Client, error) {
    c, err := client.NewOAuthStreamableHttpClient("http://localhost:8080/mcp",
        client.OAuthConfig{
            ClientID:              "your-client-id",
            ClientSecret:          "your-client-secret",
            CredentialsIssuer:     "https://auth.example.com",
            RedirectURI:           "http://localhost:8085/callback",
            Scopes:                []string{"mcp:read", "mcp:write"},
        },
    )
    if err != nil {
        return nil, err
    }
    // Start and initialize the client, completing OAuth if required.
    // The caller owns c and must close it when finished.
    return c, nil
}

StreamableHTTP Connection Pooling

go
type StreamableHTTPClientPool struct {
    clients chan *client.Client
    factory func() *client.Client
    maxSize int
}

func NewStreamableHTTPClientPool(baseURL string, maxSize int) *StreamableHTTPClientPool {
    pool := &StreamableHTTPClientPool{
        clients: make(chan *client.Client, maxSize),
        maxSize: maxSize,
        factory: func() *client.Client {
            return client.NewStreamableHttpClient(baseURL)
        },
    }

    // Pre-populate pool
    for i := 0; i < maxSize; i++ {
        pool.clients <- pool.factory()
    }

    return pool
}

func (pool *StreamableHTTPClientPool) Get() *client.Client {
    select {
    case c := <-pool.clients:
        return c
    default:
        return pool.factory()
    }
}

func (pool *StreamableHTTPClientPool) Put(c *client.Client) {
    select {
    case pool.clients <- c:
    default:
        // Pool full, close client
        c.Close()
    }
}

func (pool *StreamableHTTPClientPool) CallTool(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    c := pool.Get()
    defer pool.Put(c)

    return c.CallTool(ctx, req)
}

StreamableHTTP With Preconfigured Session

You can also create a StreamableHTTP client with a preconfigured session, which allows you to reuse the same session across multiple requests

go
func createStreamableHTTPClientWithSession() {
    // Create StreamableHTTP client with options
    sessionID := // fetch existing session ID
    c := client.NewStreamableHttpClient("https://api.example.com/mcp",
        transport.WithSession(sessionID),
    )
    defer c.Close()

    ctx := context.Background()
    // Use client...
    _, err := c.ListTools(ctx, mcp.ListToolsRequest{})
    // If the session is terminated, you must reinitialize the client
    if errors.Is(err, transport.ErrSessionTerminated) {
        c.Initialize(ctx) // Reinitialize if session is terminated
        // The session ID should change after reinitialization
        sessionID = c.GetSessionId() // Update session ID
    }
}

Once the server has answered 404 for the session, every request fails with transport.ErrSessionTerminated until Initialize succeeds again. With continuous listening, the GET stream moves to the new session.

SSE Client

SSE (Server-Sent Events) clients provide real-time communication with servers.

Basic SSE Client

go
func createSSEClient() {
    // Create SSE client
    c, err := client.NewSSEMCPClient("http://localhost:8080/mcp/sse",
        transport.WithHeaders(map[string]string{
            "Authorization": "Bearer your-token",
        }),
    )
    if err != nil {
        log.Fatal(err)
    }
    defer c.Close()

    ctx := context.Background()

    // Initialize
    if err := c.Initialize(ctx); err != nil {
        log.Fatal(err)
    }

    // Subscribe to notifications
    notifications, err := c.Subscribe(ctx)
    if err != nil {
        log.Fatal(err)
    }

    // Handle notifications in background
    go func() {
        for notification := range notifications {
            log.Printf("Notification: %+v", notification)
        }
    }()

    // Use client for regular operations
    tools, err := c.ListTools(ctx, mcp.ListToolsRequest{})
    if err != nil {
        log.Fatal(err)
    }

    log.Printf("Available tools: %d", len(tools.Tools))
}

SSE Client with Custom Configuration

go
func createCustomSSEClient() {
    // Create custom logger for debugging
    logger := myCustomLogger{}

    // Create SSE client with custom options
    c, err := client.NewSSEMCPClient("http://localhost:8080/mcp/sse",
        transport.WithSSELogger(logger),
        transport.WithHeaders(map[string]string{
            "Authorization": "Bearer your-token",
            "User-Agent":    "MyApp/1.0",
        }),
        transport.WithHTTPClient(&http.Client{
            Timeout: 30 * time.Second,
        }),
    )
    if err != nil {
        log.Fatal(err)
    }
    defer c.Close()

    ctx := context.Background()

    // Initialize
    if err := c.Initialize(ctx); err != nil {
        log.Fatal(err)
    }

    // Use client...
}

SSE Client with Reconnection

text
/* Detailed source-code truncated for AI context efficiency. */

SSE Event Handling

text
/* Detailed source-code truncated for AI context efficiency. */

In-Process Client

In-process clients provide direct communication with servers in the same process.

Basic In-Process Client

go
func createInProcessClient() {
    // Create server
    s := server.NewMCPServer("Test Server", "1.0.0")

    // Add tools to server
    s.AddTool(
        mcp.NewTool("test_tool",
            mcp.WithDescription("Test tool"),
            mcp.WithString("input", mcp.Required()),
        ),
        func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
            input := req.Params.Arguments["input"].(string)
            return mcp.NewToolResultText("Processed: " + input), nil
        },
    )

    // Create in-process client
    c := client.NewInProcessClient(s)
    defer c.Close()

    ctx := context.Background()

    // Initialize (no network overhead)
    if err := c.Initialize(ctx); err != nil {
        log.Fatal(err)
    }

    // Use client
    result, err := c.CallTool(ctx, mcp.CallToolRequest{
        Params: mcp.CallToolRequestParams{
            Name: "test_tool",
            Arguments: map[string]interface{}{
                "input": "test data",
            },
        },
    })
    if err != nil {
        log.Fatal(err)
    }

    log.Printf("Tool result: %+v", result)
}

In-Process Client for Testing

text
/* Detailed source-code truncated for AI context efficiency. */

Transport Selection

Decision Matrix

Choose your transport based on these factors:

text
/* Detailed source-code truncated for AI context efficiency. */

Multi-Transport Client Factory

text
/* Detailed source-code truncated for AI context efficiency. */

Logging Configuration

All client transports support custom logging.
Each transport provides a logger option that accepts any implementation of the util.Logger interface.

go
type myCustomLogger struct {}

func (myCustomLogger) Infof(format string, args ...any) {
    // TODO
}

func (myCustomLogger) Errorf(format string, args ...any) {
    // TODO
}

JSON-RPC Tracing with `LoggingTransport`

For debugging the wire protocol itself, wrap any transport with
transport.NewLogging. The wrapper is a transparent decorator: it implements
transport.Interface and additionally BidirectionalInterface /
HTTPConnection whenever the inner transport does, so type assertions in the
client continue to behave as expected.

go
import (
    "log/slog"
    "os"

    "github.com/mark3labs/mcp-go/client"
    "github.com/mark3labs/mcp-go/client/transport"
)

func tracedClient() (*client.Client, error) {
    inner, err := transport.NewStdio("my-server", nil)
    if err != nil {
        return nil, err
    }

    logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
        Level: slog.LevelDebug,
    }))

    // Wrap the transport so every JSON-RPC message is logged.
    logged := transport.NewLogging(inner, logger,
        transport.WithLoggingLevel(slog.LevelDebug),
        transport.WithLoggingPayloads(true), // include params/result bodies
    )

    return client.NewClient(logged), nil
}

Each record carries the message direction, method name, request id and (for
responses) the duration of the round-trip:

text
level=DEBUG msg="→ request"  method=initialize id=1
level=DEBUG msg="← response" method=initialize id=1 duration=12ms
level=DEBUG msg="→ request"  method=tools/list id=2
level=DEBUG msg="← response" method=tools/list id=2 duration=3ms
level=DEBUG msg="← notification" method=notifications/tools/list_changed

Use WithLoggingPayloads(false) to suppress message bodies when only the
method/id metadata is required, and WithLoggingLevel to change the slog
level at which records are emitted.


Www Docs Pages Servers Advanced Sampling (`www/docs/pages/servers/advanced-sampling.mdx`)

Sampling

Learn how to implement MCP servers that can request LLM completions from clients using the sampling capability.

Overview

Sampling allows MCP servers to request LLM completions from clients, enabling bidirectional communication where servers can leverage client-side LLM capabilities. This is particularly useful for tools that need to generate content, answer questions, or perform reasoning tasks.

:::info[User Consent Required]
Per the MCP specification, clients SHOULD implement human-in-the-loop approval for sampling requests.

When you request sampling from a client:

  • The user will typically be prompted to review and approve your request
  • The user may modify your prompts before sending to their LLM
  • The user may reject your request entirely
  • Response times may be longer due to user interaction

Design your tools accordingly:

  • Provide clear descriptions of why sampling is needed
  • Use descriptive system prompts explaining the purpose
  • Handle rejection errors gracefully
  • Consider timeouts for user approval delays
  • Don't assume immediate or automatic approval

Well-designed sampling requests improve user trust and approval rates.
:::

Enabling Sampling

To enable sampling in your server, call EnableSampling() during server setup:

go
package main

import (
    "context"
    "github.com/mark3labs/mcp-go/mcp"
    "github.com/mark3labs/mcp-go/server"
)

func main() {
    // Create server
    mcpServer := server.NewMCPServer("my-server", "1.0.0")
    
    // Enable sampling capability
    mcpServer.EnableSampling()
    
    // Add tools that use sampling...
    
    // Start server
    server.ServeStdio(mcpServer)
}

Requesting Sampling

Use RequestSampling() within tool handlers to request LLM completions:

text
/* Detailed source-code truncated for AI context efficiency. */

Sampling Request Parameters

The CreateMessageRequest supports various parameters to control LLM behavior:

go
samplingRequest := mcp.CreateMessageRequest{
    CreateMessageParams: mcp.CreateMessageParams{
        // Required: Messages to send to the LLM
        Messages: []mcp.SamplingMessage{
            {
                Role: mcp.RoleUser,        // or mcp.RoleAssistant
                Content: mcp.TextContent{   // or mcp.ImageContent
                    Type: "text",
                    Text: "Your message here",
                },
            },
        },

        // Optional: System prompt for context
        SystemPrompt: "You are a helpful assistant.",

        // Optional: Maximum tokens to generate
        MaxTokens: 1000,

        // Optional: Temperature for randomness (0.0 to 1.0)
        Temperature: 0.7,

        // Optional: Stop sequences
        StopSequences: []string{"\\n\\n"},

        // Optional (2025-11-25): Context inclusion hint. Only send a value other
        // than "none" when the client advertised `sampling.context` — see
        // [Capability Gating](#capability-gating-2025-11-25) below.
        IncludeContext: "thisServer",

        // Optional (2025-11-25): Tools the model may use during generation, and
        // how aggressively the model should reach for them. Only send these when
        // the client advertised `sampling.tools`.
        Tools: []mcp.Tool{ /* ... */ },
        ToolChoice: &mcp.ToolChoice{Mode: mcp.ToolChoiceModeAuto},
    },
}

Capability Gating (2025-11-25)

The 2025-11-25 protocol revision splits sampling into a baseline capability and two opt-in sub-capabilities. Inspect the connected session's ClientCapabilities.Sampling before sending the gated fields — a conforming client MUST reject a request that violates the matrix below.

Field on CreateMessageParams Required client sub-capability Sub-capability is nil ⇒
IncludeContext (any value other than "none") Sampling.Context Omit the field or send "none"
Tools Sampling.Tools Omit the field
ToolChoice Sampling.Tools Omit the field

A typical pattern inside a tool handler:

go
func askLLMWithTools(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    session := server.ClientSessionFromContext(ctx)
    sessionWithClient, ok := session.(server.SessionWithClientInfo)
    if !ok {
        return mcp.NewToolResultError("client session is not capability-aware"), nil
    }
    caps := sessionWithClient.GetClientCapabilities()

    params := mcp.CreateMessageParams{
        Messages:  []mcp.SamplingMessage{ /* ... */ },
        MaxTokens: 1000,
    }

    // Only attach Tools / ToolChoice if the client opted in.
    if caps.Sampling != nil && caps.Sampling.Tools != nil {
        params.Tools = []mcp.Tool{ /* ... */ }
        params.ToolChoice = &mcp.ToolChoice{Mode: mcp.ToolChoiceModeAuto}
    }

    // Only ask for context inclusion if the client opted in.
    if caps.Sampling != nil && caps.Sampling.Context != nil {
        params.IncludeContext = "thisServer"
    }

    result, err := mcpServer.RequestSampling(ctx, mcp.CreateMessageRequest{CreateMessageParams: params})
    // ...
}

ToolChoiceMode accepts three constants:

Constant Wire value Meaning
mcp.ToolChoiceModeAuto "auto" Model decides (default when ToolChoice is omitted).
mcp.ToolChoiceModeRequired "required" Model must call at least one tool.
mcp.ToolChoiceModeNone "none" Tool use is disabled for this request.

Message Types

Sampling supports different message roles and content types:

Message Roles

go
// User message
{
    Role: mcp.RoleUser,
    Content: mcp.TextContent{
        Type: "text",
        Text: "What is the capital of France?",
    },
}

// Assistant message (for conversation context)
{
    Role: mcp.RoleAssistant,
    Content: mcp.TextContent{
        Type: "text", 
        Text: "The capital of France is Paris.",
    },
}

Content Types

Text Content

go
mcp.TextContent{
    Type: "text",
    Text: "Your text content here",
}

Image Content

go
mcp.ImageContent{
    Type: "image",
    Data: "base64-encoded-image-data",
    MimeType: "image/jpeg",
}

Tool-Use and Tool-Result Content (2025-11-25)

When sampling with tools is in play, intermediate messages in the conversation
can also carry ToolUseContent (the model asked to call a tool) and
ToolResultContent (the result of that call) — these flow through the same
SamplingMessage.Content field:

go
// Assistant requesting a tool call
mcp.ToolUseContent{
    Type:  "tool_use",
    ID:    "tu_1",
    Name:  "get_weather",
    Input: map[string]any{"city": "London"},
}

// User-role message carrying the tool's output
mcp.ToolResultContent{
    Type:      "tool_result",
    ToolUseID: "tu_1",
    Content:   []mcp.Content{mcp.NewTextContent("Sunny, 22°C")},
}

Use the constructors mcp.NewToolUseContent and mcp.NewToolResultContent to build these without writing the Type discriminator by hand.

Error Handling

Always handle sampling errors gracefully:

go
result, err := mcpServer.RequestSampling(ctx, samplingRequest)
if err != nil {
    // Log the error
    log.Printf("Sampling request failed: %v", err)
    
    // Return appropriate error response
    return &mcp.CallToolResult{
        Content: []mcp.Content{
            mcp.TextContent{
                Type: "text",
                Text: "Sorry, I couldn't process your request at this time.",
            },
        },
        IsError: true,
    }, nil
}

Context and Timeouts

Use context for timeout control:

go
// Set a timeout for the sampling request
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()

result, err := mcpServer.RequestSampling(ctx, samplingRequest)

Best Practices

  1. Enable Sampling Early: Call EnableSampling() during server initialization
  2. Handle Timeouts: Set appropriate timeouts for sampling requests
  3. Graceful Errors: Always provide meaningful error messages to users
  4. Content Extraction: Use helper functions to extract text from responses
  5. System Prompts: Use clear system prompts to guide LLM behavior
  6. Parameter Validation: Validate tool parameters before making sampling requests

Complete Example

Here's a complete example server with sampling:

text
/* Detailed source-code truncated for AI context efficiency. */

Transport Support

Sampling is supported on the following transports:

STDIO Transport

STDIO transport provides full sampling support with JSON-RPC message passing:

go
// Start STDIO server with sampling
server.ServeStdio(mcpServer)

The client must implement a SamplingHandler and declare sampling capability during initialization.

In-Process Transport

In-process transport offers the most efficient sampling implementation with direct method calls:

go
// Create in-process client with sampling handler
mcpClient, err := client.NewInProcessClientWithSamplingHandler(mcpServer, samplingHandler)
if err != nil {
    log.Fatal(err)
}

Benefits of in-process sampling:

  • Direct Method Calls: No JSON-RPC serialization overhead
  • Type Safety: Compile-time type checking

Unsupported Transports

The following transports do not currently support sampling:

  • SSE Transport: One-way streaming nature prevents bidirectional sampling
  • StreamableHTTP Transport: Stateless HTTP requests don't support sampling callbacks

For these transports, consider implementing LLM integration directly in your tool handlers rather than using sampling.

Next Steps


Www Docs Pages Servers Advanced (`www/docs/pages/servers/advanced.mdx`)

Advanced Server Features

Icons

Icons provide visual identifiers for servers, tools, resources, and prompts. All icon URIs must use HTTPS or data URI schemes for security.

Security Considerations

  • Always validate icon sources are from trusted domains
  • Enforce size limits to prevent memory issues
  • Support at least PNG and JPEG formats
  • Consider supporting SVG and WebP for better quality/size

Example with Data URI

go
mcp.Icon{
    Src:      "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiLz4=",
    MIMEType: "image/svg+xml",
}

Theme-Specific Icons

Icons can optionally declare which background theme they're designed for. Clients use the hint to pick the right asset when rendering against a light or dark UI:

go
icons := []mcp.Icon{
    {
        Src:      "https://example.com/icons/tool-light.svg",
        MIMEType: "image/svg+xml",
        Theme:    mcp.IconThemeLight,    // Designed for light backgrounds
    },
    {
        Src:      "https://example.com/icons/tool-dark.svg",
        MIMEType: "image/svg+xml",
        Theme:    mcp.IconThemeDark,     // Designed for dark backgrounds
    },
}

The Theme field is optional — an icon without a theme is treated as theme-agnostic. The supported constants are mcp.IconThemeLight and mcp.IconThemeDark; unknown values are passed through unchanged for forward compatibility.

Explore powerful features that make MCP-Go servers production-ready: typed tools, session management, middleware, hooks, and more.

Typed Tools

Typed tools provide compile-time type safety and automatic parameter validation, reducing boilerplate and preventing runtime errors.

Basic Typed Tool

text
/* Detailed source-code truncated for AI context efficiency. */

Complex Typed Tool

text
/* Detailed source-code truncated for AI context efficiency. */

Custom Validation

go
import (
    "path/filepath"
    "strings"

    "github.com/go-playground/validator/v10"
)

type FileOperationInput struct {
    Path      string `json:"path" validate:"required,filepath"`
    Operation string `json:"operation" validate:"required,oneof=read write delete"`
    Content   string `json:"content" validate:"required_if=Operation write"`
}

// Custom validator
func init() {
    validate := validator.New()
    validate.RegisterValidation("filepath", validateFilePath)
}

func validateFilePath(fl validator.FieldLevel) bool {
    path := fl.Field().String()
    
    // Prevent directory traversal
    if strings.Contains(path, "..") {
        return false
    }
    
    // Ensure path is within allowed directory
    allowedDir := "/app/data"
    absPath, err := filepath.Abs(path)
    if err != nil {
        return false
    }
    
    return strings.HasPrefix(absPath, allowedDir)
}

Session Management

Handle multiple clients with per-session state and tools.

Per-Session State

go
type SessionState struct {
    UserID      string
    Permissions []string
    Settings    map[string]interface{}
    StartTime   time.Time
}

type SessionManager struct {
    sessions map[string]*SessionState
    mutex    sync.RWMutex
}

func NewSessionManager() *SessionManager {
    return &SessionManager{
        sessions: make(map[string]*SessionState),
    }
}

func (sm *SessionManager) CreateSession(sessionID, userID string, permissions []string) {
    sm.mutex.Lock()
    defer sm.mutex.Unlock()
    
    sm.sessions[sessionID] = &SessionState{
        UserID:      userID,
        Permissions: permissions,
        Settings:    make(map[string]interface{}),
        StartTime:   time.Now(),
    }
}

func (sm *SessionManager) GetSession(sessionID string) (*SessionState, bool) {
    sm.mutex.RLock()
    defer sm.mutex.RUnlock()
    
    session, exists := sm.sessions[sessionID]
    return session, exists
}

func (sm *SessionManager) RemoveSession(sessionID string) {
    sm.mutex.Lock()
    defer sm.mutex.Unlock()
    
    delete(sm.sessions, sessionID)
}

Session-Aware Tools

text
/* Detailed source-code truncated for AI context efficiency. */

Middleware

Add cross-cutting concerns like logging, authentication, and rate limiting.

Logging Middleware

text
/* Detailed source-code truncated for AI context efficiency. */

Rate Limiting Middleware

go
type RateLimitMiddleware struct {
    limiters map[string]*rate.Limiter
    mutex    sync.RWMutex
    rate     rate.Limit
    burst    int
}

func NewRateLimitMiddleware(requestsPerSecond float64, burst int) *RateLimitMiddleware {
    return &RateLimitMiddleware{
        limiters: make(map[string]*rate.Limiter),
        rate:     rate.Limit(requestsPerSecond),
        burst:    burst,
    }
}

func (m *RateLimitMiddleware) getLimiter(sessionID string) *rate.Limiter {
    m.mutex.RLock()
    limiter, exists := m.limiters[sessionID]
    m.mutex.RUnlock()
    
    if !exists {
        m.mutex.Lock()
        limiter = rate.NewLimiter(m.rate, m.burst)
        m.limiters[sessionID] = limiter
        m.mutex.Unlock()
    }
    
    return limiter
}

func (m *RateLimitMiddleware) ToolMiddleware(next server.ToolHandler) server.ToolHandler {
    return func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
        sessionID := server.GetSessionID(ctx)
        limiter := m.getLimiter(sessionID)
        
        if !limiter.Allow() {
            return nil, fmt.Errorf("rate limit exceeded for session %s", sessionID)
        }
        
        return next(ctx, req)
    }
}

Authentication Middleware

go
type AuthMiddleware struct {
    tokenValidator TokenValidator
}

func NewAuthMiddleware(validator TokenValidator) *AuthMiddleware {
    return &AuthMiddleware{tokenValidator: validator}
}

func (m *AuthMiddleware) ToolMiddleware(next server.ToolHandler) server.ToolHandler {
    return func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
        // Extract token from context or request
        token := extractToken(ctx, req)
        if token == "" {
            return nil, fmt.Errorf("authentication required")
        }
        
        // Validate token
        user, err := m.tokenValidator.Validate(token)
        if err != nil {
            return nil, fmt.Errorf("invalid token: %w", err)
        }
        
        // Add user to context
        ctx = context.WithValue(ctx, "user", user)
        
        return next(ctx, req)
    }
}

Hooks

Implement lifecycle callbacks for telemetry, logging, and custom behavior.

Hook Composability with GetHooks()

Use GetHooks() to append hooks to an existing server without replacing the entire hooks configuration. This is useful for libraries or plugins that need to add hooks after construction:

go
s := server.NewMCPServer("My Server", "1.0.0",
    server.WithHooks(initialHooks),
)

// Later, add more hooks without replacing existing ones
hooks := s.GetHooks()
if hooks == nil {
    hooks = &server.Hooks{}
}
hooks.AddBeforeAny(func(ctx context.Context, id any, method mcp.MCPMethod, message any) {
    log.Printf("Request: %s", method)
})

Comprehensive Hooks

text
/* Detailed source-code truncated for AI context efficiency. */

Custom Business Logic Hooks

go
type BusinessHooks struct {
    auditLogger AuditLogger
    notifier    Notifier
}

func (h *BusinessHooks) OnToolCall(sessionID, toolName string, duration time.Duration, err error) {
    // Audit sensitive operations
    if isSensitiveTool(toolName) {
        h.auditLogger.LogToolCall(sessionID, toolName, err)
    }
    
    // Alert on errors
    if err != nil {
        h.notifier.SendAlert(fmt.Sprintf("Tool %s failed for session %s: %v", 
            toolName, sessionID, err))
    }
    
    // Monitor performance
    if duration > 30*time.Second {
        h.notifier.SendAlert(fmt.Sprintf("Slow tool execution: %s took %v", 
            toolName, duration))
    }
}

func (h *BusinessHooks) OnSessionStart(sessionID string) {
    // Initialize user-specific resources
    h.initializeUserResources(sessionID)
    
    // Send welcome notification
    h.notifier.SendWelcome(sessionID)
}

func (h *BusinessHooks) OnSessionEnd(sessionID string) {
    // Cleanup user resources
    h.cleanupUserResources(sessionID)
    
    // Log session summary
    h.auditLogger.LogSessionEnd(sessionID)
}

Tool Filtering

Conditionally expose tools based on context, permissions, or other criteria.

Permission-Based Filtering

go
type PermissionFilter struct {
    sessionManager *SessionManager
}

func NewPermissionFilter(sm *SessionManager) *PermissionFilter {
    return &PermissionFilter{sessionManager: sm}
}

func (f *PermissionFilter) FilterTools(ctx context.Context, tools []mcp.Tool) []mcp.Tool {
    sessionID := server.GetSessionID(ctx)
    session, exists := f.sessionManager.GetSession(sessionID)
    if !exists {
        return []mcp.Tool{} // No tools for invalid sessions
    }

    var filtered []mcp.Tool
    for _, tool := range tools {
        if f.hasPermissionForTool(session, tool.Name) {
            filtered = append(filtered, tool)
        }
    }
    
    return filtered
}

func (f *PermissionFilter) hasPermissionForTool(session *SessionState, toolName string) bool {
    requiredPermissions := map[string][]string{
        "delete_user":    {"admin"},
        "modify_system":  {"admin", "operator"},
        "read_data":      {"admin", "operator", "user"},
        "create_report":  {"admin", "operator", "user"},
    }
    
    required, exists := requiredPermissions[toolName]
    if !exists {
        return true // Allow tools without specific requirements
    }
    
    for _, permission := range session.Permissions {
        for _, req := range required {
            if permission == req {
                return true
            }
        }
    }
    
    return false
}

Context-Based Filtering

go
type ContextFilter struct{}

func (f *ContextFilter) FilterTools(ctx context.Context, tools []mcp.Tool) []mcp.Tool {
    timeOfDay := time.Now().Hour()
    environment := os.Getenv("ENVIRONMENT")
    
    var filtered []mcp.Tool
    for _, tool := range tools {
        if f.shouldIncludeTool(tool, timeOfDay, environment) {
            filtered = append(filtered, tool)
        }
    }
    
    return filtered
}

func (f *ContextFilter) shouldIncludeTool(tool mcp.Tool, hour int, env string) bool {
    // Maintenance tools only during off-hours
    maintenanceTools := map[string]bool{
        "backup_database":  true,
        "cleanup_logs":     true,
        "restart_service":  true,
    }
    
    if maintenanceTools[tool.Name] {
        return hour < 6 || hour > 22 // Only between 10 PM and 6 AM
    }
    
    // Debug tools only in development
    debugTools := map[string]bool{
        "debug_session": true,
        "dump_state":    true,
    }
    
    if debugTools[tool.Name] {
        return env == "development"
    }
    
    return true
}

Notifications

Send server-to-client messages for real-time updates.

Custom Notifications

go
func handleLongRunningTool(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    srv := server.ServerFromContext(ctx)
    
    // Simulate long-running work
    for i := 0; i < 100; i++ {
        time.Sleep(100 * time.Millisecond)
        
        // Send custom notification to all clients
        notification := map[string]interface{}{
            "type":     "progress",
            "progress": i + 1,
            "total":    100,
            "message":  fmt.Sprintf("Processing step %d/100", i+1),
        }
        
        srv.SendNotificationToAllClients("progress", notification)
    }
    
    return mcp.NewToolResultText("Long operation completed successfully"), nil
}

Custom Notifications

text
/* Detailed source-code truncated for AI context efficiency. */

Production Configuration

Complete Production Server

text
/* Detailed source-code truncated for AI context efficiency. */

Client Capability Based Filtering

go
package main

import (
    "context"
    "fmt"

    "github.com/mark3labs/mcp-go/mcp"
    "github.com/mark3labs/mcp-go/server"
)

func main() {
    s := server.NewMCPServer("Typed Server", "1.0.0",
        server.WithToolCapabilities(true),
    )

     s.AddTool(
        mcp.NewTool("calculate",
            mcp.WithDescription("Perform basic mathematical calculations"),
            mcp.WithString("operation",
                mcp.Required(),
                mcp.Enum("add", "subtract", "multiply", "divide"),
                mcp.Description("The operation to perform"),
            ),
            mcp.WithNumber("x", mcp.Required(), mcp.Description("First number")),
            mcp.WithNumber("y", mcp.Required(), mcp.Description("Second number")),
        ),
        handleCalculate,
    )

    server.ServeStdio(s)
}

func handleCalculate(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    session := server.ClientSessionFromContext(ctx)
	if session == nil {
		return nil, fmt.Errorf("no active session")
	}

	if clientSession, ok := session.(server.SessionWithClientInfo); ok {
             clientCapabilities := clientSession.GetClientCapabilities()
			 if clientCapabilities.Sampling == nil {
				 fmt.Println("sampling is not enabled in client")
			 }
	}

    // TODO: implement calculation logic
    return mcp.NewToolResultError("not implemented"), nil
}

Sampling (Advanced)

Sampling is an advanced feature that allows servers to request LLM completions from clients. This enables bidirectional communication where servers can leverage client-side LLM capabilities for content generation, reasoning, and question answering.

Note: Sampling is an advanced feature that most servers don't need. Only implement sampling if your server specifically needs to generate content using the client's LLM.

When to Use Sampling

Consider sampling when your server needs to:

  • Generate content based on user input
  • Answer questions using LLM reasoning
  • Perform text analysis or summarization
  • Create dynamic responses that require LLM capabilities

Basic Implementation

text
/* Detailed source-code truncated for AI context efficiency. */

Additional Content Types

AudioContent

Audio data can be embedded in prompt messages or tool results:

go
audioResult := &mcp.CallToolResult{
    Content: []mcp.Content{
        mcp.AudioContent{
            Type:     "audio",
            Data:     base64EncodedAudioData,
            MIMEType: "audio/wav",
        },
    },
}

AudioContent fields:

  • Type — must be "audio"
  • Data — base64-encoded audio data
  • MIMEType — MIME type of the audio (e.g., "audio/wav", "audio/mp3")

Tool results can include links to server resources instead of inline content:

go
result := &mcp.CallToolResult{
    Content: []mcp.Content{
        mcp.ResourceLink{
            Type:        "resource_link",
            URI:         "files://reports/q4-summary.pdf",
            Name:        "Q4 Summary Report",
            Description: "Quarterly financial summary",
            MIMEType:    "application/pdf",
        },
    },
}

Roots Capability

Enable the roots capability to request root directory information from connected clients:

go
s := server.NewMCPServer("Server", "1.0.0",
    server.WithRoots(),
)

When enabled, the server advertises roots support in its capabilities. Clients that support the SessionWithRoots interface can then provide root directory information that the server can use for file operations, project scoping, and similar use cases.

For complete sampling documentation, see Server Sampling Guide.

Next Steps


Www Docs Pages Servers Basics (`www/docs/pages/servers/basics.mdx`)

Server Basics

Learn how to create, configure, and start MCP servers with different transport options.

Creating a Server

The foundation of any MCP server is the NewMCPServer() function. This creates a server instance with basic metadata and optional configuration.

Basic Server Creation

go
package main

import (
    "github.com/mark3labs/mcp-go/server"
)

func main() {
    // Create a basic server
    s := server.NewMCPServer(
        "My MCP Server",  // Server name
        "1.0.0",          // Server version
    )
    
    // Start the server (stdio transport)
    server.ServeStdio(s)
}

Server with Options

Use server options to configure capabilities and behavior:

go
s := server.NewMCPServer(
    "Advanced Server",
    "2.0.0",
    server.WithToolCapabilities(true),      // Enable tools
    server.WithResourceCapabilities(true, true),  // Enable resources
    server.WithPromptCapabilities(true),    // Enable prompts
    server.WithRecovery(),                  // Add panic recovery
    server.WithHooks(myHooks),              // Add lifecycle hooks
    server.WithIcons(                       // Add server icons
        mcp.Icon{
            Src:      "https://example.com/server-icon.png",
            MIMEType: "image/png",
            Sizes:    []string{"48x48"},
        },
    ),
)

Server Configuration

Capabilities

Capabilities tell clients what features your server supports:

go
// Enable specific capabilities
s := server.NewMCPServer(
    "Specialized Server",
    "1.0.0",
    server.WithToolCapabilities(true),      // Can execute tools
    server.WithResourceCapabilities(true, true),  // Can provide resources
    server.WithPromptCapabilities(true),    // Can provide prompts
)

// Or enable all capabilities
s := server.NewMCPServer(
    "Full-Featured Server", 
    "1.0.0",
    server.WithToolCapabilities(true),
    server.WithResourceCapabilities(true, true),
    server.WithPromptCapabilities(true),
)

Capability types:

  • Tools: Server can execute function calls from LLMs
  • Resources: Server can provide data/content to LLMs
  • Prompts: Server can provide prompt templates
  • Tasks: Server supports asynchronous task-augmented tool execution (see Task-Augmented Tools)
  • Completions: Server provides argument auto-completion (see Completions)
  • Elicitation: Server can request additional user input (see Elicitation)
  • Roots: Server can request root directory information from clients

Recovery Middleware

Add automatic panic recovery to prevent server crashes:

go
s := server.NewMCPServer(
    "Robust Server",
    "1.0.0", 
    server.WithRecovery(), // Automatically recover from panics
)

This catches panics in handlers and returns proper error responses instead of crashing.

Custom Metadata

Add additional server information:

go
s := server.NewMCPServer(
    "My Server",
    "1.0.0",
    server.WithInstructions("A server that does amazing things"),
)

Implementation Metadata

The server's Implementation struct supports additional optional fields for richer metadata sent during initialization:

go
// These fields are set automatically from NewMCPServer args:
//   Name    string — server name
//   Version string — server version
//
// Additional optional fields on mcp.Implementation:
//   Title       string — human-readable display title
//   Description string — brief description of the server
//   WebsiteURL  string — URL for the server's website or docs
//   Icons       []Icon — visual identifiers for the implementation

You can set these via server options:

go
s := server.NewMCPServer(
    "My Server",
    "1.0.0",
    server.WithTitle("My Server"),                                    // Human-readable display title
    server.WithDescription("A server that does amazing things"),     // Brief description
    server.WithWebsiteURL("https://example.com"),                    // Website or docs URL
    server.WithIcons(                                                // Visual identifiers
        mcp.Icon{
            Src:      "https://example.com/icon.png",
            MIMEType: "image/png",
            Sizes:    []string{"48x48"},
        },
    ),
)

Extensions vs Experimental Capabilities

Servers can advertise two kinds of non-standard capabilities:

  • Extensions (ServerCapabilities.Extensions) — standardized extension points for advertising well-known extension support
  • Experimental (ServerCapabilities.Experimental) — non-standard, experimental capabilities
go
s := server.NewMCPServer("My Server", "1.0.0",
    // Set experimental capabilities
    server.WithExperimental(map[string]any{
        "claude/channel": map[string]any{
            "supported": true,
        },
    }),
)

Both fields are map[string]any and are included in the server's capabilities during initialization. The same Extensions field is available on ClientCapabilities for clients to advertise extension support.

Starting Servers

MCP-Go supports multiple transport methods for different deployment scenarios.

Stdio Transport

Standard input/output - most common for local tools:

go
func main() {
    s := server.NewMCPServer("My Server", "1.0.0")
    
    // Start stdio server (blocks until terminated)
    if err := server.ServeStdio(s); err != nil {
        log.Fatal(err)
    }
}

Best for:

  • Local development tools
  • CLI integrations
  • Desktop applications
  • Single-client scenarios

HTTP Transport

Traditional HTTP request/response:

go
func main() {
    s := server.NewMCPServer("HTTP Server", "1.0.0")
    
    // Create HTTP server
    httpServer := server.NewStreamableHTTPServer(s)
    
    // Start HTTP server on port 8080
    if err := httpServer.Start(":8080"); err != nil {
        log.Fatal(err)
    }
}

Best for:

  • Web services
  • Load-balanced deployments
  • REST-like APIs
  • Caching scenarios

Server-Sent Events (SSE)

HTTP-based streaming for real-time updates:

go
func main() {
    s := server.NewMCPServer("SSE Server", "1.0.0")
    
    // Create SSE server
    sseServer := server.NewSSEServer(s)
    
    // Start SSE server on port 8080
    if err := sseServer.Start(":8080"); err != nil {
        log.Fatal(err)
    }
}

Best for:

  • Web applications
  • Real-time notifications
  • Multiple concurrent clients
  • Browser-based tools

Custom Transport Options

Configure transport-specific options:

go
// HTTP with custom options
httpServer := server.NewStreamableHTTPServer(s,
    server.WithEndpointPath("/mcp"),
    server.WithStateLess(true),
    server.WithTLSCert("/path/to/cert.pem", "/path/to/key.pem"),
)

if err := httpServer.Start(":8080"); err != nil {
    log.Fatal(err)
}

// SSE with custom options
sseServer := server.NewSSEServer(s,
    server.WithSSEEndpoint("/events"),
    server.WithMessageEndpoint("/message"),
    server.WithKeepAlive(true),
)

if err := sseServer.Start(":8080"); err != nil {
    log.Fatal(err)
}

Environment-Based Configuration

Configure servers based on environment variables:

go
func main() {
    s := server.NewMCPServer("Configurable Server", "1.0.0")
    
    // Choose transport based on environment
    transport := os.Getenv("MCP_TRANSPORT")
    port := os.Getenv("PORT")
    if port == "" {
        port = "8080"
    }
    
    switch transport {
    case "http":
        httpServer := server.NewStreamableHTTPServer(s)
        httpServer.Start(":"+port)
    case "sse":
        sseServer := server.NewSSEServer(s)
        sseServer.Start(":"+port)
    default:
        server.ServeStdio(s)
    }
}
}

Server Lifecycle

Understanding the server lifecycle helps with proper resource management:

go
func main() {
    hooks := &server.Hooks{}
    
    // Add session lifecycle hooks
    hooks.AddOnRegisterSession(func(ctx context.Context, session server.ClientSession) {
        log.Printf("Client %s connected", session.ID())
    })
    
    hooks.AddOnUnregisterSession(func(ctx context.Context, session server.ClientSession) {
        log.Printf("Client %s disconnected", session.ID())
    })
    
    // Add request hooks
    hooks.AddBeforeAny(func(ctx context.Context, id any, method mcp.MCPMethod, message any) {
        log.Printf("Processing %s request", method)
    })
    
    hooks.AddOnError(func(ctx context.Context, id any, method mcp.MCPMethod, message any, err error) {
        log.Printf("Error in %s: %v", method, err)
    })
    
    s := server.NewMCPServer("Lifecycle Server", "1.0.0",
        server.WithHooks(hooks),
    )
    
    // Graceful shutdown
    c := make(chan os.Signal, 1)
    signal.Notify(c, os.Interrupt, syscall.SIGTERM)
    
    go func() {
        <-c
        log.Println("Shutting down server...")
        s.Shutdown()
    }()
    
    server.ServeStdio(s)
}

Error Handling

Proper error handling ensures robust server operation:

go
func main() {
    s := server.NewMCPServer("Error-Safe Server", "1.0.0",
        server.WithRecovery(), // Panic recovery
    )
    
    // Add error handling for server startup
    if err := server.ServeStdio(s); err != nil {
        if errors.Is(err, server.ErrServerClosed) {
            log.Println("Server closed gracefully")
        } else {
            log.Fatalf("Server error: %v", err)
        }
    }
}

Next Steps

Now that you understand server basics, learn how to add functionality:


Www Docs Pages Servers Completions (`www/docs/pages/servers/completions.mdx`)

Auto-Completion

The completion/complete MCP method enables servers to provide argument completions for prompts and resource templates. This helps clients offer autocomplete suggestions when users are filling in prompt arguments or resource template parameters.

Enabling Completions

To support completions, enable the capability when creating your server:

go
s := server.NewMCPServer("Completion Server", "1.0.0",
    server.WithPromptCapabilities(true),
    server.WithResourceCapabilities(true, true),
    server.WithCompletions(), // Enable the completion capability
)

Once enabled, clients can send completion/complete requests and receive suggested values for prompt arguments and resource template parameters.

Prompt Completion Provider

Implement the PromptCompletionProvider interface to return completions for prompt arguments:

go
type PromptCompletionProvider interface {
    CompletePromptArgument(ctx context.Context, promptName string, argument mcp.CompleteArgument, context mcp.CompleteContext) (*mcp.Completion, error)
}

Example Implementation

go
type MyPromptCompletionProvider struct {
    prompts map[string][]string // prompt name -> known argument values
}

func (p *MyPromptCompletionProvider) CompletePromptArgument(
    ctx context.Context,
    promptName string,
    argument mcp.CompleteArgument,
    compCtx mcp.CompleteContext,
) (*mcp.Completion, error) {
    // Return completions based on the prompt name and argument
    switch promptName {
    case "code_review":
        if argument.Name == "language" {
            languages := []string{"go", "python", "javascript", "rust", "java"}
            // Filter by what the user has typed so far
            var matches []string
            for _, lang := range languages {
                if strings.HasPrefix(lang, strings.ToLower(argument.Value)) {
                    matches = append(matches, lang)
                }
            }
            return &mcp.Completion{
                Values:  matches,
                HasMore: false,
            }, nil
        }
    }
    return &mcp.Completion{Values: []string{}}, nil
}

Registering the Provider

Pass your provider as a server option:

go
s := server.NewMCPServer("Server", "1.0.0",
    server.WithCompletions(),
    server.WithPromptCompletionProvider(&MyPromptCompletionProvider{}),
)

Resource Completion Provider

Implement the ResourceCompletionProvider interface to return completions for resource URI template parameters:

go
type ResourceCompletionProvider interface {
    CompleteResourceArgument(ctx context.Context, uri string, argument mcp.CompleteArgument, context mcp.CompleteContext) (*mcp.Completion, error)
}

Example Implementation

go
type MyResourceCompletionProvider struct{}

func (p *MyResourceCompletionProvider) CompleteResourceArgument(
    ctx context.Context,
    uri string,
    argument mcp.CompleteArgument,
    compCtx mcp.CompleteContext,
) (*mcp.Completion, error) {
    // Provide completions for resource URI template parameters
    if strings.HasPrefix(uri, "db://") && argument.Name == "table" {
        tables := []string{"users", "orders", "products", "inventory"}
        var matches []string
        for _, t := range tables {
            if strings.HasPrefix(t, argument.Value) {
                matches = append(matches, t)
            }
        }
        return &mcp.Completion{
            Values:  matches,
            HasMore: false,
        }, nil
    }
    return &mcp.Completion{Values: []string{}}, nil
}

Registering the Provider

go
s := server.NewMCPServer("Server", "1.0.0",
    server.WithCompletions(),
    server.WithResourceCompletionProvider(&MyResourceCompletionProvider{}),
)

Default Providers

If you don't set custom providers, DefaultPromptCompletionProvider and DefaultResourceCompletionProvider are used automatically. These defaults return empty completions for every request, so the server will always respond to completion/complete without errors — it simply won't suggest anything.

You only need to register a custom provider when you want to return actual suggestions.

The Completion Type

The mcp.Completion struct is what your provider returns to the client:

go
type Completion struct {
    // An array of completion values. Must not exceed 100 items.
    Values []string `json:"values"`
    // The total number of completion options available. This can exceed the
    // number of values actually sent in the response.
    Total int `json:"total,omitempty"`
    // Indicates whether there are additional completion options beyond those
    // provided in the current response, even if the exact total is unknown.
    HasMore bool `json:"hasMore,omitempty"`
}
  • Values — the suggested completions, filtered by what the user has typed so far. Keep this list at 100 items or fewer.
  • Total — the full count of matching options when there are more results than you return. Omit when all matches fit in Values.
  • HasMore — set to true when additional completions exist beyond the current response.

Supporting Types

The request carries two small structs your provider receives:

go
// CompleteArgument is the argument the client is asking about.
type CompleteArgument struct {
    Name  string `json:"name"`  // Argument name
    Value string `json:"value"` // What the user has typed so far
}

// CompleteContext holds values for arguments that are already resolved.
type CompleteContext struct {
    Arguments map[string]string `json:"arguments"`
}

Use CompleteContext when the completion for one argument depends on the value of another — for example, suggesting column names only after a table has been selected.

Next Steps

  • Prompts - Learn to create reusable interaction templates
  • Resources - Learn about exposing data sources
  • Tools - Learn to implement interactive functionality
  • Advanced Features - Explore hooks, middleware, and more

Www Docs Pages Servers Elicitation (`www/docs/pages/servers/elicitation.mdx`)

Elicitation

Learn how to implement MCP servers that can request additional information from users during interactions.

Overview

Elicitation allows servers to request additional information from users during an interaction. This is useful when a tool or operation needs clarification, confirmation, or extra input from the user that wasn't provided in the original request.

There are two modes of elicitation:

  • Form mode (mcp.ElicitationModeForm) — the server provides a JSON Schema and the client presents a structured form to the user. This is the default mode.
  • URL mode (mcp.ElicitationModeURL) — the server provides a URL for the user to visit, enabling out-of-band interactions like authentication flows.

:::info[User Consent Required]
Elicitation is inherently user-facing. When your server sends an elicitation request:

  • The client will present the request to the user
  • The user may accept, decline, or cancel
  • Response times depend on user interaction

Design your tools accordingly:

  • Provide clear, human-readable messages explaining what you need and why
  • Handle all three response actions (accept, decline, cancel) gracefully
  • Consider timeouts for user interaction delays
    :::

Enabling Elicitation

To enable elicitation in your server, use the WithElicitation() option during server creation:

go
package main

import (
    "context"
    "github.com/mark3labs/mcp-go/mcp"
    "github.com/mark3labs/mcp-go/server"
)

func main() {
    // Create server with elicitation capability
    s := server.NewMCPServer("Elicitation Server", "1.0.0",
        server.WithElicitation(),
    )

    // Add tools that use elicitation...

    // Start server
    server.ServeStdio(s)
}

ElicitationParams

The ElicitationParams structure defines what the server is requesting from the user:

go
type ElicitationParams struct {
    Meta            *Meta  `json:"_meta,omitempty"`
    Mode            string `json:"mode,omitempty"`            // "form" or "url", defaults to "form"
    Message         string `json:"message"`                   // Human-readable message

    // Form mode fields
    RequestedSchema any    `json:"requestedSchema,omitempty"` // JSON Schema for the form

    // URL mode fields
    ElicitationID   string `json:"elicitationId,omitempty"`   // Unique ID for tracking
    URL             string `json:"url,omitempty"`             // URL for the user to visit
}

Validation

ElicitationParams has a Validate() method that enforces mode-specific requirements:

  • Form mode requires RequestedSchema to be non-nil
  • URL mode requires both ElicitationID and URL to be set

Validation is called automatically when you use RequestElicitation().

Response Actions

When a user responds to an elicitation request, the result includes one of three actions:

go
mcp.ElicitationResponseActionAccept   // User provided the requested information
mcp.ElicitationResponseActionDecline  // User explicitly declined
mcp.ElicitationResponseActionCancel   // User cancelled without making a choice

The result is returned as an ElicitationResult:

go
type ElicitationResult struct {
    Result
    ElicitationResponse
}

type ElicitationResponse struct {
    Action  ElicitationResponseAction `json:"action"`
    Content any                       `json:"content,omitempty"` // User's response data
}

When Action is accept, the Content field contains the user's response data, which should conform to the RequestedSchema from the original request.

Form Mode

Form mode is the default. The server provides a JSON Schema describing the expected input, and the client renders a form for the user.

Requesting Elicitation in a Tool Handler

Use RequestElicitation() within tool handlers to ask the user for information:

text
/* Detailed source-code truncated for AI context efficiency. */

Conditional Elicitation

You can request elicitation only when needed — for example, to confirm a potentially destructive operation:

text
/* Detailed source-code truncated for AI context efficiency. */

URL Mode

URL mode is used for out-of-band interactions where the user needs to visit an external URL — for example, an OAuth flow or an API key setup page.

Requesting URL Elicitation

Use RequestURLElicitation() for a convenient way to send URL mode requests:

go
mcpServer.AddTool(
    mcp.NewTool(
        "auth_via_url",
        mcp.WithDescription("Authenticate via browser"),
    ),
    func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
        session := server.ClientSessionFromContext(ctx)
        if session == nil {
            return nil, fmt.Errorf("no active session")
        }

        // Generate a unique elicitation ID for tracking
        elicitationID := uuid.New().String()
        url := fmt.Sprintf("https://myserver.com/auth?id=%s", elicitationID)

        // Send the URL elicitation request
        result, err := mcpServer.RequestURLElicitation(
            ctx,
            session,
            elicitationID,
            url,
            "Please authenticate in your browser to continue.",
        )
        if err != nil {
            return nil, fmt.Errorf("URL elicitation failed: %w", err)
        }

        if result.Action == mcp.ElicitationResponseActionAccept {
            return &mcp.CallToolResult{
                Content: []mcp.Content{
                    mcp.NewTextContent("Authentication flow initiated. Complete the process in your browser."),
                },
            }, nil
        }

        return &mcp.CallToolResult{
            Content: []mcp.Content{
                mcp.NewTextContent(fmt.Sprintf("User declined authentication: %s", result.Action)),
            },
        }, nil
    },
)

Completion Notification

For URL mode, once the user completes the out-of-band flow (e.g., submits a form in their browser and the server receives a callback), notify the client that the elicitation is complete:

go
// Send after the server receives the callback from the browser
err := mcpServer.SendElicitationComplete(ctx, session, elicitationID)
if err != nil {
    log.Printf("Failed to send completion notification: %v", err)
}

You can also construct the notification directly:

go
notification := mcp.NewElicitationCompleteNotification("elicitation-id-123")

URLElicitationRequiredError

When a tool requires authorization that hasn't been set up, you can return a URLElicitationRequiredError to signal that the client should initiate a URL elicitation flow:

go
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    if !isAuthorized(ctx) {
        elicitationID := uuid.New().String()
        return nil, mcp.URLElicitationRequiredError{
            Elicitations: []mcp.ElicitationParams{
                {
                    Mode:          mcp.ElicitationModeURL,
                    ElicitationID: elicitationID,
                    URL:           fmt.Sprintf("https://myserver.com/authorize?id=%s", elicitationID),
                    Message:       "Authorization is required to access this resource.",
                },
            },
        }
    }

    return mcp.NewToolResultText("Action completed successfully!"), nil
}

Client-Side Handler

Clients implement the ElicitationHandler interface to handle elicitation requests from servers:

go
type ElicitationHandler interface {
    Elicit(ctx context.Context, request mcp.ElicitationRequest) (*mcp.ElicitationResult, error)
}

Register the handler when creating the client:

go
import "github.com/mark3labs/mcp-go/client"

handler := &MyElicitationHandler{}
c := client.NewClient(transport, client.WithElicitationHandler(handler))

The implementation should:

  1. Present the request message to the user (and the URL if in URL mode)
  2. Validate input against the requested schema (for form mode)
  3. Allow the user to accept, decline, or cancel
  4. Return the appropriate ElicitationResult

ElicitationCapability

The ElicitationCapability struct advertises which elicitation modes a client or server supports during initialization:

go
type ElicitationCapability struct {
    Form *struct{} `json:"form,omitempty"` // Supports form mode
    URL  *struct{} `json:"url,omitempty"`  // Supports URL mode
}

This capability is exchanged as part of the ClientCapabilities and ServerCapabilities during the MCP initialization handshake.

Error Handling

Always handle elicitation errors and all response actions gracefully:

go
result, err := mcpServer.RequestElicitation(ctx, elicitationRequest)
if err != nil {
    // Common errors:
    // - server.ErrNoActiveSession: no session in context
    // - server.ErrElicitationNotSupported: session/transport doesn't support elicitation
    // - validation errors from ElicitationParams.Validate()
    log.Printf("Elicitation failed: %v", err)
    return mcp.NewToolResultError(fmt.Sprintf("Could not request information: %v", err)), nil
}

Context and Timeouts

Use context for timeout control, especially since elicitation requires user interaction:

go
ctx, cancel := context.WithTimeout(ctx, 2*time.Minute)
defer cancel()

result, err := mcpServer.RequestElicitation(ctx, elicitationRequest)

Best Practices

  1. Clear Messages: Write human-readable messages that explain what you need and why
  2. Minimal Schemas: Only request the information you actually need
  3. Handle All Actions: Always handle accept, decline, and cancel — don't assume the user will accept
  4. Validate Responses: Type-assert and validate the Content field before using it
  5. Graceful Degradation: If elicitation fails or is unsupported, provide a sensible fallback
  6. Use URL Mode Sparingly: Reserve URL mode for flows that genuinely require a browser (e.g., OAuth)
  7. Track Elicitation IDs: For URL mode, use unique IDs to correlate browser callbacks with sessions

Next Steps


Www Docs Pages Servers Middleware (`www/docs/pages/servers/middleware.mdx`)

Middleware

Middleware lets you wrap tool handlers with cross-cutting logic such as logging, authentication, and rate limiting. Middlewares execute in registration order (first registered runs first) and form a chain around the underlying tool handler.

Overview

A ToolHandlerMiddleware is a function that takes a ToolHandlerFunc and returns a new ToolHandlerFunc. This pattern is identical to HTTP middleware in Go's standard library.

go
type ToolHandlerFunc func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error)

type ToolHandlerMiddleware func(ToolHandlerFunc) ToolHandlerFunc

Registering Middleware

Using `Use()` (Runtime)

The Use() method registers one or more tool handler middlewares on a running server. Middlewares can be added at any time — Use() is concurrency-safe.

go
s := server.NewMCPServer("my-server", "1.0.0")

// Register a single middleware
s.Use(loggingMiddleware)

// Register multiple middlewares at once
s.Use(authMiddleware, rateLimitMiddleware)

Middlewares registered via Use() execute in the order they were added. In the example above, a tool call passes through loggingMiddleware → authMiddleware → rateLimitMiddleware → tool handler.

Using Server Options (Construction Time)

You can also register middleware when constructing the server:

go
s := server.NewMCPServer("my-server", "1.0.0",
    server.WithToolHandlerMiddleware(loggingMiddleware),
    server.WithResourceHandlerMiddleware(resourceLogger),
    server.WithPromptHandlerMiddleware(promptLogger),
)

WithToolHandlerMiddleware, WithResourceHandlerMiddleware, and WithPromptHandlerMiddleware register middleware for their respective handler types at construction time.

Writing Middleware

Simple Middleware

A basic middleware that logs every tool call:

go
import (
    "context"
    "log"

    "github.com/mark3labs/mcp-go/mcp"
    "github.com/mark3labs/mcp-go/server"
)

func loggingMiddleware(next server.ToolHandlerFunc) server.ToolHandlerFunc {
    return func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
        log.Printf("tool call: %s", req.Params.Name)
        return next(ctx, req)
    }
}

s := server.NewMCPServer("my-server", "1.0.0")
s.Use(loggingMiddleware)

Timing Middleware

Measure how long each tool call takes:

go
func timingMiddleware(next server.ToolHandlerFunc) server.ToolHandlerFunc {
    return func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
        start := time.Now()
        result, err := next(ctx, req)
        log.Printf("tool=%s duration=%v", req.Params.Name, time.Since(start))
        return result, err
    }
}

Authentication Middleware

Reject unauthenticated requests before they reach the tool handler. You need to provide your own extractToken, validateToken, and userKey implementations:

go
// userKey is a custom context key for storing the authenticated user.
type contextKey string
const userKey contextKey = "user"

func authMiddleware(next server.ToolHandlerFunc) server.ToolHandlerFunc {
    return func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
        token := extractToken(ctx) // your token extraction logic
        if token == "" {
            return nil, fmt.Errorf("authentication required")
        }

        user, err := validateToken(token) // your token validation logic
        if err != nil {
            return nil, fmt.Errorf("invalid token: %w", err)
        }

        ctx = context.WithValue(ctx, userKey, user)
        return next(ctx, req)
    }
}

Rate-Limiting Middleware

Limit the number of tool calls per session. This example uses golang.org/x/time/rate and server.GetSessionID() to identify the caller:

go
type rateLimiter struct {
    limiters sync.Map
    rate     rate.Limit
    burst    int
}

func newRateLimiter(rps float64, burst int) *rateLimiter {
    return &rateLimiter{rate: rate.Limit(rps), burst: burst}
}

func (r *rateLimiter) middleware(next server.ToolHandlerFunc) server.ToolHandlerFunc {
    return func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
        sessionID := server.GetSessionID(ctx)
        limiter, _ := r.limiters.LoadOrStore(sessionID, rate.NewLimiter(r.rate, r.burst))
        if !limiter.(*rate.Limiter).Allow() {
            return nil, fmt.Errorf("rate limit exceeded")
        }
        return next(ctx, req)
    }
}

// Usage
rl := newRateLimiter(10, 20)
s.Use(rl.middleware)

Execution Order

Middlewares execute in registration order — the first middleware registered is the outermost wrapper and runs first:

go
s.Use(A, B)
s.Use(C)

// Call order: A → B → C → tool handler → C → B → A

This matches the convention used by net/http middleware in Go.

Task-Augmented Tools

Middleware registered via Use() also applies to regular tools executed via the task path (when a tool has TaskSupportOptional or TaskSupportPreferred and the client requests task execution). The same middleware chain wraps the regular tool handler automatically.

Note: Native task tools (those registered with a TaskToolHandlerFunc) do not have Use() middleware applied. If you need cross-cutting logic for native task tools, implement it directly in your task handler.

go
tool := mcp.NewTool("long-running",
    mcp.WithDescription("A tool that supports task mode"),
    mcp.WithTaskSupport(mcp.TaskSupportOptional),
)
s.AddTool(tool, handler)

// This middleware applies to regular calls and when this regular tool is executed via the task path
s.Use(loggingMiddleware)

Built-in Middleware

MCP-Go includes built-in recovery middleware that catches panics in tool handlers and returns an error response instead of crashing the server:

go
s := server.NewMCPServer("my-server", "1.0.0",
    server.WithRecovery(),
)

Concurrency Safety

Use() is safe to call from multiple goroutines concurrently. Internally, it uses a read-write mutex to protect the middleware slice. This means you can dynamically add middleware at runtime without stopping the server.

Next Steps


Www Docs Pages Servers Prompts (`www/docs/pages/servers/prompts.mdx`)

Implementing Prompts

Prompts are reusable interaction templates that help structure conversations between users and LLMs. They provide context, instructions, and can include dynamic content from resources.

Prompt Fundamentals

Prompts in MCP serve as templates that can be invoked by LLMs to generate structured interactions. They're particularly useful for complex workflows, analysis tasks, or any scenario where you want to provide consistent context and instructions.

Basic Prompt Structure

go
// Create a simple prompt
prompt := mcp.NewPrompt("code_review",
    mcp.WithPromptDescription("Review code for best practices and issues"),
    mcp.WithPromptArgument("code",
        mcp.Required(),
        mcp.Description("The code to review"),
    ),
    mcp.WithPromptArgument("language",
        mcp.Description("Programming language"),
        mcp.Default("auto-detect"),
    ),
)

Prompt Icons

Prompts can include visual identifiers:

go
prompt := mcp.NewPrompt("code_review",
    mcp.WithPromptDescription("Review code for best practices"),
    mcp.WithPromptIcons(
        mcp.Icon{
            Src:      "https://example.com/icons/review.png",
            MIMEType: "image/png",
        },
    ),
    // ... arguments
)

Display Title

Prompts and their arguments can carry a human-readable display title. Clients render the title in pickers and UI surfaces; the name stays a stable programmatic identifier.

go
prompt := mcp.NewPrompt("code_review",
    mcp.WithPromptTitle("Code Review"),                            // Shown in UI
    mcp.WithPromptDescription("Review code for best practices"),
    mcp.WithArgument("code",
        mcp.ArgumentTitle("Source Code"),                          // Argument display name
        mcp.ArgumentDescription("The code to review"),
        mcp.RequiredArgument(),
    ),
)

When Title is empty, clients fall back to Name. Both fields serialise with omitempty, so existing prompts that don't set a title are unchanged on the wire.

Prompt Templates

Basic Code Review Prompt

text
/* Detailed source-code truncated for AI context efficiency. */

Data Analysis Prompt

text
/* Detailed source-code truncated for AI context efficiency. */

Prompt Arguments

Flexible Parameter Handling

text
/* Detailed source-code truncated for AI context efficiency. */

Message Types

Multi-Message Conversations

text
/* Detailed source-code truncated for AI context efficiency. */

System and User Roles

text
/* Detailed source-code truncated for AI context efficiency. */

Embedded Resources

Including Resource Data

text
/* Detailed source-code truncated for AI context efficiency. */

Dynamic Resource Integration

text
/* Detailed source-code truncated for AI context efficiency. */

Advanced Prompt Patterns

Conditional Prompts

text
/* Detailed source-code truncated for AI context efficiency. */

Template-Based Prompts

text
/* Detailed source-code truncated for AI context efficiency. */

Prompt Filtering

Filter which prompts are visible to each client based on context, session, or permissions — analogous to tool filtering via WithToolFilter.

Permission-Based Prompt Filtering

go
s := server.NewMCPServer("Prompt Server", "1.0.0",
    server.WithPromptCapabilities(true),
    server.WithPromptFilter(func(ctx context.Context, prompts []mcp.Prompt) []mcp.Prompt {
        session := server.ClientSessionFromContext(ctx)
        if session == nil {
            return nil
        }

        // Filter prompts based on session or context
        var filtered []mcp.Prompt
        for _, p := range prompts {
            if shouldExposePrompt(ctx, p) {
                filtered = append(filtered, p)
            }
        }
        return filtered
    }),
)

Multiple filters can be added and they are applied in order:

go
s := server.NewMCPServer("Server", "1.0.0",
    server.WithPromptCapabilities(true),
    server.WithPromptFilter(roleFilter),
    server.WithPromptFilter(environmentFilter),
)

Prompt Handler Middleware

Wrap prompt handlers with cross-cutting concerns like logging, caching, or authentication — analogous to WithToolHandlerMiddleware.

go
s := server.NewMCPServer("Server", "1.0.0",
    server.WithPromptCapabilities(true),
    server.WithPromptHandlerMiddleware(func(next server.PromptHandlerFunc) server.PromptHandlerFunc {
        return func(ctx context.Context, req mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
            start := time.Now()
            log.Printf("Prompt requested: %s", req.Params.Name)

            result, err := next(ctx, req)

            log.Printf("Prompt %s served in %v", req.Params.Name, time.Since(start))
            return result, err
        }
    }),
)

Middlewares are composed in order — the first registered wraps the outermost layer:

go
s := server.NewMCPServer("Server", "1.0.0",
    server.WithPromptCapabilities(true),
    server.WithPromptHandlerMiddleware(loggingMiddleware),
    server.WithPromptHandlerMiddleware(authMiddleware),
)

Next Steps


Www Docs Pages Servers Resources (`www/docs/pages/servers/resources.mdx`)

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

go
// 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:

go
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.

go
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:

go
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:

go
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:

go
// 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:

go
// 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:

text
/* Detailed source-code truncated for AI context efficiency. */

API Resources

Proxy external APIs through resources:

go
// 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

go
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

go
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

go
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:

go
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

go
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

go
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:

go
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:

go
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:

go
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:

go
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:

go
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:

go
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.

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

text
/* Detailed source-code truncated for AI context efficiency. */

Direct Interface Usage

You can also work directly with the SessionWithResources interface:

go
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

Www Docs Pages Servers Tasks (`www/docs/pages/servers/tasks.mdx`)

Task-Augmented Tools

Task-augmented tools allow long-running operations to execute asynchronously. Instead of blocking until completion, the server creates a task and the client can poll for status updates. This is ideal for operations like batch processing, data analysis, or any work that may take more than a few seconds.

Overview

With regular tools, the handler runs synchronously — the client waits for the full result before continuing. Task-augmented tools change this by returning a CreateTaskResult immediately, while the actual work continues in the background. The client can then:

  • Poll for the task's current status
  • List all active tasks
  • Cancel a running task

Task Support Modes

Every tool declares how it interacts with the task system via mcp.TaskSupport. There are three modes:

Mode Constant Behavior
Forbidden mcp.TaskSupportForbidden Default. The tool cannot be invoked as a task. It runs synchronously like a regular tool.
Optional mcp.TaskSupportOptional The tool can be invoked as a task or run synchronously depending on the client's request.
Required mcp.TaskSupportRequired The tool must be invoked as a task. Synchronous calls are rejected with an error.
go
// Set task support when creating a tool
tool := mcp.NewTool("long_process",
    mcp.WithDescription("A long-running operation"),
    mcp.WithTaskSupport(mcp.TaskSupportRequired),
)

Enabling Task Capabilities

To use task-augmented tools, enable both tool and task capabilities on the server. WithTaskCapabilities takes three booleans controlling which task operations are available:

  1. list — allow clients to list tasks
  2. cancel — allow clients to cancel running tasks
  3. toolCallTasks — enable task augmentation for tool calls
go
s := server.NewMCPServer("Task Server", "1.0.0",
    server.WithToolCapabilities(true),
    server.WithTaskCapabilities(true, true, true), // list, cancel, toolCallTasks
)

Registering Task Tools

AddTaskTool

Use AddTaskTool to register a single task-augmented tool with its handler:

go
tool := mcp.NewTool("long_process",
    mcp.WithDescription("Run a long process asynchronously"),
    mcp.WithTaskSupport(mcp.TaskSupportRequired),
    mcp.WithString("input", mcp.Required(), mcp.Description("Input data")),
)

s.AddTaskTool(tool, func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CreateTaskResult, error) {
    input := req.GetString("input", "")

    // Do the actual work here — this runs in the background.
    // Check ctx.Done() to support cancellation.
    for i := 0; i < 100; i++ {
        select {
        case <-ctx.Done():
            return nil, ctx.Err()
        default:
            // simulate work
            time.Sleep(100 * time.Millisecond)
        }
    }

    // Return result — the server manages the Task lifecycle
    return &mcp.CreateTaskResult{
        Task: mcp.NewTask("unique-task-id",
            mcp.WithTaskStatusMessage("Processing started"),
            mcp.WithTaskPollInterval(2000), // suggest polling every 2s
            mcp.WithTaskTTL(300000),        // task expires after 5 minutes
        ),
    }, nil
})

AddTaskTools

Register multiple task tools at once with AddTaskTools:

go
s.AddTaskTools(
    server.ServerTaskTool{Tool: tool1, Handler: handler1},
    server.ServerTaskTool{Tool: tool2, Handler: handler2},
)

Handler Signature

Task tool handlers use a dedicated function type that returns *mcp.CreateTaskResult instead of the regular *mcp.CallToolResult:

go
type TaskToolHandlerFunc func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CreateTaskResult, error)

The handler runs asynchronously in the background. The server takes care of creating the task entry, tracking status, and sending notifications to the client.

Task Lifecycle

A task moves through a well-defined set of statuses:

text
Working ──→ Completed
       ──→ Failed
       ──→ Cancelled
       ──→ InputRequired ──→ Working (resumed)

The available status constants are:

Status Constant Description
Working mcp.TaskStatusWorking Task is actively processing
Completed mcp.TaskStatusCompleted Task finished successfully
Failed mcp.TaskStatusFailed Task encountered an error
Cancelled mcp.TaskStatusCancelled Task was cancelled by the client
Input Required mcp.TaskStatusInputRequired Task needs additional input to continue

Use IsTerminal() to check whether a task has reached a final state:

go
task := mcp.NewTask("my-task")

// Later, check if the task is done
if task.Status.IsTerminal() {
    // task is Completed, Failed, or Cancelled
}

Task Options

When creating a task with mcp.NewTask, you can configure it with functional options:

go
task := mcp.NewTask("task-123",
    mcp.WithTaskStatus(mcp.TaskStatusWorking),       // set initial status (default: Working)
    mcp.WithTaskStatusMessage("Initializing..."),     // human-readable status message
    mcp.WithTaskTTL(300000),                          // TTL in milliseconds (5 minutes)
    mcp.WithTaskPollInterval(2000),                   // suggested poll interval in ms
    mcp.WithTaskCreatedAt("2025-01-15T10:30:00Z"),    // custom creation timestamp
)
Option Description
WithTaskStatus Set the task's initial status. Defaults to TaskStatusWorking.
WithTaskStatusMessage Attach a human-readable message describing the current state.
WithTaskTTL Time-to-live in milliseconds. After this duration, the task may be deleted.
WithTaskPollInterval Suggested polling interval in milliseconds for the client.
WithTaskCreatedAt Override the creation timestamp (defaults to time.Now()).

Model Immediate Response

Servers can provide an immediate response to the model while the task continues executing in the background. This lets the model acknowledge the request without waiting for the full result:

go
s.AddTaskTool(tool, func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CreateTaskResult, error) {
    // ... start processing ...

    task := mcp.NewTask("task-456",
        mcp.WithTaskStatusMessage("Processing started"),
        mcp.WithTaskPollInterval(5000),
    )

    return &mcp.CreateTaskResult{
        Task: task,
        Result: mcp.Result{
            Meta: mcp.WithModelImmediateResponse("Processing your request. This may take a few minutes."),
        },
    }, nil
})

The WithModelImmediateResponse helper sets the io.modelcontextprotocol/model-immediate-response metadata key, providing a string that the client can pass directly to the model as an interim result.

You can associate messages or results with a specific task using WithRelatedTask. This is useful when sending follow-up data that references an existing task:

go
meta := mcp.WithRelatedTask("task-id-123")

The metadata contains the io.modelcontextprotocol/related-task key with the task ID, enabling clients to correlate responses with their originating tasks.

Task Hooks

Task hooks provide observability into the task lifecycle. Use server.TaskHooks to register callbacks for creation, completion, failure, cancellation, and any status change:

go
taskHooks := &server.TaskHooks{}

taskHooks.AddOnTaskCreated(func(ctx context.Context, metrics server.TaskMetrics) {
    log.Printf("Task %s created for tool %s", metrics.TaskID, metrics.ToolName)
})

taskHooks.AddOnTaskCompleted(func(ctx context.Context, metrics server.TaskMetrics) {
    log.Printf("Task %s completed in %v", metrics.TaskID, metrics.Duration)
})

taskHooks.AddOnTaskFailed(func(ctx context.Context, metrics server.TaskMetrics) {
    log.Printf("Task %s failed: %v", metrics.TaskID, metrics.Error)
})

taskHooks.AddOnTaskCancelled(func(ctx context.Context, metrics server.TaskMetrics) {
    log.Printf("Task %s was cancelled", metrics.TaskID)
})

taskHooks.AddOnTaskStatusChanged(func(ctx context.Context, metrics server.TaskMetrics) {
    log.Printf("Task %s status: %s", metrics.TaskID, metrics.Status)
})

Pass the hooks to the server with WithTaskHooks:

go
s := server.NewMCPServer("Task Server", "1.0.0",
    server.WithToolCapabilities(true),
    server.WithTaskCapabilities(true, true, true),
    server.WithTaskHooks(taskHooks),
)

TaskMetrics Fields

The server.TaskMetrics struct provides rich context for each hook:

Field Type Description
TaskID string Unique identifier for the task
ToolName string Name of the tool that created the task
Status mcp.TaskStatus Current status of the task
StatusMessage string Optional status message
CreatedAt time.Time When the task was created
CompletedAt *time.Time When the task completed (nil if still running)
Duration time.Duration How long the task has run (zero if not completed)
SessionID string Session that owns this task
Error error Error if task failed (nil otherwise)

Concurrency Limits

Use WithMaxConcurrentTasks to limit how many tasks can run simultaneously. When the limit is reached, new task requests are rejected with an error:

go
s := server.NewMCPServer("Task Server", "1.0.0",
    server.WithToolCapabilities(true),
    server.WithTaskCapabilities(true, true, true),
    server.WithMaxConcurrentTasks(10),
)

Task Status Notifications

The server automatically sends notifications/tasks/status to connected clients whenever a task's status changes. This means clients don't have to rely solely on polling — they can also listen for push notifications to react to status transitions in real time.

Complete Example

Here's a full example combining all the concepts — a server with a required task tool, an optional task tool, and observability hooks:

text
/* Detailed source-code truncated for AI context efficiency. */

Next Steps

  • Tools — Learn about regular synchronous tools
  • Advanced Features — Explore middleware, hooks, and typed handlers

Www Docs Pages Servers Tools (`www/docs/pages/servers/tools.mdx`)

Implementing Tools

Tools provide functionality that LLMs can invoke to take actions or perform computations. Think of them as function calls that extend the LLM's capabilities.

Tool Fundamentals

Tools are the primary way LLMs interact with your server to perform actions. They have structured schemas that define parameters, types, and constraints, ensuring type-safe interactions.

Basic Tool Structure

go
// Create a simple tool
tool := mcp.NewTool("calculate",
    mcp.WithDescription("Perform arithmetic operations"),
    mcp.WithString("operation", 
        mcp.Required(),
        mcp.Enum("add", "subtract", "multiply", "divide"),
        mcp.Description("The arithmetic operation to perform"),
    ),
    mcp.WithNumber("x", mcp.Required(), mcp.Description("First number")),
    mcp.WithNumber("y", mcp.Required(), mcp.Description("Second number")),
)

Tool Definition

Display Title

Tools can carry a human-readable display title separate from their programmatic name. Clients display titles in tool pickers and UI surfaces; the name is reserved for model-facing identifiers.

go
tool := mcp.NewTool("calculate_arithmetic",
    mcp.WithToolTitle("Calculator"),               // Shown in UI
    mcp.WithDescription("Perform arithmetic operations"),
    // ... parameters
)

Per the MCP spec, clients should resolve the display label in this order:

  1. Title (the top-level field set by WithToolTitle)
  2. Annotations.Title (legacy hint set by WithTitleAnnotation, see Tool Annotations)
  3. Name

Both fields are optional; omitting them falls back to the next candidate.

Tool Icons

Tools can include visual identifiers using icons. Icons must use HTTPS or data URIs:

go
tool := mcp.NewTool("calculate",
    mcp.WithDescription("Perform arithmetic operations"),
    mcp.WithToolIcons(
        mcp.Icon{
            Src:      "https://example.com/icons/calculator.png",
            MIMEType: "image/png",
            Sizes:    []string{"32x32", "64x64"},
        },
    ),
    // ... parameters
)

Parameter Types

MCP-Go supports various parameter types with validation:

go
// String parameters
mcp.WithString("name", 
    mcp.Required(),
    mcp.Description("User's name"),
    mcp.MinLength(1),
    mcp.MaxLength(100),
)

// Number parameters  
mcp.WithNumber("age",
    mcp.Required(),
    mcp.Description("User's age"),
    mcp.Min(0),
    mcp.Max(150),
)

// Integer parameters
mcp.WithInteger("count",
    mcp.DefaultNumber(10),
    mcp.Description("Number of items"),
    mcp.Min(1),
    mcp.Max(1000),
)

// Boolean parameters
mcp.WithBoolean("enabled",
    mcp.DefaultBool(true),
    mcp.Description("Whether feature is enabled"),
)

// Array parameters
mcp.WithArray("tags",
    mcp.Description("List of tags"),
    mcp.Items(map[string]any{"type": "string"}),
)

// Object parameters
mcp.WithObject("config",
    mcp.Description("Configuration object"),
    mcp.Properties(map[string]any{
        "timeout": map[string]any{"type": "number"},
        "retries": map[string]any{"type": "integer"},
    }),
)

Enums and Constraints

go
// Enum values
mcp.WithString("priority",
    mcp.Required(),
    mcp.Enum("low", "medium", "high", "critical"),
    mcp.Description("Task priority level"),
)

// String constraints
mcp.WithString("email",
    mcp.Required(),
    mcp.Pattern(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`),
    mcp.Description("Valid email address"),
)

// Number constraints
mcp.WithNumber("price",
    mcp.Required(),
    mcp.Min(0),
    mcp.Max(10000),
    mcp.Description("Product price in USD"),
)

Struct-Based Schema Definition

MCP-Go supports defining input and output schemas using Go structs with automatic JSON schema generation. This provides a type-safe alternative to manual parameter definition, especially useful for complex tools with structured inputs and outputs.

Input Schema with Go Structs

Define your input parameters as a Go struct and use WithInputSchema:

go
// Define input struct with JSON schema tags
type SearchRequest struct {
    Query      string   `json:"query" jsonschema:"Search query"`
    Limit      int      `json:"limit,omitempty" jsonschema:"Maximum results"`
    Categories []string `json:"categories,omitempty" jsonschema:"Filter by categories"`
    SortBy     string   `json:"sortBy,omitempty" jsonschema:"Sort field"`
}

// Create tool with struct-based input schema
searchTool := mcp.NewTool("search_products",
    mcp.WithDescription("Search product catalog"),
    mcp.WithInputSchema[SearchRequest](),
)

Output Schema with Go Structs

Define structured output for predictable tool responses:

go
// Define output struct
type SearchResponse struct {
    Query       string    `json:"query" jsonschema:"Original search query"`
    TotalCount  int       `json:"totalCount" jsonschema:"Total matching products"`
    Products    []Product `json:"products" jsonschema:"Search results"`
    ProcessedAt time.Time `json:"processedAt" jsonschema:"When search was performed"`
}

type Product struct {
    ID          string  `json:"id" jsonschema:"Product ID"`
    Name        string  `json:"name" jsonschema:"Product name"`
    Price       float64 `json:"price" jsonschema:"Price in USD"`
    InStock     bool    `json:"inStock" jsonschema:"Availability"`
}

// Create tool with both input and output schemas
searchTool := mcp.NewTool("search_products",
    mcp.WithDescription("Search product catalog with structured output"),
    mcp.WithInputSchema[SearchRequest](),
    mcp.WithOutputSchema[SearchResponse](),
)

Structured Tool Handlers

Use NewStructuredToolHandler for type-safe handler implementation:

go
func main() {
    s := server.NewMCPServer("Product Search", "1.0.0",
        server.WithToolCapabilities(false),
    )

    // Define tool with input and output schemas
    searchTool := mcp.NewTool("search_products",
        mcp.WithDescription("Search product catalog"),
        mcp.WithInputSchema[SearchRequest](),
        mcp.WithOutputSchema[SearchResponse](),
    )

    // Add tool with structured handler
    s.AddTool(searchTool, mcp.NewStructuredToolHandler(searchProductsHandler))
    
    server.ServeStdio(s)
}

// Handler receives typed input and returns typed output
func searchProductsHandler(ctx context.Context, req mcp.CallToolRequest, args SearchRequest) (SearchResponse, error) {
    // Input is already validated and bound to SearchRequest struct
    limit := args.Limit
    if limit <= 0 {
        limit = 10
    }

    // Perform search logic
    products := searchDatabase(args.Query, args.Categories, limit)

    // Return structured response
    return SearchResponse{
        Query:       args.Query,
        TotalCount:  len(products),
        Products:    products,
        ProcessedAt: time.Now(),
    }, nil
}

Validating Output Against the Schema

Declaring an output schema is a contract: clients can rely on the shape of
structuredContent matching the schema you advertised. To enforce that
contract at runtime, opt in to output schema validation when constructing
the server:

go
s := server.NewMCPServer("Product Search", "1.0.0",
    server.WithToolCapabilities(false),
    server.WithOutputSchemaValidation(), // reject results that don't match outputSchema
)

When enabled, every tool result whose StructuredContent does not conform
to the declared outputSchema is replaced with a tool execution error
(IsError: true). This catches handler bugs server-side instead of letting
them reach the client as a silent protocol violation. Tools without an
output schema, results without StructuredContent, and error results are
all passed through unchanged.

Array Output Schema

Tools can return arrays of structured data:

go
// Define asset struct
type Asset struct {
    ID       string  `json:"id" jsonschema:"Asset identifier"`
    Name     string  `json:"name" jsonschema:"Asset name"`
    Value    float64 `json:"value" jsonschema:"Current value"`
    Currency string  `json:"currency" jsonschema:"Currency code"`
}

// Tool that returns array of assets
assetsTool := mcp.NewTool("list_assets",
    mcp.WithDescription("List portfolio assets"),
    mcp.WithInputSchema[struct {
        Portfolio string `json:"portfolio" jsonschema:"Portfolio ID"`
    }](),
    mcp.WithOutputSchema[[]Asset](), // Array output schema
)

func listAssetsHandler(ctx context.Context, req mcp.CallToolRequest, args struct{ Portfolio string }) ([]Asset, error) {
    // Return array of assets
    return []Asset{
        {ID: "btc", Name: "Bitcoin", Value: 45000.50, Currency: "USD"},
        {ID: "eth", Name: "Ethereum", Value: 3200.75, Currency: "USD"},
    }, nil
}

Caching Schemas for Stateless / Serverless Deployments

Generating JSON schemas with WithInputSchema[T]() and WithOutputSchema[T]()
uses reflection at server start-up. In long-running servers this is a one-time
cost, but in serverless environments (AWS Lambda, Cloud Functions, Cloud Run
with scale-to-zero) the server is reconstructed on every cold start and the
reflection cost is paid again and again.

Use mcp.SchemaCache to pre-compute schemas at build time, persist them to
disk, and reload them at start-up:

go
// build/warm-cache.go — run as part of your build pipeline
func main() {
    cache := mcp.NewSchemaCache()
    if err := mcp.WarmFor[WeatherRequest](cache); err != nil {
        log.Fatal(err)
    }
    if err := mcp.WarmFor[WeatherResponse](cache); err != nil {
        log.Fatal(err)
    }
    if err := cache.Save("schemas.json"); err != nil {
        log.Fatal(err)
    }
}

At runtime, load the cache once and pass it to the cache-aware tool options.
On a cache hit the schema is served straight from memory; on a miss the cache
falls back to reflection and stores the result for next time, so the same
code works locally without a pre-built cache file:

go
var cache = func() *mcp.SchemaCache {
    c, err := mcp.LoadSchemaCache("schemas.json")
    if err != nil {
        return mcp.NewSchemaCache() // fall back to live reflection
    }
    return c
}()

weatherTool := mcp.NewTool("get_weather",
    mcp.WithDescription("Get current weather"),
    mcp.WithCachedInputSchema[WeatherRequest](cache),
    mcp.WithCachedOutputSchema[WeatherResponse](cache),
)

The cache key defaults to the package-qualified Go type name
(e.g. main.WeatherRequest). If you need a stable key that survives renames
or package moves, use the explicit-key variants:

go
if err := mcp.WarmFor[WeatherRequest](cache, "weather.v1.input"); err != nil {
    log.Fatal(err)
}

mcp.WithCachedInputSchemaKey[WeatherRequest](cache, "weather.v1.input")

SchemaCache is safe for concurrent use, marshals to deterministic JSON
(sorted keys), and writes via an atomic temp-file-and-rename in Save.

Schema Tags Reference

MCP-Go uses the google/jsonschema-go library with the jsonschema struct tag for schema generation:

go
type ExampleStruct struct {
    // Required field (no omitempty in json tag means required)
    Name string `json:"name" jsonschema:"User's full name"`

    // Field with description
    Age int `json:"age" jsonschema:"User age in years"`

    // Optional field (omitempty makes it optional)
    PageSize int `json:"pageSize,omitempty" jsonschema:"Number of items per page"`

    // Optional field with description
    Status string `json:"status,omitempty" jsonschema:"Current status"`
}

:::info Migration Note
MCP-Go migrated from invopop/jsonschema to google/jsonschema-go. If you are upgrading from an older version, note these breaking changes:

  • Description tag: Changed from jsonschema_description:"text" to jsonschema:"text"
  • Required fields: The jsonschema:"required" tag is no longer supported. Fields without omitempty in their json tag are automatically required.
  • Validation constraints: Tags like jsonschema:"enum=...", jsonschema:"minimum=..." are no longer supported in struct tags. Use mcp.WithRawInputSchema() for these constraints or define them with the builder API (mcp.WithString, mcp.WithNumber, etc.).
  • Additional properties: additionalProperties: false is now automatically included in generated schemas.
    :::

Manual Structured Results

For more control over the response, use NewTypedToolHandler with manual result creation:

go
manualTool := mcp.NewTool("process_data",
    mcp.WithDescription("Process data with custom result"),
    mcp.WithInputSchema[ProcessRequest](),
    mcp.WithOutputSchema[ProcessResponse](),
)

s.AddTool(manualTool, mcp.NewTypedToolHandler(manualProcessHandler))

func manualProcessHandler(ctx context.Context, req mcp.CallToolRequest, args ProcessRequest) (*mcp.CallToolResult, error) {
    // Process the data
    response := ProcessResponse{
        Status:      "completed",
        ProcessedAt: time.Now(),
        ItemCount:   42,
    }
    
    // Create custom fallback text for backward compatibility
    fallbackText := fmt.Sprintf("Processed %d items successfully", response.ItemCount)
    
    // Return structured result with custom text
    return mcp.NewToolResultStructured(response, fallbackText), nil
}

Complete Example: File Operations with Structured I/O

Here's a complete example using the file operations pattern from earlier, enhanced with structured schemas:

text
/* Detailed source-code truncated for AI context efficiency. */

Tool Handlers

Tool handlers process the actual function calls from LLMs. MCP-Go provides convenient helper methods for safe parameter extraction.

Parameter Extraction Methods

MCP-Go offers several helper methods on CallToolRequest for type-safe parameter access:

go
// Required parameters - return error if missing or wrong type
name, err := req.RequireString("name")
age, err := req.RequireInt("age") 
price, err := req.RequireFloat("price")
enabled, err := req.RequireBool("enabled")

// Optional parameters with defaults
name := req.GetString("name", "default")
count := req.GetInt("count", 10)
price := req.GetFloat("price", 0.0)
enabled := req.GetBool("enabled", false)

// Structured data binding
type Config struct {
    Timeout int    `json:"timeout"`
    Retries int    `json:"retries"`
    Debug   bool   `json:"debug"`
}
var config Config
if err := req.BindArguments(&config); err != nil {
    return mcp.NewToolResultError(err.Error()), nil
}

// Raw access (for backward compatibility)
args := req.GetArguments() // returns map[string]any
rawArgs := req.GetRawArguments() // returns any

Basic Handler Pattern

go
func handleCalculate(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    // Extract parameters using helper methods
    operation, err := req.RequireString("operation")
    if err != nil {
        return mcp.NewToolResultError(err.Error()), nil
    }
    
    x, err := req.RequireFloat("x")
    if err != nil {
        return mcp.NewToolResultError(err.Error()), nil
    }
    
    y, err := req.RequireFloat("y")
    if err != nil {
        return mcp.NewToolResultError(err.Error()), nil
    }
    
    // Perform calculation
    var result float64
    switch operation {
    case "add":
        result = x + y
    case "subtract":
        result = x - y
    case "multiply":
        result = x * y
    case "divide":
        if y == 0 {
            return mcp.NewToolResultError("division by zero"), nil
        }
        result = x / y
    default:
        return mcp.NewToolResultError(fmt.Sprintf("unknown operation: %s", operation)), nil
    }
    
    // Return result
    return mcp.NewToolResultText(fmt.Sprintf("%.2f", result)), nil
}

File Operations Tool

text
/* Detailed source-code truncated for AI context efficiency. */

Database Query Tool

text
/* Detailed source-code truncated for AI context efficiency. */

HTTP Request Tool

text
/* Detailed source-code truncated for AI context efficiency. */

Argument Validation

Type-Safe Parameter Extraction

MCP-Go provides helper methods for safe parameter extraction:

go
func handleValidatedTool(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    // Required parameters with validation
    name, err := req.RequireString("name")
    if err != nil {
        return mcp.NewToolResultError(err.Error()), nil
    }
    
    age, err := req.RequireFloat("age")
    if err != nil {
        return mcp.NewToolResultError(err.Error()), nil
    }
    
    // Optional parameter with default
    enabled := req.GetBool("enabled", true)
    
    // Validate constraints
    if len(name) == 0 {
        return mcp.NewToolResultError("name cannot be empty"), nil
    }
    
    if age < 0 || age > 150 {
        return mcp.NewToolResultError("age must be between 0 and 150"), nil
    }
    
    // Process with validated parameters
    result := processUser(name, int(age), enabled)
    
    jsonData, err := json.Marshal(result)
    if err != nil {
        return mcp.NewToolResultError(fmt.Sprintf("failed to marshal result: %v", err)), nil
    }
    
    return mcp.NewToolResultText(string(jsonData)), nil
}

Available Helper Methods

go
// Required parameters (return error if missing or wrong type)
name, err := req.RequireString("name")
age, err := req.RequireInt("age")
price, err := req.RequireFloat("price")
enabled, err := req.RequireBool("enabled")

// Optional parameters with defaults
name := req.GetString("name", "default")
count := req.GetInt("count", 10)
price := req.GetFloat("price", 0.0)
enabled := req.GetBool("enabled", false)

// Structured data binding
type UserData struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}
var user UserData
if err := req.BindArguments(&user); err != nil {
    return mcp.NewToolResultError(err.Error()), nil
}
text
### Custom Validation Functions

```go
func validateEmail(email string) error {
    emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
    if !emailRegex.MatchString(email) {
        return fmt.Errorf("invalid email format")
    }
    return nil
}

func validateURL(url string) error {
    parsed, err := url.Parse(url)
    if err != nil {
        return fmt.Errorf("invalid URL format: %w", err)
    }
    
    if parsed.Scheme != "http" && parsed.Scheme != "https" {
        return fmt.Errorf("URL must use http or https scheme")
    }
    
    return nil
}

Result Types

Text Results

go
func handleTextTool(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    message := "Operation completed successfully"
    return mcp.NewToolResultText(message), nil
}

JSON Results

go
func handleJSONTool(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    result := map[string]interface{}{
        "status":    "success",
        "timestamp": time.Now().Unix(),
        "data": map[string]interface{}{
            "processed": 42,
            "errors":    0,
        },
    }
    
    jsonData, err := json.Marshal(result)
    if err != nil {
        return mcp.NewToolResultError(fmt.Sprintf("failed to marshal result: %v", err)), nil
    }
    
    return mcp.NewToolResultText(string(jsonData)), nil
}

Multiple Content Types

go
func handleMultiContentTool(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    data := map[string]interface{}{
        "name": "John Doe",
        "age":  30,
    }
    
    return &mcp.CallToolResult{
        Content: []mcp.Content{
            {
                Type: "text",
                Text: "User information retrieved successfully",
            },
            {
                Type: "text",
                Text: fmt.Sprintf("Name: %s, Age: %d", data["name"], data["age"]),
            },
        },
    }, nil
}

Tools can return resource links that reference other resources in your MCP server. This is useful when you want to point to existing data without duplicating content:

go
func handleGetResourceLinkTool(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	resourceID, err := req.RequireString("resource_id")
	if err != nil {
		return mcp.NewToolResultError(err.Error()), nil
	}

	// Create a resource link pointing to an existing resource
	uri := fmt.Sprintf("file://documents/%s", resourceID)
	resourceLink := mcp.NewResourceLink(uri, "Document", "The requested document", "application/pdf")
	return &mcp.CallToolResult{
		Content: []mcp.Content{
			mcp.NewTextContent("Found the requested document:"),
			resourceLink,
		},
	}, nil
}

You can combine different content types including resource links in a single tool result:

go
func handleSearchDocumentsTool(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	query, err := req.RequireString("query")
	if err != nil {
		return mcp.NewToolResultError(err.Error()), nil
	}

	// Simulate document search
	foundDocs := []string{"doc1.pdf", "doc2.txt", "doc3.md"}

	content := []mcp.Content{
		mcp.NewTextContent(fmt.Sprintf("Found %d documents matching '%s':", len(foundDocs), query)),
	}

	// Add resource links for each found document
	for _, doc := range foundDocs {
		uri := fmt.Sprintf("file://documents/%s", doc)
		parts := strings.SplitN(doc, ".", 2)
		name := parts[0]
		mimeType := "application/octet-stream" // default
		if len(parts) > 1 {
			// Map extension to MIME type (simplified)
			switch parts[1] {
			case "pdf":
				mimeType = "application/pdf"
			case "txt":
				mimeType = "text/plain"
			case "md":
				mimeType = "text/markdown"
			}
		}
		resourceLink := mcp.NewResourceLink(uri, name, fmt.Sprintf("Document: %s", doc), mimeType)
		content = append(content, resourceLink)
	}

	return &mcp.CallToolResult{
		Content: content,
	}, nil
}

Resource links can include additional metadata through annotations:

go
func handleGetAnnotatedResourceTool(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	docType := req.GetString("type", "general")
	// Create resource link with annotations
	annotated := mcp.Annotated{
		Annotations: &mcp.Annotations{
			Audience:     []mcp.Role{mcp.RoleUser},
			LastModified: "2025-01-12T15:00:58Z",
		},
	}
	url := "file://documents/test.pdf"
	resourceLink := mcp.NewResourceLink(url, "Test Document", fmt.Sprintf("A %s document", docType), "application/pdf")
	resourceLink.Annotated = annotated
	return &mcp.CallToolResult{
		Content: []mcp.Content{
			mcp.NewTextContent("Here's the important document you requested:"),
			resourceLink,
		},
	}, nil
}

Error Results

go
func handleToolWithErrors(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    // For validation errors, return error result (not Go error)
    name, err := req.RequireString("name")
    if err != nil {
        return mcp.NewToolResultError(err.Error()), nil
    }
    
    // For business logic errors, also return error result
    if someCondition {
        return mcp.NewToolResultError("invalid input: " + reason), nil
    }
    
    // For system errors, you can return Go errors
    if systemError {
        return nil, fmt.Errorf("system failure: %v", err)
    }
    
    // Or return structured error information
    return &mcp.CallToolResult{
        Content: []mcp.Content{
            {
                Type: "text", 
                Text: "Operation failed",
            },
        },
        IsError: true,
    }, nil
}

Application-defined JSON-RPC errors must be marked explicitly before a handler
returns them. This prevents an error received from an upstream server from being
forwarded accidentally:

go
type InvalidCustomerIDError struct {
    CustomerID string
}

func (e InvalidCustomerIDError) Error() string {
    return "invalid customer ID"
}

func (e InvalidCustomerIDError) JSONRPCError() mcp.JSONRPCError {
    return mcp.JSONRPCError{
        Error: mcp.NewJSONRPCErrorDetails(
            mcp.INVALID_PARAMS,
            e.Error(),
            map[string]any{"customerId": e.CustomerID},
        ),
    }
}

func handleCustomer(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    customerID := req.GetString("customerId", "")
    if customerID == "" {
        return nil, server.NewHandlerError(InvalidCustomerIDError{CustomerID: customerID})
    }
    return mcp.NewToolResultText("customer found"), nil
}

Unmarked Go errors, including errors from another JSON-RPC server, are returned
as internal errors.

Tool Annotations

Provide hints to help LLMs use your tools effectively:

go
tool := mcp.NewTool("search_database",
    mcp.WithDescription("Search the product database"),
    mcp.WithString("query",
        mcp.Required(),
        mcp.Description("Search query (supports wildcards with *)"),
    ),
    mcp.WithNumber("limit",
        mcp.DefaultNumber(10),
        mcp.Min(1),
        mcp.Max(100),
        mcp.Description("Maximum number of results to return"),
    ),
    mcp.WithArray("categories",
        mcp.Description("Filter by product categories"),
        mcp.Items(map[string]any{"type": "string"}),
    ),
)

s.AddTool(tool, handleSearchDatabase)

Advanced Tool Patterns

Streaming Results

For long-running operations, consider streaming results:

go
func handleStreamingTool(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    // For operations that take time, provide progress updates
    results := []string{}
    
    for i := 0; i < 10; i++ {
        // Simulate work
        time.Sleep(100 * time.Millisecond)
        
        // Check for cancellation
        select {
        case <-ctx.Done():
            return nil, ctx.Err()
        default:
        }
        
        results = append(results, fmt.Sprintf("Processed item %d", i+1))
    }
    
    resultData := map[string]interface{}{
        "status":  "completed",
        "results": results,
    }
    
    jsonData, err := json.Marshal(resultData)
    if err != nil {
        return mcp.NewToolResultError(fmt.Sprintf("failed to marshal result: %v", err)), nil
    }
    
    return mcp.NewToolResultText(string(jsonData)), nil
}

Conditional Tools

Tools that are only available under certain conditions:

go
func addConditionalTools(s *server.MCPServer, userRole string) {
    // Admin-only tools
    if userRole == "admin" {
        adminTool := mcp.NewTool("delete_user",
            mcp.WithDescription("Delete a user account (admin only)"),
            mcp.WithString("user_id", mcp.Required()),
        )
        s.AddTool(adminTool, handleDeleteUser)
    }
    
    // User tools available to all
    userTool := mcp.NewTool("get_profile",
        mcp.WithDescription("Get user profile information"),
    )
    s.AddTool(userTool, handleGetProfile)
}

Session-specific Tools

You can add tools to a specific client session, allowing different clients to have access to different tools or different implementations of the same tool.

The server provides convenient helper functions for managing session tools:

go
// Add a single session tool
userTool := mcp.NewTool(
    "get_user_data",
    mcp.WithDescription("Get current user's data"),
)

err := s.AddSessionTool(
    sessionID,
    userTool,
    func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
        // This handler is only available to this specific session
        return mcp.NewToolResultText("User data for " + sessionID), nil
    },
)
if err != nil {
    log.Printf("Failed to add session tool: %v", err)
}

// Add multiple session tools at once
err = s.AddSessionTools(
    sessionID,
    server.ServerTool{
        Tool: mcp.NewTool("user_settings", mcp.WithDescription("Manage user settings")),
        Handler: func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
            return handleUserSettings(sessionID, req)
        },
    },
    server.ServerTool{
        Tool: mcp.NewTool("user_history", mcp.WithDescription("Access user history")),
        Handler: func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
            return handleUserHistory(sessionID, req)
        },
    },
)
if err != nil {
    log.Printf("Failed to add session tools: %v", err)
}

// Delete session tools when no longer needed
err = s.DeleteSessionTools(sessionID, "get_user_data", "user_settings")
if err != nil {
    log.Printf("Failed to delete session tools: %v", err)
}

Direct Interface Usage

You can also work directly with the SessionWithTools interface:

go
sseServer := server.NewSSEServer(
    s,
    server.WithAppendQueryToMessageEndpoint(),
    server.WithSSEContextFunc(func(ctx context.Context, r *http.Request) context.Context {
        withNewTools := r.URL.Query().Get("withNewTools")
        if withNewTools != "1" {
            return ctx
        }

        session := server.ClientSessionFromContext(ctx)
        if sessionWithTools, ok := session.(server.SessionWithTools); ok {
            // Add the new tools
            sessionWithTools.SetSessionTools(map[string]server.ServerTool{
                myNewTool.Name: {
                    Tool:    myNewTool,
                    Handler: NewToolHandler(myNewToolHandler),
                },
            })
        }

        return ctx
    }),
)

Important Notes

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

Next Steps

  • Prompts - Learn to create reusable interaction templates
  • Advanced Features - Explore typed tools, middleware, and hooks
  • Resources - Learn about exposing data sources

Www Docs Pages Transports Http (`www/docs/pages/transports/http.mdx`)

StreamableHTTP Transport

StreamableHTTP transport provides traditional request/response communication for MCP servers, perfect for REST-like interactions, stateless clients, and integration with existing web infrastructure.

Use Cases

StreamableHTTP transport excels in scenarios requiring:

  • Web services: Traditional REST API patterns
  • Stateless interactions: Each request is independent
  • Load balancing: Distribute requests across multiple servers
  • Caching: Leverage HTTP caching mechanisms
  • Integration: Work with existing HTTP infrastructure
  • Public APIs: Expose MCP functionality as web APIs

Example applications:

  • Microservice architectures
  • Public API endpoints
  • Integration with API gateways
  • Cached data services
  • Rate-limited services
  • Multi-tenant applications

Implementation

Basic StreamableHTTP Server

text
/* Detailed source-code truncated for AI context efficiency. */

Advanced StreamableHTTP Configuration

text
/* Detailed source-code truncated for AI context efficiency. */

Endpoints

Standard MCP Endpoints

When you start a StreamableHTTP MCP server, it automatically creates these endpoints:

text
POST /mcp/initialize     - Initialize MCP session
POST /mcp/tools/list     - List available tools
POST /mcp/tools/call     - Call a tool
POST /mcp/resources/list - List available resources
POST /mcp/resources/read - Read a resource
POST /mcp/prompts/list   - List available prompts
POST /mcp/prompts/get    - Get a prompt
GET  /mcp/health         - Health check
GET  /mcp/capabilities   - Server capabilities

Custom Endpoints

Add custom HTTP endpoints alongside MCP:

text
/* Detailed source-code truncated for AI context efficiency. */

Request/Response Patterns

Standard MCP Request

json
POST /mcp/tools/call
Content-Type: application/json

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "search_users",
    "arguments": {
      "query": "john",
      "limit": 10,
      "offset": 0
    }
  }
}

Standard MCP Response

json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "{\"users\":[...],\"total\":25,\"limit\":10,\"offset\":0}"
      }
    ]
  }
}

Error Response

json
{
  "jsonrpc": "2.0",
  "id": 1,
  "error": {
    "code": -32602,
    "message": "Invalid params",
    "data": {
      "details": "user_id is required"
    }
  }
}

Session Management

Session Idle TTL

When clients disconnect without sending a DELETE request, per-session transport state (tools, resources, log levels, etc.) can leak memory. Use WithSessionIdleTTL to automatically sweep idle sessions:

go
httpServer := server.NewStreamableHTTPServer(s,
    server.WithSessionIdleTTL(10*time.Minute), // Sweep sessions idle for 10+ minutes
)

The sweeper runs in the background and removes per-session state for sessions that haven't received any requests within the TTL. It also invalidates swept session IDs so they cannot be reused. A zero or negative TTL disables the sweeper (default behavior). The sweeper is automatically stopped on Shutdown().

Stream Resumability

By default, messages sent while a client's SSE connection is down are lost. Configure an EventStore with WithEventStore to record every message delivered on a session's SSE streams so clients can resume after a broken connection:

go
httpServer := server.NewStreamableHTTPServer(s,
    server.WithEventStore(server.NewInMemoryEventStore()),
)

With a store configured, each SSE event carries an id field issued by the store. To resume, a client reconnects with a GET request carrying the standard Last-Event-ID header (alongside its Mcp-Session-Id); the server replays the messages recorded after that event on the same stream, then continues live delivery. This also covers messages produced while nobody was connected: a tool call whose POST connection broke keeps recording its notifications and final response, all of which are redelivered on resume. Unknown event IDs, or event IDs belonging to a different session, are rejected with 400 Bad Request.

NewInMemoryEventStore keeps events in process memory, which suits single-process deployments. A session's events are purged when the session terminates — an explicit DELETE or, with WithSessionIdleTTL configured, an idle sweep — after which its event IDs can no longer be resumed from. For multi-node deployments, implement the EventStore interface against shared storage (Redis, a database, etc.) and pass it to WithEventStore.

Stateful vs Stateless

go
// Each request is independent
func handleStatelessTool(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    // Extract all needed information from request
    userID := extractUserFromToken(ctx)
    params := req.Params.Arguments
    
    // Process without relying on server state
    result, err := processRequest(userID, params)
    if err != nil {
        return nil, err
    }
    
    return mcp.NewToolResultJSON(result)
}

// Use external storage for persistence
func getUserPreferences(userID string) (map[string]interface{}, error) {
    // Load from database, cache, etc.
    return loadFromRedis(fmt.Sprintf("user:%s:prefs", userID))
}

Stateful Design (When Needed)

text
/* Detailed source-code truncated for AI context efficiency. */

Authentication and Authorization

text
/* Detailed source-code truncated for AI context efficiency. */

OAuth Protected Resource Metadata (RFC 9728)

When your MCP server requires OAuth, clients need a way to discover which
authorization servers, scopes, and bearer methods are supported. The MCP
authorization spec
points clients at a RFC 9728
/.well-known/oauth-protected-resource endpoint to do exactly that.

mcp-go provides two ways to serve this metadata.

Option A: Auto-mounted via a server option

The simplest approach — the well-known endpoint is registered automatically
by Start() and is also dispatched when the server is mounted as an
http.Handler via ServeHTTP.

go
mcpServer := server.NewMCPServer("my-server", "1.0.0")

httpServer := server.NewStreamableHTTPServer(mcpServer,
    server.WithProtectedResourceMetadata(server.ProtectedResourceMetadataConfig{
        Resource:               "https://my-mcp-server.com",
        AuthorizationServers:   []string{"https://auth.example.com"},
        ScopesSupported:        []string{"mcp:read", "mcp:write"},
        BearerMethodsSupported: []string{"header"},
        ResourceName:           "My MCP Server",
    }),
)

httpServer.Start(":8080")
// GET http://localhost:8080/.well-known/oauth-protected-resource
//   -> 200 application/json with the metadata above

For SSE servers, use the equivalent WithSSEProtectedResourceMetadata:

go
sseServer := server.NewSSEServer(mcpServer,
    server.WithSSEProtectedResourceMetadata(server.ProtectedResourceMetadataConfig{
        Resource:             "https://my-mcp-server.com",
        AuthorizationServers: []string{"https://auth.example.com"},
    }),
)

Option B: Standalone handler for custom routing

If you build your own http.ServeMux (or use a router like chi / gin),
use NewProtectedResourceMetadataHandler directly:

go
mux := http.NewServeMux()
mux.Handle("/mcp", mcpHandler)
mux.Handle("/.well-known/oauth-protected-resource",
    server.NewProtectedResourceMetadataHandler(server.ProtectedResourceMetadataConfig{
        Resource:             "https://my-mcp-server.com",
        AuthorizationServers: []string{"https://auth.example.com"},
        ScopesSupported:      []string{"mcp:read", "mcp:write"},
    }))
http.ListenAndServe(":8080", mux)

Path-qualified resources (RFC 9728 §3.1)

When your resource identifier has a path (e.g. https://example.com/mcp),
the well-known URL must include that path suffix:
/.well-known/oauth-protected-resource/mcp. Use
server.ProtectedResourceMetadataPath to derive the correct path:

go
cfg := server.ProtectedResourceMetadataConfig{
    Resource:             "https://example.com/mcp",
    AuthorizationServers: []string{"https://auth.example.com"},
}
mux.Handle(server.ProtectedResourceMetadataPath(cfg.Resource),
    server.NewProtectedResourceMetadataHandler(cfg))
// -> mounts at /.well-known/oauth-protected-resource/mcp

The auto-mount options (WithProtectedResourceMetadata /
WithSSEProtectedResourceMetadata) apply this rule automatically.

Handler behavior

Method Response
GET 200 with application/json body, Cache-Control: no-store
HEAD 200 headers, no body
OPTIONS 204 (CORS preflight), permissive Access-Control-Allow-* headers
other 405 with Allow: GET, HEAD, OPTIONS

CORS is enabled with Access-Control-Allow-Origin: * so browser-based MCP
clients can perform discovery cross-origin.

DNS Rebinding Protection

Local MCP servers are a prime target for DNS rebinding attacks:
a malicious website rebinds its own domain to 127.0.0.1, letting the
victim's browser issue what it considers same-origin requests against a
server running on the victim's machine. CORS does not stop this — after
rebinding, the browser sees a same-origin request and applies no
cross-origin restrictions.

Both HTTP transports therefore validate the Host header automatically.
When a request arrives over a loopback connection (127.0.0.1, [::1])
but its Host header is not a localhost value (localhost, a 127.0.0.0/8
address, or [::1], with or without a port), the request is rejected with
403 Forbidden.

The protection is enabled by default and requires no configuration:

  • It is detected at runtime from the connection's local address
    (http.LocalAddrContextKey), so it applies whether the server listens on
    localhost or 0.0.0.0.
  • Requests arriving via non-loopback addresses (external traffic) are
    never affected.

Reverse proxies: if a proxy (nginx, Envoy, ...) on the same host
forwards requests to the MCP server via localhost while preserving the
original Host header, those requests would be rejected. Prefer
configuring the proxy to rewrite the Host header to localhost;
alternatively, opt out explicitly:

go
httpServer := server.NewStreamableHTTPServer(mcpServer,
    // Only disable this if you understand the security implications.
    server.WithDisableLocalhostProtection(true),
)

Use server.WithSSEDisableLocalhostProtection(true) for the
SSE transport.

The protection applies to ServeHTTP (and the SSE handlers) only. The
framework-agnostic Handle entry
point has no access to the connection's local address, so adapters must
enforce the check themselves — see Behavior Notes.

Cross-Origin Resource Sharing (CORS)

The MCP endpoints themselves are not CORS-enabled by default — every
deployment has different origin requirements, and emitting permissive
headers unconditionally would be a security regression. Browser-based MCP
clients hosted on a different origin than the server will be blocked unless
you opt in.

Use server.WithStreamableHTTPCORS (or server.WithSSECORS for the SSE
transport) to enable CORS handling. The transport then answers preflight
(OPTIONS) requests directly and decorates simple cross-origin responses
with the appropriate Access-Control-* headers, including a Vary: Origin
so downstream caches behave correctly.

go
httpServer := server.NewStreamableHTTPServer(mcpServer,
    server.WithEndpointPath("/mcp"),
    server.WithStreamableHTTPCORS(
        server.WithCORSAllowedOrigins("https://my-ai-app.com", "http://localhost:3000"),
        server.WithCORSAllowCredentials(),
        server.WithCORSMaxAge(300),
    ),
)

Available CORSOption helpers:

Option Purpose
WithCORSAllowedOrigins(...) Origins permitted to access the server. "*" allows any origin.
WithCORSAllowedMethods(...) Methods advertised in preflight (defaults to GET, POST, DELETE, OPTIONS).
WithCORSAllowedHeaders(...) Request headers advertised in preflight (defaults to Content-Type, Mcp-Session-Id, Last-Event-ID, Authorization).
WithCORSExposedHeaders(...) Response headers exposed to JavaScript (defaults to Mcp-Session-Id).
WithCORSAllowCredentials() Sends Access-Control-Allow-Credentials: true.
WithCORSMaxAge(seconds) Sets Access-Control-Max-Age for preflight caching.

When WithCORSAllowedOrigins("*") is combined with WithCORSAllowCredentials(),
the server echoes the request's Origin header instead of * to remain
compliant with the CORS specification.

Request Headers

The StreamableHTTP transport now passes HTTP request headers to MCP handlers. This allows you to access the original HTTP headers that were sent with the request in your tool and resource handlers.

Accessing Headers in Handlers

Headers are available in all MCP request objects:

go
func handleGetUser(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    // Access request headers
    headers := req.Header

    // Use headers for authentication, tracing, etc.
    authToken := headers.Get("Authorization")
    if authToken == "" {
        return nil, fmt.Errorf("authentication required")
    }
    
    // Access other headers
    requestID := headers.Get("X-Request-ID")
    userAgent := headers.Get("User-Agent")
    
    // Rest of your handler code...
}

This works for all MCP request types including:

  • CallToolRequest
  • ReadResourceRequest
  • ListToolsRequest
  • ListResourcesRequest
  • InitializeRequest
  • And other MCP request types

The headers are automatically populated by the transport layer and are available in your handlers without any additional configuration.

Sampling Support

StreamableHTTP transport now supports bidirectional sampling, allowing servers to request LLM completions from clients. This enables advanced scenarios where servers can leverage client-side LLM capabilities.

:::warning[Security: Human-in-the-Loop Required]
Per the MCP specification, implementations SHOULD always include a human in the loop with the ability to deny sampling requests.

Your sampling handler implementation MUST:

  • Present sampling requests to users for review before execution
  • Allow users to view and edit prompts before sending to the LLM
  • Display generated responses for approval before returning to the server
  • Provide clear UI to accept or reject sampling requests

Failing to implement approval flows creates serious security and trust risks, including:

  • Servers making unauthorized LLM requests on behalf of users
  • Exposure of sensitive data through unreviewed prompts
  • Uncontrolled API costs from automated sampling
  • Lack of user consent for AI interactions

See the example implementation below for a reference approval pattern.
:::

Requirements for Sampling

To enable sampling with StreamableHTTP transport, the client must use the WithContinuousListening() option:

go
// Client setup with sampling support
httpTransport, err := transport.NewStreamableHTTP(
    serverURL,
    transport.WithContinuousListening(), // Required for sampling
)

// Create client with sampling handler
mcpClient := client.NewClient(httpTransport, 
    client.WithSamplingHandler(samplingHandler))

Without WithContinuousListening(), the client won't maintain a persistent connection to receive sampling requests from the server.

Server-Side Implementation

Enable sampling in your StreamableHTTP server:

text
/* Detailed source-code truncated for AI context efficiency. */

How It Works

  1. Persistent Connection: When WithContinuousListening() is enabled, the client maintains a persistent SSE connection to the server
  2. Bidirectional Communication: The server can send sampling requests through the SSE stream
  3. Response Channel: The client responds to sampling requests via HTTP POST to the same endpoint
  4. Session Correlation: Responses are correlated using session IDs to ensure they reach the correct handler

Limitations

  • Sampling requires WithContinuousListening() to maintain the SSE connection
  • Without continuous listening, the transport operates in stateless request/response mode only
  • Network interruptions may require reconnection and re-establishment of the sampling channel

Example with Approval Flow

Here's a reference implementation showing proper human-in-the-loop approval:

go
type ApprovalSamplingHandler struct {
    llmClient LLMClient // Your actual LLM client
    ui        UserInterface // Your UI for presenting requests to users
}

func (h *ApprovalSamplingHandler) CreateMessage(ctx context.Context, request mcp.CreateMessageRequest) (*mcp.CreateMessageResult, error) {
    // Step 1: Present the sampling request to the user for review
    approved, modifiedRequest, err := h.ui.PresentSamplingRequest(ctx, request)
    if err != nil {
        return nil, fmt.Errorf("failed to get user approval: %w", err)
    }
    
    if !approved {
        return nil, fmt.Errorf("user rejected sampling request")
    }
    
    // Step 2: Send the approved/modified request to the LLM
    response, err := h.llmClient.CreateCompletion(ctx, modifiedRequest)
    if err != nil {
        return nil, fmt.Errorf("LLM request failed: %w", err)
    }
    
    // Step 3: Present the response to the user for final approval
    approved, modifiedResponse, err := h.ui.PresentSamplingResponse(ctx, response)
    if err != nil {
        return nil, fmt.Errorf("failed to get response approval: %w", err)
    }
    
    if !approved {
        return nil, fmt.Errorf("user rejected sampling response")
    }
    
    // Step 4: Return the approved response to the server
    return modifiedResponse, nil
}

Key Points:

  • Users must explicitly approve both the request (before sending to LLM) and the response (before returning to server)
  • Users can modify prompts or responses before approval
  • Rejection at any stage returns an error to the server
  • The UI should clearly display what the server is requesting and why

Embedding in Non-net/http Frameworks

StreamableHTTPServer exposes two equivalent entry points:

  • ServeHTTP(w http.ResponseWriter, r *http.Request) — the standard
    net/http handler used by the examples above.
  • Handle(w HTTPResponseWriter, r *HTTPRequest) — a transport-agnostic
    entry point that lets you embed MCP into HTTP frameworks that do not
    go through net/http (e.g. fasthttp
    or fiber) without round-tripping the response
    through a buffering adaptor.

Both methods drive the same internal state machine; ServeHTTP is
implemented as a thin wrapper around Handle.

When to Use Which

Situation Use
Standalone server, mounted in http.ServeMux, chi, gorilla/mux, etc. ServeHTTP (or srv.Start(":8080"))
Mounted in a fiber/echo/gin app via adaptor.HTTPHandler and you don't need streaming ServeHTTP
Mounted in fasthttp/fiber and you want native chunked SSE without an adaptor in the path Handle
Custom transport (test harness, in-memory dispatcher, alternative wire format on top of HTTP semantics) Handle

:::tip
If you only need request/response (no SSE notifications and no GET
listening connection), the community workaround
fiberApp.Group("/mcp", adaptor.HTTPHandler(server.NewStreamableHTTPServer(s)))
is the simplest path and is fully supported.
:::

API Surface

go
// HTTPRequest is a transport-agnostic view of an incoming MCP HTTP request.
type HTTPRequest struct {
    Method  string          // "POST", "GET", "DELETE"
    URL     *url.URL        // optional; only Path is consulted
    Header  http.Header     // required; plain map[string][]string
    Body    []byte          // already buffered; may be nil for GET/DELETE
    Context context.Context // request context; cancellation is honored
}

// HTTPResponseWriter is the minimum surface mcp-go needs to write a
// streamable HTTP response.
type HTTPResponseWriter interface {
    Header() http.Header
    WriteHeader(statusCode int)
    Write(p []byte) (int, error)
    Flush()
    CanStream() bool
}

func (s *StreamableHTTPServer) Handle(w HTTPResponseWriter, r *HTTPRequest)

Streaming-Capability Contract

The CanStream() bit on HTTPResponseWriter lets the server gracefully
degrade when the underlying transport cannot deliver chunked responses:

  • CanStream() returns true — Flush() MUST forward bytes to the
    client immediately. POST responses can upgrade to text/event-stream
    when notifications fire mid-flight, and GET (the SSE listening channel)
    is accepted.
  • CanStream() returns false — Flush() SHOULD be a no-op. POST
    responses stay as a single buffered application/json reply (any
    notifications produced during the request are dropped, since they cannot
    be delivered without SSE). GET requests are rejected with 405 Method Not Allowed.

Adapter Example: fasthttp

text
/* Detailed source-code truncated for AI context efficiency. */

Behavior Notes

  • WithStreamableHTTPCORS is not applied when entering through
    Handle. Use the framework's native CORS middleware.
  • DNS rebinding protection is not applied when entering through
    Handle, because it has no access to the connection's local address.
    Adapters for localhost-reachable servers should reject requests that
    arrive on a loopback connection with a non-loopback Host header,
    answering 403 Forbidden. See
    DNS Rebinding Protection above.
  • WithProtectedResourceMetadata is not applied when entering through
    Handle. Mount the metadata route separately using
    NewProtectedResourceMetadataHandler. See
    OAuth Protected Resource Metadata
    above.
  • WithHTTPContextFunc is honored. When the request entered through
    ServeHTTP it receives the original *http.Request; when entered
    through Handle it receives a synthetic *http.Request derived from
    the HTTPRequest. Callers can also pre-decorate r.Context directly,
    which is the recommended pattern for non-net/http integrations.
  • SessionIdManagerResolver still receives a *http.Request for
    backwards compatibility; the synthetic request carries the headers and
    URL needed by all built-in resolvers.
  • Backwards compatibility: every existing public API
    (NewStreamableHTTPServer, ServeHTTP, Start, Shutdown, every
    option, every session interface) is unchanged.

Next Steps


Www Docs Pages Transports Inprocess (`www/docs/pages/transports/inprocess.mdx`)

In-Process Transport

In-process transport enables direct integration of MCP servers within the same process, eliminating network overhead and providing seamless integration for embedded scenarios.

Use Cases

In-process transport is perfect for:

  • Embedded servers: MCP functionality within existing applications
  • Testing and development: Fast, reliable testing without network overhead
  • Library integrations: MCP as a library component
  • Single-process architectures: Monolithic applications with MCP capabilities

Example applications:

  • Desktop applications with plugin architectures
  • Testing frameworks
  • Embedded analytics engines
  • Game engines with AI tool integration

Implementation

Basic In-Process Server

text
/* Detailed source-code truncated for AI context efficiency. */

Embedded Application Integration

text
/* Detailed source-code truncated for AI context efficiency. */

Sampling Support

In-process transport supports sampling, allowing servers to request LLM completions from clients. This enables bidirectional communication where servers can leverage client-side LLM capabilities.

Enabling Sampling

To enable sampling in your in-process server, call EnableSampling() during server setup:

text
/* Detailed source-code truncated for AI context efficiency. */

Real LLM Integration

For production use, replace the mock handler with a real LLM integration:

OpenAI Integration

text
/* Detailed source-code truncated for AI context efficiency. */

Sampling Request Parameters

The CreateMessageRequest supports various parameters to control LLM behavior:

go
samplingRequest := mcp.CreateMessageRequest{
    CreateMessageParams: mcp.CreateMessageParams{
        // Required: Messages to send to the LLM
        Messages: []mcp.SamplingMessage{
            {
                Role: mcp.RoleUser,        // or mcp.RoleAssistant
                Content: mcp.TextContent{   // or mcp.ImageContent
                    Type: "text",
                    Text: "Your message here",
                },
            },
        },
        
        // Optional: System prompt for context
        SystemPrompt: "You are a helpful assistant.",
        
        // Optional: Maximum tokens to generate
        MaxTokens: 1000,
        
        // Optional: Temperature for randomness (0.0 to 1.0)
        Temperature: 0.7,
        
        // Optional: Top-p sampling parameter
        TopP: 0.9,
        
        // Optional: Stop sequences
        StopSequences: []string{"\\n\\n"},
    },
}

Error Handling

Always handle sampling errors gracefully:

go
result, err := mcpServer.RequestSampling(ctx, samplingRequest)
if err != nil {
    // Log the error
    log.Printf("Sampling request failed: %v", err)
    
    // Return appropriate error response
    return &mcp.CallToolResult{
        Content: []mcp.Content{
            mcp.TextContent{
                Type: "text",
                Text: "Sorry, I couldn't process your request at this time.",
            },
        },
        IsError: true,
    }, nil
}

Next Steps


Www Docs Pages Transports Sse (`www/docs/pages/transports/sse.mdx`)

SSE Transport

Server-Sent Events (SSE) transport enables real-time, web-friendly communication between MCP clients and servers. Perfect for web applications that need live updates and multi-client support.

Use Cases

SSE transport is ideal for:

  • Web applications: Browser-based LLM interfaces
  • Real-time dashboards: Live data monitoring and visualization
  • Collaborative tools: Multi-user environments with shared state
  • Streaming responses: Long-running operations with progress updates
  • Event-driven systems: Applications that need server-initiated communication

Example applications:

  • Web-based chat interfaces with LLMs
  • Real-time analytics dashboards
  • Collaborative document editing
  • Live system monitoring tools
  • Streaming data processing interfaces

Implementation

Basic SSE Server

text
/* Detailed source-code truncated for AI context efficiency. */

Advanced SSE Configuration

text
/* Detailed source-code truncated for AI context efficiency. */

Configuration

SSE Server Options

The SSE server can be configured with various options:

go
sseServer := server.NewSSEServer(s,
    // Set the base path for SSE endpoints
    server.WithStaticBasePath("/api/mcp"),
    
    // Configure keep-alive interval
    server.WithKeepAliveInterval(30*time.Second),
    
    // Set base URL for client connections
    server.WithBaseURL("http://localhost:8080"),
    
    // Configure SSE and message endpoints
    server.WithSSEEndpoint("/sse"),
    server.WithMessageEndpoint("/message"),
    
    // Add context function for request processing
    server.WithSSEContextFunc(func(ctx context.Context, r *http.Request) context.Context {
        // Add custom context values from headers
        return ctx
    }),

    // Enable Cross-Origin Resource Sharing for browser-based clients
    server.WithSSECORS(
        server.WithCORSAllowedOrigins("https://my-ai-app.com"),
        server.WithCORSAllowCredentials(),
    ),
)

CORS handling is opt-in. Without WithSSECORS, the SSE stream falls back to
the historical default of Access-Control-Allow-Origin: * for backwards
compatibility, while the message endpoint emits no CORS headers.
See the Streamable HTTP CORS docs
for the full list of CORSOption helpers shared between both transports.

The SSE server also ships with automatic
DNS rebinding protection:
requests arriving over a loopback connection (127.0.0.1, [::1]) whose
Host header is not a localhost value are rejected with 403 Forbidden.
This is enabled by default and covers ServeHTTP as well as the
SSEHandler()/MessageHandler() sub-handlers. Use
server.WithSSEDisableLocalhostProtection(true) to opt out, for example
behind a same-host reverse proxy that preserves the original Host header
(prefer configuring the proxy to rewrite the Host header to localhost
instead).

Resulting endpoints:

  • SSE stream: http://localhost:8080/api/mcp/sse
  • Message endpoint: http://localhost:8080/api/mcp/message

SSE Client Timeout Options

When using the legacy SSE transport on the client side, you can configure endpoint and response timeouts:

go
import "github.com/mark3labs/mcp-go/client/transport"

sseTransport, err := transport.NewSSE("http://localhost:8080/sse",
    // Max time to wait for the SSE endpoint URL during Start() (default: 30s)
    transport.WithEndpointTimeout(60*time.Second),

    // Max time to wait for a response after sending a request (default: 60s)
    transport.WithResponseTimeout(120*time.Second),
)

Zero or negative values are silently ignored to prevent misconfiguration. If the context passed to the operation has a shorter deadline, the shorter value takes precedence.

Real-Time Notifications

SSE transport enables real-time server-to-client communication through notifications. Use the server context to send notifications:

go
func handleRealtimeTool(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    // Get the MCP server from context
    mcpServer := server.ServerFromContext(ctx)
    
    // Send a notification to the client
    if mcpServer != nil {
        err := mcpServer.SendNotificationToClient(ctx, "custom_event", map[string]interface{}{
            "message": "Real-time update",
            "timestamp": time.Now().Unix(),
        })
        if err != nil {
            log.Printf("Failed to send notification: %v", err)
        }
    }
    
    return mcp.NewToolResultText(`{"status":"notification_sent"}`), nil
}

Session Management

The SSE server automatically handles session management. You can send events to specific sessions using the server's notification methods:

go
// Send notification to current client session
mcpServer.SendNotificationToClient(ctx, "progress_update", progressData)

// Send notification to all connected clients (if supported)
// Note: Check the server implementation for broadcast capabilities

Request Headers

Like the StreamableHTTP transport, the SSE transport passes HTTP request headers to MCP handlers. This allows you to access the original HTTP headers that were sent with the SSE connection in your tool and resource handlers.

Accessing Headers in Handlers

Headers from the SSE connection are available in all MCP request objects:

go
func handleStreamData(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    // Access request headers
    headers := req.Header

    // Use headers for authentication, tracing, etc.
    authToken := headers.Get("Authorization")
    if authToken == "" {
        return nil, fmt.Errorf("authentication required")
    }
    
    // Access other headers
    requestID := headers.Get("X-Request-ID")
    userAgent := headers.Get("User-Agent")
    
    // Rest of your handler code...
    mcpServer := server.ServerFromContext(ctx)
    // ...
}

This works for all MCP request types including:

  • CallToolRequest
  • ReadResourceRequest
  • ListToolsRequest
  • ListResourcesRequest
  • InitializeRequest
  • And other MCP request types

The headers are automatically populated by the SSE transport layer from the initial SSE connection and are available in your handlers without any additional configuration.

Note: Since SSE maintains a persistent connection, the headers are captured when the connection is established and remain the same for all requests during that connection's lifetime.

Next Steps


Www Docs Pages Transports Stdio (`www/docs/pages/transports/stdio.mdx`)

STDIO Transport

STDIO (Standard Input/Output) transport is the most common MCP transport method, perfect for command-line tools, desktop applications, and local integrations.

Use Cases

STDIO transport excels in scenarios where:

  • Command-line tools: CLI utilities that LLMs can invoke
  • Desktop applications: IDE plugins, text editors, local tools
  • Subprocess communication: Parent processes managing MCP servers
  • Local development: Testing and debugging MCP implementations
  • Single-user scenarios: Personal productivity tools

Example applications:

  • File system browsers for IDEs
  • Local database query tools
  • Git repository analyzers
  • System monitoring utilities
  • Development workflow automation

Implementation

Basic STDIO Server

text
/* Detailed source-code truncated for AI context efficiency. */

Advanced STDIO Server

text
/* Detailed source-code truncated for AI context efficiency. */

Client Integration

How LLM Applications Connect

LLM applications typically connect to STDIO MCP servers by:

  1. Spawning the process: Starting your server as a subprocess
  2. Pipe communication: Using stdin/stdout for JSON-RPC messages
  3. Lifecycle management: Handling process startup, shutdown, and errors

Claude Desktop Integration

Configure your STDIO server in Claude Desktop:

json
{
  "mcpServers": {
    "file-tools": {
      "command": "go",
      "args": ["run", "/path/to/your/server/main.go"],
      "env": {
        "LOG_LEVEL": "info"
      }
    }
  }
}

macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
Windows: %APPDATA%\Claude\claude_desktop_config.json

Custom Client Integration

text
/* Detailed source-code truncated for AI context efficiency. */

Customizing Subprocess Execution

If you need more control over how a sub-process is spawned when creating a new STDIO client, you can use
NewStdioMCPClientWithOptions instead of NewStdioMCPClient.

By passing the WithCommandFunc option, you can supply a custom factory function to create the exec.Cmd that launches
the server. This allows configuration of environment variables, working directories, and system-level process attributes.

Referring to the previous example, we can replace the line that creates the client:

go
c, err := client.NewStdioClient(
    "go", nil, "run", "/path/to/server/main.go",
)

With the options-aware version:

go
c, err := client.NewStdioMCPClientWithOptions(
	"go",
	nil,
	[]string {"run", "/path/to/server/main.go"},
	transport.WithCommandFunc(func(ctx context.Context, command string, args []string, env []string) (*exec.Cmd, error) {
        cmd := exec.CommandContext(ctx, command, args...)
        cmd.Env = env // Explicit environment for the subprocess.
        cmd.Dir = "/var/sandbox/mcp-server" // Working directory (not isolated unless paired with chroot or namespace).

        // Apply low-level process isolation and privilege dropping.
        cmd.SysProcAttr = &syscall.SysProcAttr{
            // Drop to non-root user (e.g., user/group ID 1001)
            Credential: &syscall.Credential{
                Uid: 1001,
                Gid: 1001,
            },
            // File system isolation: only works if running as root.
            Chroot: "/var/sandbox/mcp-server",

            // Linux namespace isolation (Linux only):
            // Prevents access to other processes, mounts, IPC, networks, etc.
            Cloneflags: syscall.CLONE_NEWIPC | // Isolate inter-process comms
                syscall.CLONE_NEWNS  | // Isolate filesystem mounts
                syscall.CLONE_NEWPID | // Isolate PID namespace (child sees itself as PID 1)
                syscall.CLONE_NEWUTS | // Isolate hostname
                syscall.CLONE_NEWNET,  // Isolate networking (optional)
        }

		return cmd, nil
	}),
)

Debugging

Command Line Testing

Test your STDIO server directly from the command line:

bash
# Start your server
go run main.go

# Send initialization request
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{"tools":{}},"clientInfo":{"name":"test","version":"1.0.0"}}}' | go run main.go

# List tools
echo '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' | go run main.go

# Call a tool
echo '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"list_files","arguments":{"path":".","recursive":false}}}' | go run main.go

Interactive Testing Script

bash
#!/bin/bash

# interactive_test.sh
SERVER_CMD="go run main.go"

echo "Starting MCP STDIO server test..."

# Function to send JSON-RPC request
send_request() {
    local request="$1"
    echo "Sending: $request"
    echo "$request" | $SERVER_CMD
    echo "---"
}

# Initialize
send_request '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{"tools":{}},"clientInfo":{"name":"test","version":"1.0.0"}}}'

# List tools
send_request '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'

# List resources
send_request '{"jsonrpc":"2.0","id":3,"method":"resources/list","params":{}}'

# Call tool
send_request '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"list_files","arguments":{"path":".","recursive":false}}}'

echo "Test completed."

Debug Logging

Add debug logging to your STDIO server:

go
func main() {
    // Setup debug logging
    logFile, err := os.OpenFile("mcp-server.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
    if err != nil {
        log.Fatal(err)
    }
    defer logFile.Close()

    logger := log.New(logFile, "[MCP] ", log.LstdFlags|log.Lshortfile)

    s := server.NewMCPServer("Debug Server", "1.0.0",
        server.WithToolCapabilities(true),
        server.WithLogging(),
    )

    // Add tools with debug logging
    s.AddTool(
        mcp.NewTool("debug_echo",
            mcp.WithDescription("Echo with debug logging"),
            mcp.WithString("message", mcp.Required()),
        ),
        func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
            message := req.GetString("message", "")
            logger.Printf("Echo tool called with message: %s", message)
            return mcp.NewToolResultText(fmt.Sprintf("Echo: %s", message)), nil
        },
    )

    logger.Println("Starting STDIO server...")
    if err := server.ServeStdio(s); err != nil {
        logger.Printf("Server error: %v", err)
    }
}

MCP Inspector Integration

Use the MCP Inspector for visual debugging:

bash
# Install MCP Inspector
npm install @modelcontextprotocol/inspector

# Run your server with inspector
mcp-inspector go run main.go

This opens a web interface where you can:

  • View available tools and resources
  • Test tool calls interactively
  • Inspect request/response messages
  • Debug protocol issues

Error Handling

Robust Error Handling

go
func handleToolWithErrors(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    // Validate required parameters
    path, err := req.RequireString("path")
    if err != nil {
        return nil, fmt.Errorf("path parameter is required and must be a string")
    }

    // Validate path security
    if !isValidPath(path) {
        return nil, fmt.Errorf("invalid or unsafe path: %s", path)
    }

    // Check if path exists
    if _, err := os.Stat(path); os.IsNotExist(err) {
        return nil, fmt.Errorf("path does not exist: %s", path)
    }

    // Handle context cancellation
    select {
    case <-ctx.Done():
        return nil, ctx.Err()
    default:
    }

    // Perform operation with timeout
    ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
    defer cancel()

    result, err := performOperation(ctx, path)
    if err != nil {
        // Log error for debugging
        logError(fmt.Sprintf("Operation failed for path %s: %v", path, err))
        
        // Return user-friendly error
        if errors.Is(err, context.DeadlineExceeded) {
            return nil, fmt.Errorf("operation timed out")
        }
        
        return nil, fmt.Errorf("operation failed: %w", err)
    }

    return mcp.NewToolResultText(fmt.Sprintf("%v", result)), nil
}

Process Management

go
func main() {
    // Handle panics gracefully
    defer func() {
        if r := recover(); r != nil {
            logError(fmt.Sprintf("Server panic: %v", r))
            os.Exit(1)
        }
    }()

    s := server.NewMCPServer("Robust Server", "1.0.0",
        server.WithRecovery(), // Built-in panic recovery
    )

    // Setup signal handling
    setupSignalHandling()

    // Start server with retry logic
    for attempts := 0; attempts < 3; attempts++ {
        if err := server.ServeStdio(s); err != nil {
            logError(fmt.Sprintf("Server attempt %d failed: %v", attempts+1, err))
            if attempts == 2 {
                os.Exit(1)
            }
            time.Sleep(time.Second * time.Duration(attempts+1))
        } else {
            break
        }
    }
}

func setupSignalHandling() {
    c := make(chan os.Signal, 1)
    signal.Notify(c, os.Interrupt, syscall.SIGTERM)
    
    go func() {
        sig := <-c
        logToFile(fmt.Sprintf("Received signal: %v", sig))
        os.Exit(0)
    }()
}

Performance Optimization

Efficient Resource Usage

go
// Use connection pooling for database tools
var dbPool *sql.DB

func init() {
    var err error
    dbPool, err = sql.Open("sqlite3", "data.db")
    if err != nil {
        log.Fatal(err)
    }
    
    dbPool.SetMaxOpenConns(10)
    dbPool.SetMaxIdleConns(5)
    dbPool.SetConnMaxLifetime(time.Hour)
}

// Cache frequently accessed data
var fileCache = make(map[string]cacheEntry)
var cacheMutex sync.RWMutex

type cacheEntry struct {
    content   string
    timestamp time.Time
}

func getCachedFile(path string) (string, bool) {
    cacheMutex.RLock()
    defer cacheMutex.RUnlock()
    
    entry, exists := fileCache[path]
    if !exists || time.Since(entry.timestamp) > 5*time.Minute {
        return "", false
    }
    
    return entry.content, true
}

Memory Management

go
func handleLargeFile(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    path := req.GetString("path", "")
    
    // Stream large files instead of loading into memory
    file, err := os.Open(path)
    if err != nil {
        return nil, err
    }
    defer file.Close()

    // Process in chunks
    const chunkSize = 64 * 1024
    buffer := make([]byte, chunkSize)
    
    var result strings.Builder
    for {
        n, err := file.Read(buffer)
        if err == io.EOF {
            break
        }
        if err != nil {
            return nil, err
        }
        
        // Process chunk
        processed := processChunk(buffer[:n])
        result.WriteString(processed)
        
        // Check for cancellation
        select {
        case <-ctx.Done():
            return nil, ctx.Err()
        default:
        }
    }

    return mcp.NewToolResultText(result.String()), nil
}

Next Steps


--- METRICS ---