Technical Documentation: mark3labs/mcp-go
ℹ️ Provenance: Multi-Tier Ingestion · CodeWiki Reference
Overview (`README.md`)
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
go get github.com/mark3labs/mcp-goQuickstart
Let's create a simple MCP server that exposes a calculator tool and some data:
/* 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:
// 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:
// 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:
// 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
/* Detailed source-code truncated for AI context efficiency. */Task execution flow:
- Client calls tool with task parameter
- Server immediately returns task ID
- Tool executes asynchronously in the background
- Client polls
tasks/resultto retrieve the result - 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):
// 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 asynchronouslyLimiting Concurrent Tasks
To prevent resource exhaustion, you can limit the number of concurrent running tasks:
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:
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:
/* 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:
/* 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:
examples/task_tool/- Demonstrates task-augmented tools with TaskSupportRequired and TaskSupportOptional modesexamples/structured_input_and_output/- Shows how to use struct-based input/output schemas with type-safe tool handlersexamples/typed_tools/- Demonstrates type-safe tool handlers with strongly-typed argumentsexamples/custom_context/- Shows how to use custom contexts in tool handlers- Additional examples covering resources, prompts, and more in the examples directory
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:
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 (orserver.WithSSEProtectedResourceMetadata) to auto-mount the endpoint, orserver.NewProtectedResourceMetadataHandler to wire it into a custom router.
See the HTTP transport docs for examples.
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 orserver.WithSSECORS:
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 andVary 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]) whoseHost 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 theHost header to localhost, or opt out explicitly:
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
// 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:
/* Detailed source-code truncated for AI context efficiency. */Tool Filtering
You can also apply filters to control which tools are available to certain sessions:
// 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:
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 theserver.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:
go generate ./...You need go installed and the goimports tool available. The generator runsgoimports 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
/* 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
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
Totalto indicate the total number of available matches - Use
HasMoreto 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.
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 theSamplingHandlerinterfaceNewInProcessClientWithSamplingHandler()- Creates client with sampling support- The handler receives sampling requests and returns mock LLM responses
Running the Example
go run main.goExpected Output
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
# Set environment variables (optional)
export MCP_CLIENT_ID=your_client_id
export MCP_CLIENT_SECRET=your_client_secret
# Run the example
go run main.goHow it Works
- The client attempts to initialize a connection to the MCP server
- If the server requires OAuth authentication, it will return a 401 Unauthorized response
- 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
- The user authorizes the application in their browser
- The authorization server redirects back to the local callback server
- The client exchanges the authorization code for an access token
- The client retries the initialization with the access token
- The client can now make authenticated requests to the MCP server
Configuration
Edit the following constants in main.go to match your environment:
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 resourcesmcp.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
SamplingHandlerinterface 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:
go build -o sampling_clientRun with the sampling server:
./sampling_client ../sampling_server/sampling_serverOr with any other MCP server that supports sampling:
./sampling_client /path/to/your/mcp/serverImplementation Details
- Sampling Handler: Implements
client.SamplingHandlerinterface - Client Configuration: Uses
client.WithSamplingHandler()to enable sampling - Automatic Capability: Sampling capability is automatically declared during initialization
- Request Processing: Handles incoming
sampling/createMessagerequests from servers
Sample Output
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:
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
Start an MCP server that supports sampling (e.g., using the
sampling_serverexample)Update the server URL in
main.go:gohttpClient, err := client.NewStreamableHttpClient( "http://your-server:port", // Replace with your server URL )Run the client:
bashgo 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:
httpTransport, err := transport.NewStreamableHTTP("http://localhost:8080")
mcpClient := client.NewClient(httpTransport, client.WithSamplingHandler(samplingHandler))Sampling Handler
The MockSamplingHandler implements the client.SamplingHandler interface:
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:
mcpClient := client.NewClient(
httpTransport,
client.WithSamplingHandler(samplingHandler),
)
// Sampling capability is automatically declared when a handler is providedReal Implementation
For a production implementation, replace the MockSamplingHandler with a real LLM client:
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
Start the server:
bashgo run main.goThe server will be available at:
http://localhost:8080/mcpConnect with an HTTP client that supports sampling (like the
sampling_http_clientexample)
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
- Tool Invocation: Client calls
ask_llmtool - Sampling Request: Server creates sampling request with user's question
- SSE Transmission: Server sends JSON-RPC request to client via SSE stream
- Client Processing: Client's sampling handler processes the request
- HTTP Response: Client sends JSON-RPC response back via HTTP POST
- Tool Response: Server returns the LLM response to the original tool caller
Communication Architecture
Client (HTTP + SSE) ←→ Server (HTTP)
│ │
├─ POST: Tool Call ──→ │
│ │
│ ←── SSE: Sampling ───┤
│ Request │
│ │
├─ POST: Sampling ───→ │
│ Response │
│ │
│ ←── HTTP: Tool ──────┤
ResponseKey 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:
Start this server:
bashgo run examples/sampling_http_server/main.goIn another terminal, start the client:
bashgo run examples/sampling_http_client/main.goThe 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:
// 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 LLMsystem_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:
go build -o sampling_server
./sampling_serverThe server communicates via stdio and expects to be connected to an MCP client that supports sampling.
Implementation Details
- Enable Sampling: The server calls
mcpServer.EnableSampling()to declare sampling capability - Request Sampling: Tools use
mcpServer.RequestSampling(ctx, request)to send sampling requests to the client - Handle Responses: The server receives and processes the LLM responses from the client via bidirectional stdio communication
- 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:
cd ../sampling_client
go build -o sampling_client
./sampling_client ../sampling_server/sampling_serverOverview (`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:
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:
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:
tool := mcp.NewTool("get_weather",
mcp.WithDescription("Get weather information"),
mcp.WithInputSchema[WeatherRequest](),
mcp.WithOutputSchema[WeatherResponse](),
)Return structured data in tool result:
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 olderjsonschema_description:"..."tag is not read and is silently ignored. - Required: derived automatically — a field is required unless its
json
tag containsomitemptyoromitzero. There is nojson:"...,required"
option, and thejsonschema:"required"tag is no longer supported. - Constraints (
enum,minimum,maximum, ...): not supported as struct
tags. Ajsonschematag value that begins withword=is rejected withtag must not begin with 'WORD='. To add constraints, usemcp.WithRawInputSchema()or the builder API (mcp.WithString,mcp.WithNumber, ... with options likemcp.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 emitsmcp.<method>server spans for every dispatched JSON-RPC method, plus atool.<name>child span around each tool invocation.otel.WithClientTracing(tracer)installs the same on the client. Outgoing
spans aremcp.<method>client-kind, and the client injects W3Ctraceparentinto the request headers.- The streamable-HTTP transport carries the
traceparentheader 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
go run ./otel/exampleSpans 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:
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 attachtraceparent 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:
go generate ./...Development
request_handler.go.tmplgeneratesserver/request_handler.go, andhooks.go.tmplgeneratesserver/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 throughgoimports 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:
go get github.com/mark3labs/mcp-goMCP-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:
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
- Save the code above to a file (e.g.,
main.go) - 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:
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:
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:
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
- Client Basics - Creating and managing client lifecycle
- Client Operations - Using tools, resources, and prompts
- Client Transports - Transport-specific client implementations
Quick Example
Here's a complete example showing how to create and use an MCP client:
/* 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
// Create STDIO client
client, err := client.NewStdioMCPClient("server-command", "arg1", "arg2")StreamableHTTP Client
Best for:
- Web applications
- Microservice architectures
- Load-balanced deployments
- REST-like interactions
// Create StreamableHTTP client
client := client.NewStreamableHttpClient("http://localhost:8080/mcp")SSE Client
Best for:
- Real-time web applications
- Browser-based interfaces
- Streaming data scenarios
- Multi-client environments
// Create SSE client
client := client.NewSSEMCPClient("http://localhost:8080/mcp/sse")In-Process Client
Best for:
- Testing and development
- Embedded scenarios
- High-performance applications
- Library integrations
// Create in-process client
client := client.NewInProcessClient(server)Common Client Patterns
Connection Management
import (
"context"
"errors"
"fmt"
"log"
"sync"
"time"
"github.com/mark3labs/mcp-go/client"
"github.com/mark3labs/mcp-go/mcp"
)
type MCPClientManager struct {
client client.Client
ctx context.Context
cancel context.CancelFunc
}
func NewMCPClientManager(clientType, address string) (*MCPClientManager, error) {
var c client.Client
var err error
switch clientType {
case "stdio":
c, err = client.NewStdioMCPClient("server-command")
case "http":
c = client.NewStreamableHttpClient(address)
case "sse":
c = client.NewSSEMCPClient(address)
default:
return nil, fmt.Errorf("unknown client type: %s", clientType)
}
if err != nil {
return nil, err
}
ctx, cancel := context.WithCancel(context.Background())
manager := &MCPClientManager{
client: c,
ctx: ctx,
cancel: cancel,
}
// Initialize connection
if err := c.Initialize(ctx); err != nil {
cancel()
return nil, fmt.Errorf("failed to initialize client: %w", err)
}
return manager, nil
}
func (m *MCPClientManager) Close() error {
m.cancel()
return m.client.Close()
}Error Handling
func handleClientErrors(ctx context.Context, c client.Client) {
// Tool call with error handling
result, err := c.CallTool(ctx, mcp.CallToolRequest{
Params: mcp.CallToolRequestParams{
Name: "example_tool",
Arguments: map[string]interface{}{
"param": "value",
},
},
})
if err != nil {
switch {
case errors.Is(err, client.ErrConnectionLost):
log.Println("Connection lost, attempting reconnect...")
// Implement reconnection logic
case errors.Is(err, client.ErrToolNotFound):
log.Printf("Tool not found: %v", err)
case errors.Is(err, client.ErrInvalidArguments):
log.Printf("Invalid arguments: %v", err)
default:
log.Printf("Unexpected error: %v", err)
}
return
}
// Process successful result
processToolResult(result)
}Retry Logic
func callToolWithRetry(ctx context.Context, c client.Client, req mcp.CallToolRequest, maxRetries int) (*mcp.CallToolResult, error) {
var lastErr error
for attempt := 0; attempt <= maxRetries; attempt++ {
result, err := c.CallTool(ctx, req)
if err == nil {
return result, nil
}
lastErr = err
// Don't retry certain errors
if errors.Is(err, client.ErrInvalidArguments) ||
errors.Is(err, client.ErrToolNotFound) {
break
}
// Exponential backoff
if attempt < maxRetries {
backoff := time.Duration(1<<attempt) * time.Second
log.Printf("Attempt %d failed, retrying in %v: %v", attempt+1, backoff, err)
select {
case <-time.After(backoff):
case <-ctx.Done():
return nil, ctx.Err()
}
}
}
return nil, fmt.Errorf("failed after %d attempts: %w", maxRetries+1, lastErr)
}Integration Patterns
LLM Application Integration
/* Detailed source-code truncated for AI context efficiency. */Multi-Server Client
/* Detailed source-code truncated for AI context efficiency. */Next Steps
Explore each client topic in detail:
- Client Basics - Client lifecycle and error handling
- Client Operations - Tools, resources, and prompts
- Client Transports - Transport-specific implementations
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
- Server Basics - Creating and configuring servers
- Resources - Exposing data to LLMs
- Tools - Providing functionality to LLMs
- Prompts - Creating reusable interaction templates
- Task-Augmented Tools - Asynchronous long-running tool execution
- Completions - Argument auto-completion for prompts and resources
- Elicitation - Requesting additional user input during interactions
- Advanced Features - Typed tools, middleware, hooks, sampling, and more
Quick Example
Here's a complete MCP server that demonstrates the key concepts:
/* 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:
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
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
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
// 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:
- STDIO Transport - Command-line integration
- SSE Transport - Real-time web applications
- StreamableHTTP Transport - Traditional web services
- In-Process Transport - Embedded scenarios
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)
// 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)
// 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
// 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:
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
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
HTTP - Traditional request/response
- Best for: Web services, REST-like APIs
- Pros: Standard protocol, caching, load balancing
- Cons: No real-time updates, more complex
// 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
// 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
// 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
// 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:
- Initialize: Client connects and exchanges capabilities
- Active: Client makes requests, server responds
- Cleanup: Connection closes, resources are freed
// 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:
// 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:
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:
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:
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:
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:
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 theelicitation/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:
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.
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 wonOn 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, andReadResource fulfil the server's requests and retry for you:
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 checkresult.NeedsInput() and pass InputResponses and RequestState on the next
call.
Subscriptions
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
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:
- Rewrite server-initiated requests as multi round-trip handlers. Replace
RequestSampling,RequestElicitation, andRequestRootscalls withNewInputRequestBuilder. The result works for old and new clients alike. - 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. - Replace
SubscribewithListenon clients that consume notifications. - Set cache hints for catalogs that do not change often.
- Move off
WithContinuousListening,Ping, andSetLevel, 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:
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:
cd hello-server
go mod init hello-server
go get github.com/mark3labs/mcp-go
go run main.goRunning Your First Server
Testing with Claude Desktop
Install Claude Desktop from Anthropic's website
Configure your server by editing Claude's config file:
macOS:
~/Library/Application Support/Claude/claude_desktop_config.json
Windows:%APPDATA%\Claude\claude_desktop_config.jsonjson{ "mcpServers": { "hello-world": { "command": "go", "args": ["run", "/path/to/your/hello-server/main.go"] } } }Restart Claude Desktop and look for the 🔌 icon indicating MCP connection
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:
# Install the MCP Inspector
npm install @modelcontextprotocol/inspector
# Run your server with the inspector
mcp-inspector go run main.goThis 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:
/* Detailed source-code truncated for AI context efficiency. */StreamableHTTP Client Example
For StreamableHTTP-based servers, use the StreamableHTTP client:
/* 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:
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:
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:
/* Detailed source-code truncated for AI context efficiency. */Real LLM Integration
OpenAI Integration
/* Detailed source-code truncated for AI context efficiency. */Anthropic Integration
/* 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:
// 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:
{
"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:
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:
{
"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:
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:
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
- Implement Proper Error Handling: Always handle LLM API errors gracefully
- Respect Rate Limits: Implement rate limiting and backoff strategies
- Validate Inputs: Check message content and parameters before processing
- Use Context: Respect context cancellation and timeouts
- Log Appropriately: Log requests for debugging but avoid logging sensitive data
- Model Selection: Allow configuration of which LLM model to use
- Content Filtering: Implement content filtering if required by your use case
Testing Your Implementation
Test your sampling handler with the sampling server example:
# 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_serverTransport Support
Sampling is available on the following transports:
STDIO Transport
For STDIO clients, create the transport and client separately:
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:
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
- Learn about server-side sampling implementation
- Explore client operations
- Check out the sampling examples
- See in-process transport documentation for embedded scenarios
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
// 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
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
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
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
- Creation - Instantiate the client
- Initialization - Establish connection and exchange capabilities
- Operation - Use tools, resources, and prompts
- Cleanup - Close connections and free resources
Complete Lifecycle Example
/* Detailed source-code truncated for AI context efficiency. */Initialization Process
The initialization process establishes the MCP connection and exchanges capabilities:
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
/* Detailed source-code truncated for AI context efficiency. */Error Handling
Proper error handling is essential for robust client applications.
Error Types
// 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
/* Detailed source-code truncated for AI context efficiency. */Retry Logic with Exponential Backoff
/* Detailed source-code truncated for AI context efficiency. */Context and Timeout Management
/* Detailed source-code truncated for AI context efficiency. */Connection Monitoring
Health Checks
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
/* Detailed source-code truncated for AI context efficiency. */Next Steps
- Client Operations - Learn to use tools, resources, and prompts
- Client Transports - Explore transport-specific features
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
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
/* 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
/* Detailed source-code truncated for AI context efficiency. */Typed Resource Reading
/* Detailed source-code truncated for AI context efficiency. */Resource Caching
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
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
/* Detailed source-code truncated for AI context efficiency. */Batch Tool Operations
/* Detailed source-code truncated for AI context efficiency. */Using Prompts
Prompts provide reusable templates for LLM interactions.
Basic Prompt Usage
/* Detailed source-code truncated for AI context efficiency. */Prompt Template Processing
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
/* Detailed source-code truncated for AI context efficiency. */Subscriptions
Some transports support subscriptions for receiving real-time notifications.
Basic Subscription Handling
/* Detailed source-code truncated for AI context efficiency. */Advanced Subscription Management
/* 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
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 theMCPClient 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:
cc, ok := c.(*client.Client)
if !ok {
return fmt.Errorf("iterators require *client.Client, got %T", c)
}
// safe to call cc.IterTools / cc.IterResources / ... hereThroughout 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
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:
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:
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
- Client Transports - Learn transport-specific client features
- Client Basics - Review fundamental concepts
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
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
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
/* Detailed source-code truncated for AI context efficiency. */STDIO Process Management
/* Detailed source-code truncated for AI context efficiency. */// 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:
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
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
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:
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 onGetAuthorizationURL 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
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
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
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
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
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
/* Detailed source-code truncated for AI context efficiency. */SSE Event Handling
/* 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
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
/* Detailed source-code truncated for AI context efficiency. */Transport Selection
Decision Matrix
Choose your transport based on these factors:
/* Detailed source-code truncated for AI context efficiency. */Multi-Transport Client Factory
/* 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.
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 withtransport.NewLogging. The wrapper is a transparent decorator: it implementstransport.Interface and additionally BidirectionalInterface /HTTPConnection whenever the inner transport does, so type assertions in the
client continue to behave as expected.
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:
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_changedUse 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:
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:
/* Detailed source-code truncated for AI context efficiency. */Sampling Request Parameters
The CreateMessageRequest supports various parameters to control LLM behavior:
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:
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
// 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
mcp.TextContent{
Type: "text",
Text: "Your text content here",
}Image Content
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) andToolResultContent (the result of that call) — these flow through the sameSamplingMessage.Content field:
// 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:
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:
// 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
- Enable Sampling Early: Call
EnableSampling()during server initialization - Handle Timeouts: Set appropriate timeouts for sampling requests
- Graceful Errors: Always provide meaningful error messages to users
- Content Extraction: Use helper functions to extract text from responses
- System Prompts: Use clear system prompts to guide LLM behavior
- Parameter Validation: Validate tool parameters before making sampling requests
Complete Example
Here's a complete example server with sampling:
/* 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:
// 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:
// 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
- Learn about client-side sampling implementation
- Explore advanced server features
- Check out the sampling examples
- See in-process sampling documentation for embedded scenarios
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
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:
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
/* Detailed source-code truncated for AI context efficiency. */Complex Typed Tool
/* Detailed source-code truncated for AI context efficiency. */Custom Validation
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
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
/* Detailed source-code truncated for AI context efficiency. */Middleware
Add cross-cutting concerns like logging, authentication, and rate limiting.
Logging Middleware
/* Detailed source-code truncated for AI context efficiency. */Rate Limiting Middleware
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
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:
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
/* Detailed source-code truncated for AI context efficiency. */Custom Business Logic Hooks
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
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
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
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
/* Detailed source-code truncated for AI context efficiency. */Production Configuration
Complete Production Server
/* Detailed source-code truncated for AI context efficiency. */Client Capability Based Filtering
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
/* Detailed source-code truncated for AI context efficiency. */Additional Content Types
AudioContent
Audio data can be embedded in prompt messages or tool results:
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 dataMIMEType— MIME type of the audio (e.g.,"audio/wav","audio/mp3")
ResourceLink
Tool results can include links to server resources instead of inline content:
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:
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
- Task-Augmented Tools - Asynchronous long-running tools
- Completions - Argument auto-completion
- Elicitation - Request user input during interactions
- Client Development - Learn to build MCP clients
- Server Basics - Review fundamental concepts
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
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:
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:
// 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:
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:
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:
// 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 implementationYou can set these via server options:
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
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:
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:
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:
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:
// 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:
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:
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:
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:
- Resources - Expose data to LLMs
- Tools - Provide functionality to LLMs
- Prompts - Create reusable interaction templates
- Task-Augmented Tools - Asynchronous long-running tool execution
- Completions - Argument auto-completion
- Elicitation - Request additional user input
- Advanced Features - Hooks, middleware, and more
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:
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:
type PromptCompletionProvider interface {
CompletePromptArgument(ctx context.Context, promptName string, argument mcp.CompleteArgument, context mcp.CompleteContext) (*mcp.Completion, error)
}Example Implementation
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:
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:
type ResourceCompletionProvider interface {
CompleteResourceArgument(ctx context.Context, uri string, argument mcp.CompleteArgument, context mcp.CompleteContext) (*mcp.Completion, error)
}Example Implementation
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
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:
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 inValues.HasMore— set totruewhen additional completions exist beyond the current response.
Supporting Types
The request carries two small structs your provider receives:
// 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:
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:
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
RequestedSchemato be non-nil - URL mode requires both
ElicitationIDandURLto 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:
mcp.ElicitationResponseActionAccept // User provided the requested information
mcp.ElicitationResponseActionDecline // User explicitly declined
mcp.ElicitationResponseActionCancel // User cancelled without making a choiceThe result is returned as an ElicitationResult:
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:
/* 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:
/* 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:
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:
// 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:
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:
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:
type ElicitationHandler interface {
Elicit(ctx context.Context, request mcp.ElicitationRequest) (*mcp.ElicitationResult, error)
}Register the handler when creating the client:
import "github.com/mark3labs/mcp-go/client"
handler := &MyElicitationHandler{}
c := client.NewClient(transport, client.WithElicitationHandler(handler))The implementation should:
- Present the request message to the user (and the URL if in URL mode)
- Validate input against the requested schema (for form mode)
- Allow the user to accept, decline, or cancel
- Return the appropriate
ElicitationResult
ElicitationCapability
The ElicitationCapability struct advertises which elicitation modes a client or server supports during initialization:
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:
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:
ctx, cancel := context.WithTimeout(ctx, 2*time.Minute)
defer cancel()
result, err := mcpServer.RequestElicitation(ctx, elicitationRequest)Best Practices
- Clear Messages: Write human-readable messages that explain what you need and why
- Minimal Schemas: Only request the information you actually need
- Handle All Actions: Always handle accept, decline, and cancel — don't assume the user will accept
- Validate Responses: Type-assert and validate the
Contentfield before using it - Graceful Degradation: If elicitation fails or is unsupported, provide a sensible fallback
- Use URL Mode Sparingly: Reserve URL mode for flows that genuinely require a browser (e.g., OAuth)
- Track Elicitation IDs: For URL mode, use unique IDs to correlate browser callbacks with sessions
Next Steps
- Learn about client-side elicitation handling
- Explore advanced server features
- See the elicitation example
- Read about sampling for server-initiated LLM requests
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.
type ToolHandlerFunc func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error)
type ToolHandlerMiddleware func(ToolHandlerFunc) ToolHandlerFuncRegistering 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.
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:
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:
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:
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:
// 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:
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:
s.Use(A, B)
s.Use(C)
// Call order: A → B → C → tool handler → C → B → AThis 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.
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:
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
- Tools — Define tools that middleware wraps
- Task-Augmented Tools — Long-running tools with task support
- Advanced Features — Hooks, session management, and more
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
// 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:
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.
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
/* Detailed source-code truncated for AI context efficiency. */Data Analysis Prompt
/* Detailed source-code truncated for AI context efficiency. */Prompt Arguments
Flexible Parameter Handling
/* Detailed source-code truncated for AI context efficiency. */Message Types
Multi-Message Conversations
/* Detailed source-code truncated for AI context efficiency. */System and User Roles
/* Detailed source-code truncated for AI context efficiency. */Embedded Resources
Including Resource Data
/* Detailed source-code truncated for AI context efficiency. */Dynamic Resource Integration
/* Detailed source-code truncated for AI context efficiency. */Advanced Prompt Patterns
Conditional Prompts
/* Detailed source-code truncated for AI context efficiency. */Template-Based Prompts
/* 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
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:
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.
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:
s := server.NewMCPServer("Server", "1.0.0",
server.WithPromptCapabilities(true),
server.WithPromptHandlerMiddleware(loggingMiddleware),
server.WithPromptHandlerMiddleware(authMiddleware),
)Next Steps
- Advanced Features - Explore typed tools, middleware, and hooks
- Client Integration - Learn how to build MCP clients
- Tools - Learn about implementing server tools
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
// Create a simple resource
resource := mcp.NewResource(
"docs://readme", // URI - unique identifier
"Project README", // Name - human-readable
mcp.WithResourceDescription("Main project documentation"),
mcp.WithMIMEType("text/markdown"),
)Resource Icons
Resources can include icons for better visual identification:
resource := mcp.NewResource(
"docs://readme",
"Project README",
mcp.WithResourceDescription("Main project documentation"),
mcp.WithMIMEType("text/markdown"),
mcp.WithResourceIcons(
mcp.Icon{
Src: "https://example.com/icons/document.svg",
MIMEType: "image/svg+xml",
},
),
)Display Title
Resources and resource templates can carry a human-readable display title separate from the name. Clients show the title in file browsers and pickers; name remains a stable identifier suitable for programmatic lookup.
resource := mcp.NewResource(
"docs://readme",
"readme.md", // name (stable identifier)
mcp.WithResourceTitle("Project README"), // Shown in UI
mcp.WithResourceDescription("Main project documentation"),
mcp.WithMIMEType("text/markdown"),
)
// Same option exists on resource templates
template := mcp.NewResourceTemplate(
"users://{user_id}",
"user-profile",
mcp.WithTemplateTitle("User Profile"),
mcp.WithTemplateDescription("Profile information for a specific user"),
)If Title is empty, clients fall back to Name. The field serialises with omitempty, so resources that don't set a title are unchanged on the wire.
Resource Size
When the raw byte size of a resource is known in advance, set it so hosts can render file sizes and pre-budget context-window usage without fetching the body:
resource := mcp.NewResource(
"file:///var/log/app.log",
"app.log",
mcp.WithResourceTitle("Application Log"),
mcp.WithMIMEType("text/plain"),
mcp.WithResourceSize(2_048_576), // 2 MiB, measured before base64 encoding
)Size is exposed as *int64 on the Resource struct so an explicit zero-byte resource is distinguishable from an unknown size. Omit WithResourceSize entirely when the size is unknown.
Static Resources
Static resources have fixed URIs and typically serve predetermined content.
File-Based Resources
Expose files from your filesystem:
func main() {
s := server.NewMCPServer("File Server", "1.0.0",
server.WithResourceCapabilities(true),
)
// Add a static file resource
s.AddResource(
mcp.NewResource(
"file://README.md",
"Project README",
mcp.WithResourceDescription("Main project documentation"),
mcp.WithMIMEType("text/markdown"),
),
handleReadmeFile,
)
server.ServeStdio(s)
}
func handleReadmeFile(ctx context.Context, req mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
content, err := os.ReadFile("README.md")
if err != nil {
return nil, fmt.Errorf("failed to read README: %w", err)
}
return &mcp.ReadResourceResult{
Contents: []mcp.ResourceContent{
{
URI: req.Params.URI,
MIMEType: "text/markdown",
Text: string(content),
},
},
}, nil
}Configuration Resources
Expose application configuration:
// Configuration resource
s.AddResource(
mcp.NewResource(
"config://app",
"Application Configuration",
mcp.WithResourceDescription("Current application settings"),
mcp.WithMIMEType("application/json"),
),
handleAppConfig,
)
func handleAppConfig(ctx context.Context, req mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
config := map[string]interface{}{
"database_url": os.Getenv("DATABASE_URL"),
"debug_mode": os.Getenv("DEBUG") == "true",
"version": "1.0.0",
"features": []string{
"authentication",
"caching",
"logging",
},
}
configJSON, err := json.Marshal(config)
if err != nil {
return nil, err
}
return &mcp.ReadResourceResult{
Contents: []mcp.ResourceContent{
mcp.TextResourceContent{
URI: req.Params.URI,
MIMEType: "application/json",
Text: string(configJSON),
},
},
}, nil
}Dynamic Resources
Dynamic resources use URI templates with parameters, allowing for flexible, parameterized access to data.
URI Templates
Use {parameter} syntax for dynamic parts:
// User profile resource with dynamic user ID
s.AddResource(
mcp.NewResource(
"users://{user_id}",
"User Profile",
mcp.WithResourceDescription("User profile information"),
mcp.WithMIMEType("application/json"),
),
handleUserProfile,
)
func handleUserProfile(ctx context.Context, req mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
// Extract user_id from URI
userID := extractUserID(req.Params.URI) // "users://123" -> "123"
// Fetch user data (from database, API, etc.)
user, err := getUserFromDB(userID)
if err != nil {
return nil, fmt.Errorf("user not found: %w", err)
}
jsonData, err := json.Marshal(user)
if err != nil {
return nil, err
}
return []mcp.ResourceContents{
mcp.TextResourceContents{
URI: req.Params.URI,
MIMEType: "application/json",
Text: string(jsonData),
},
}, nil
}
func extractUserID(uri string) string {
// Extract ID from "users://123" format
parts := strings.Split(uri, "://")
if len(parts) == 2 {
return parts[1]
}
return ""
}Database Resources
Expose database records dynamically:
/* Detailed source-code truncated for AI context efficiency. */API Resources
Proxy external APIs through resources:
// Weather API resource
s.AddResource(
mcp.NewResource(
"weather://{location}",
"Weather Data",
mcp.WithResourceDescription("Current weather for a location"),
mcp.WithMIMEType("application/json"),
),
handleWeatherData,
)
func handleWeatherData(ctx context.Context, req mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
location := extractLocation(req.Params.URI)
// Call external weather API
apiURL := fmt.Sprintf("https://api.weather.com/v1/current?location=%s&key=%s",
url.QueryEscape(location), os.Getenv("WEATHER_API_KEY"))
resp, err := http.Get(apiURL)
if err != nil {
return nil, fmt.Errorf("weather API error: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
return &mcp.ReadResourceResult{
Contents: []mcp.ResourceContent{
{
URI: req.Params.URI,
MIMEType: "application/json",
Text: string(body),
},
},
}, nil
}Content Types
Resources can serve different types of content with appropriate MIME types.
Text Content
func handleTextResource(ctx context.Context, req mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
content := "This is plain text content"
return &mcp.ReadResourceResult{
Contents: []mcp.ResourceContent{
{
URI: req.Params.URI,
MIMEType: "text/plain",
Text: content,
},
},
}, nil
}JSON Content
func handleJSONResource(ctx context.Context, req mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
data := map[string]interface{}{
"message": "Hello, World!",
"timestamp": time.Now().Unix(),
"status": "success",
}
jsonData, err := json.Marshal(data)
if err != nil {
return nil, err
}
return []mcp.ResourceContents{
mcp.TextResourceContents{
URI: req.Params.URI,
MIMEType: "application/json",
Text: string(jsonData),
},
}, nil
}Binary Content
func handleImageResource(ctx context.Context, req mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
imageData, err := os.ReadFile("logo.png")
if err != nil {
return nil, err
}
// Encode binary data as base64
encoded := base64.StdEncoding.EncodeToString(imageData)
return &mcp.ReadResourceResult{
Contents: []mcp.ResourceContent{
{
URI: req.Params.URI,
MIMEType: "image/png",
Blob: encoded,
},
},
}, nil
}Multiple Content Types
A single resource can return multiple content representations:
func handleMultiFormatResource(ctx context.Context, req mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
data := map[string]interface{}{
"name": "John Doe",
"age": 30,
"city": "New York",
}
// JSON representation
jsonData, _ := json.Marshal(data)
// Text representation
textData := fmt.Sprintf("Name: %s\nAge: %d\nCity: %s",
data["name"], data["age"], data["city"])
return &mcp.ReadResourceResult{
Contents: []mcp.ResourceContent{
{
URI: req.Params.URI,
MIMEType: "application/json",
Text: string(jsonData),
},
{
URI: req.Params.URI,
MIMEType: "text/plain",
Text: textData,
},
},
}, nil
}Error Handling
Proper error handling ensures robust resource access:
Common Error Patterns
func handleResourceWithErrors(ctx context.Context, req mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
// Validate URI format
if !isValidURI(req.Params.URI) {
return nil, fmt.Errorf("invalid URI format: %s", req.Params.URI)
}
// Check permissions
if !hasPermission(ctx, req.Params.URI) {
return nil, fmt.Errorf("access denied to resource: %s", req.Params.URI)
}
// Handle resource not found
data, err := fetchResourceData(req.Params.URI)
if err != nil {
if errors.Is(err, ErrResourceNotFound) {
return nil, fmt.Errorf("resource not found: %s", req.Params.URI)
}
return nil, fmt.Errorf("failed to fetch resource: %w", err)
}
jsonData, err := json.Marshal(data)
if err != nil {
return nil, err
}
return &mcp.ReadResourceResult{
Contents: []mcp.ResourceContents{
mcp.TextResourceContents{
URI: req.Params.URI,
MIMEType: "application/json",
Text: string(jsonData),
},
},
}, nil
}Timeout Handling
func handleResourceWithTimeout(ctx context.Context, req mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
// Create timeout context
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
// Use context in operations
data, err := fetchDataWithContext(ctx, req.Params.URI)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
return nil, fmt.Errorf("resource fetch timeout: %s", req.Params.URI)
}
return nil, err
}
jsonData, err := json.Marshal(data)
if err != nil {
return nil, err
}
return &mcp.ReadResourceResult{
Contents: []mcp.ResourceContent{
mcp.TextResourceContent{
URI: req.Params.URI,
MIMEType: "application/json",
Text: string(jsonData),
},
},
}, nil
}
}Resource Listing
Implement resource discovery for clients:
func main() {
s := server.NewMCPServer("Resource Server", "1.0.0",
server.WithResourceCapabilities(true),
)
// Add multiple resources
resources := []struct {
uri string
name string
description string
mimeType string
handler server.ResourceHandler
}{
{"docs://readme", "README", "Project documentation", "text/markdown", handleReadme},
{"config://app", "App Config", "Application settings", "application/json", handleConfig},
{"users://{id}", "User Profile", "User information", "application/json", handleUser},
}
for _, r := range resources {
s.AddResource(
mcp.NewResource(r.uri, r.name,
mcp.WithResourceDescription(r.description),
mcp.WithMIMEType(r.mimeType),
),
r.handler,
)
}
server.ServeStdio(s)
}Caching Resources
Implement caching for expensive resources:
type CachedResourceHandler struct {
cache map[string]cacheEntry
mutex sync.RWMutex
ttl time.Duration
}
type cacheEntry struct {
data *mcp.ReadResourceResult
timestamp time.Time
}
func (h *CachedResourceHandler) HandleResource(ctx context.Context, req mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
h.mutex.RLock()
if entry, exists := h.cache[req.Params.URI]; exists {
if time.Since(entry.timestamp) < h.ttl {
h.mutex.RUnlock()
return entry.data, nil
}
}
h.mutex.RUnlock()
// Fetch fresh data
data, err := h.fetchFreshData(ctx, req)
if err != nil {
return nil, err
}
// Cache the result
h.mutex.Lock()
h.cache[req.Params.URI] = cacheEntry{
data: data,
timestamp: time.Now(),
}
h.mutex.Unlock()
return data, nil
}Resource Subscriptions
MCP lets clients subscribe to updates for individual resources via resources/subscribe, then cancel with resources/unsubscribe. The server later pushes notifications/resources/updated whenever a subscribed resource changes.
Enable the capability with the first argument to WithResourceCapabilities:
s := server.NewMCPServer("Resource Server", "1.0.0",
// (subscribe, listChanged)
server.WithResourceCapabilities(true, true),
)Once the subscribe capability is advertised, the server accepts resources/subscribe and resources/unsubscribe requests and acknowledges them with an empty result. If a client calls either method while the capability is disabled (or no resource capabilities are configured), the server responds with METHOD_NOT_FOUND.
Tracking subscriptions per session
The default handlers acknowledge the request but do not store any subscription state on their own. To remember which session is interested in which URI, implement the optional SessionWithResourceSubscriptions interface on your custom session type — the dispatcher will call into it automatically:
type mySession struct {
server.ClientSession
mu sync.Mutex
subs map[string]struct{}
}
func (s *mySession) SubscribeToResource(uri string) {
s.mu.Lock()
defer s.mu.Unlock()
if s.subs == nil {
s.subs = make(map[string]struct{})
}
s.subs[uri] = struct{}{}
}
func (s *mySession) UnsubscribeFromResource(uri string) {
s.mu.Lock()
defer s.mu.Unlock()
delete(s.subs, uri)
}
func (s *mySession) SubscribedResources() []string {
s.mu.Lock()
defer s.mu.Unlock()
out := make([]string, 0, len(s.subs))
for uri := range s.subs {
out = append(out, uri)
}
return out
}
func (s *mySession) IsSubscribedToResource(uri string) bool {
s.mu.Lock()
defer s.mu.Unlock()
_, ok := s.subs[uri]
return ok
}Duplicate subscribes and unsubscribes for unknown URIs are treated as no-ops, matching the spec.
Pushing `resources/updated` notifications
When a resource changes, deliver the update only to clients that asked for it. The simplest approach is to track sessions yourself via the OnRegisterSession / OnUnregisterSession hooks, then look up the ones subscribed to the URI you're updating and call SendNotificationToSpecificClient:
var (
sessionsMu sync.Mutex
sessions = map[string]server.ClientSession{}
)
hooks := &server.Hooks{}
hooks.AddOnRegisterSession(func(ctx context.Context, session server.ClientSession) {
sessionsMu.Lock()
defer sessionsMu.Unlock()
sessions[session.SessionID()] = session
})
hooks.AddOnUnregisterSession(func(ctx context.Context, session server.ClientSession) {
sessionsMu.Lock()
defer sessionsMu.Unlock()
delete(sessions, session.SessionID())
})
func notifyResourceUpdated(s *server.MCPServer, uri string) {
sessionsMu.Lock()
snapshot := make([]server.ClientSession, 0, len(sessions))
for _, sess := range sessions {
snapshot = append(snapshot, sess)
}
sessionsMu.Unlock()
for _, sess := range snapshot {
subs, ok := sess.(server.SessionWithResourceSubscriptions)
if !ok || !subs.IsSubscribedToResource(uri) {
continue
}
_ = s.SendNotificationToSpecificClient(
sess.SessionID(),
"notifications/resources/updated",
map[string]any{"uri": uri},
)
}
}Subscription hooks
If you only need to observe subscribe / unsubscribe traffic (for metrics, audit logging, or to drive an external pub/sub system) without maintaining per-session state, attach hooks:
hooks := &server.Hooks{}
hooks.AddBeforeSubscribe(func(ctx context.Context, id any, req *mcp.SubscribeRequest) {
log.Printf("subscribe: %s", req.Params.URI)
})
hooks.AddAfterUnsubscribe(func(ctx context.Context, id any, req *mcp.UnsubscribeRequest, _ *mcp.EmptyResult) {
log.Printf("unsubscribed: %s", req.Params.URI)
})
s := server.NewMCPServer("Resource Server", "1.0.0",
server.WithResourceCapabilities(true, true),
server.WithHooks(hooks),
)The matching AddAfterSubscribe and AddBeforeUnsubscribe hooks are also available. See Hooks for the broader hook contract.
Advanced Resource Patterns
Session-specific Resources
You can add resources to a specific client session, allowing different clients to see different resources or override global resources with session-specific implementations.
Using Helper Functions (Recommended)
The server provides convenient helper functions that mirror the session tool helpers:
/* Detailed source-code truncated for AI context efficiency. */Direct Interface Usage
You can also work directly with the SessionWithResources interface:
sseServer := server.NewSSEServer(
s,
server.WithAppendQueryToMessageEndpoint(),
server.WithSSEContextFunc(func(ctx context.Context, r *http.Request) context.Context {
withNewResources := r.URL.Query().Get("withNewResources")
if withNewResources != "1" {
return ctx
}
session := server.ClientSessionFromContext(ctx)
if sessionWithResources, ok := session.(server.SessionWithResources); ok {
// Add the new resources
sessionWithResources.SetSessionResources(map[string]server.ServerResource{
myNewResource.URI: {
Resource: myNewResource,
Handler: myNewResourceHandler,
},
})
}
return ctx
}),
)Important Notes
- Session resources override global resources with the same URI
- Notifications (
resources/list_changed) are automatically sent when resources are added/removed - The server automatically registers resource capabilities when session resources are first added
- Operations are thread-safe and can be called concurrently
- Resources are only available to initialized sessions unless explicitly added before initialization
Next Steps
- Tools - Learn to implement interactive functionality
- Prompts - Create reusable interaction templates
- Advanced Features - Explore hooks, middleware, and more
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. |
// 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:
- list — allow clients to list tasks
- cancel — allow clients to cancel running tasks
- toolCallTasks — enable task augmentation for tool calls
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:
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:
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:
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:
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:
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:
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:
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.
Related Task Metadata
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:
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:
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:
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:
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:
/* 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
// 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.
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:
Title(the top-level field set byWithToolTitle)Annotations.Title(legacy hint set byWithTitleAnnotation, see Tool Annotations)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:
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:
// 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
// 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:
// 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:
// 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:
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 ofstructuredContent matching the schema you advertised. To enforce that
contract at runtime, opt in to output schema validation when constructing
the server:
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:
// 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:
// 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:
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:
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:
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"tojsonschema:"text" - Required fields: The
jsonschema:"required"tag is no longer supported. Fields withoutomitemptyin theirjsontag are automatically required. - Validation constraints: Tags like
jsonschema:"enum=...",jsonschema:"minimum=..."are no longer supported in struct tags. Usemcp.WithRawInputSchema()for these constraints or define them with the builder API (mcp.WithString,mcp.WithNumber, etc.). - Additional properties:
additionalProperties: falseis now automatically included in generated schemas.
:::
Manual Structured Results
For more control over the response, use NewTypedToolHandler with manual result creation:
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:
/* 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:
// 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 anyBasic Handler Pattern
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
/* Detailed source-code truncated for AI context efficiency. */Database Query Tool
/* Detailed source-code truncated for AI context efficiency. */HTTP Request Tool
/* Detailed source-code truncated for AI context efficiency. */Argument Validation
Type-Safe Parameter Extraction
MCP-Go provides helper methods for safe parameter extraction:
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
// 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
}### 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
func handleTextTool(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
message := "Operation completed successfully"
return mcp.NewToolResultText(message), nil
}JSON Results
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
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
}Resource Links
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:
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
}Mixed Content with Resource Links
You can combine different content types including resource links in a single tool result:
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 Link with Annotations
Resource links can include additional metadata through annotations:
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
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:
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:
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:
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:
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.
Using Helper Functions (Recommended)
The server provides convenient helper functions for managing session tools:
// 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:
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
/* Detailed source-code truncated for AI context efficiency. */Advanced StreamableHTTP Configuration
/* Detailed source-code truncated for AI context efficiency. */Endpoints
Standard MCP Endpoints
When you start a StreamableHTTP MCP server, it automatically creates these endpoints:
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 capabilitiesCustom Endpoints
Add custom HTTP endpoints alongside MCP:
/* Detailed source-code truncated for AI context efficiency. */Request/Response Patterns
Standard MCP Request
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
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [
{
"type": "text",
"text": "{\"users\":[...],\"total\":25,\"limit\":10,\"offset\":0}"
}
]
}
}Error Response
{
"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:
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:
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
Stateless Design (Recommended)
// 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)
/* Detailed source-code truncated for AI context efficiency. */Authentication and Authorization
/* 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 anhttp.Handler via ServeHTTP.
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 aboveFor SSE servers, use the equivalent WithSSEProtectedResourceMetadata:
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:
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. Useserver.ProtectedResourceMetadataPath to derive the correct path:
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/mcpThe 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 with403 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 onlocalhostor0.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:
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.
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:
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:
CallToolRequestReadResourceRequestListToolsRequestListResourcesRequestInitializeRequest- 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:
// 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:
/* Detailed source-code truncated for AI context efficiency. */How It Works
- Persistent Connection: When
WithContinuousListening()is enabled, the client maintains a persistent SSE connection to the server - Bidirectional Communication: The server can send sampling requests through the SSE stream
- Response Channel: The client responds to sampling requests via HTTP POST to the same endpoint
- 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:
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 standardnet/httphandler 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 throughnet/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 workaroundfiberApp.Group("/mcp", adaptor.HTTPHandler(server.NewStreamableHTTPServer(s)))
is the simplest path and is fully supported.
:::
API Surface
// 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()returnstrue—Flush()MUST forward bytes to the
client immediately. POST responses can upgrade totext/event-stream
when notifications fire mid-flight, and GET (the SSE listening channel)
is accepted.CanStream()returnsfalse—Flush()SHOULD be a no-op. POST
responses stay as a single bufferedapplication/jsonreply (any
notifications produced during the request are dropped, since they cannot
be delivered without SSE). GET requests are rejected with405 Method Not Allowed.
Adapter Example: fasthttp
/* Detailed source-code truncated for AI context efficiency. */Behavior Notes
WithStreamableHTTPCORSis not applied when entering throughHandle. 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-loopbackHostheader,
answering403 Forbidden. See
DNS Rebinding Protection above. WithProtectedResourceMetadatais not applied when entering throughHandle. Mount the metadata route separately usingNewProtectedResourceMetadataHandler. See
OAuth Protected Resource Metadata
above.WithHTTPContextFuncis honored. When the request entered throughServeHTTPit receives the original*http.Request; when entered
throughHandleit receives a synthetic*http.Requestderived from
theHTTPRequest. Callers can also pre-decorater.Contextdirectly,
which is the recommended pattern for non-net/http integrations.SessionIdManagerResolverstill receives a*http.Requestfor
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
- In-Process Transport - Learn about embedded scenarios
- Client Development - Build MCP clients for HTTP transport
- Server Basics - Review fundamental server concepts
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
/* Detailed source-code truncated for AI context efficiency. */Embedded Application Integration
/* 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:
/* 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
/* Detailed source-code truncated for AI context efficiency. */Sampling Request Parameters
The CreateMessageRequest supports various parameters to control LLM behavior:
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:
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
- Client Development - Build MCP clients for all transports
- HTTP Transport - Learn about web-based scenarios
- Server Advanced Features - Explore production-ready features
- Client Sampling - Learn more about client-side sampling implementation
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
/* Detailed source-code truncated for AI context efficiency. */Advanced SSE Configuration
/* Detailed source-code truncated for AI context efficiency. */Configuration
SSE Server Options
The SSE server can be configured with various options:
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]) whoseHost header is not a localhost value are rejected with 403 Forbidden.
This is enabled by default and covers ServeHTTP as well as theSSEHandler()/MessageHandler() sub-handlers. Useserver.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:
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:
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:
// 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 capabilitiesRequest 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:
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:
CallToolRequestReadResourceRequestListToolsRequestListResourcesRequestInitializeRequest- 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
- HTTP Transport - Learn about traditional web service patterns
- In-Process Transport - Explore embedded scenarios
- Client Development - Build MCP clients for different transports
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
/* Detailed source-code truncated for AI context efficiency. */Advanced STDIO Server
/* Detailed source-code truncated for AI context efficiency. */Client Integration
How LLM Applications Connect
LLM applications typically connect to STDIO MCP servers by:
- Spawning the process: Starting your server as a subprocess
- Pipe communication: Using stdin/stdout for JSON-RPC messages
- Lifecycle management: Handling process startup, shutdown, and errors
Claude Desktop Integration
Configure your STDIO server in Claude Desktop:
{
"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
/* 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 useNewStdioMCPClientWithOptions 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:
c, err := client.NewStdioClient(
"go", nil, "run", "/path/to/server/main.go",
)With the options-aware version:
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:
# 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.goInteractive Testing Script
#!/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:
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:
# Install MCP Inspector
npm install @modelcontextprotocol/inspector
# Run your server with inspector
mcp-inspector go run main.goThis 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
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
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
// 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
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
- SSE Transport - Learn about real-time web communication
- HTTP Transport - Explore traditional web service patterns
- In-Process Transport - Understand embedded scenarios
--- METRICS ---
- Files Extracted: 38
- Estimated Token Budget: ~77736 tokens
- Canonical Reference: https://codewiki.google/github.com/mark3labs/mcp-go