Technical Documentation: modelcontextprotocol/go-sdk
ℹ️ Provenance: Multi-Tier Ingestion · CodeWiki Reference
Overview (`README.md`)
MCP Go SDK
This repository contains an implementation of the official Go software
development kit (SDK) for the Model Context Protocol (MCP).
Package / Feature documentation
The SDK consists of several importable packages:
- The
github.com/modelcontextprotocol/go-sdk/mcp
package defines the primary APIs for constructing and using MCP clients and
servers. - The
github.com/modelcontextprotocol/go-sdk/jsonrpcpackage is for users implementing
their own transports. - The
github.com/modelcontextprotocol/go-sdk/auth
package provides some primitives for supporting OAuth. - The
github.com/modelcontextprotocol/go-sdk/oauthex
package provides extensions to the OAuth protocol, such as ProtectedResourceMetadata.
The SDK endeavors to implement the full MCP spec. The docs/ directory
contains feature documentation, mapping the MCP spec to the packages above.
Version Compatibility
The following table shows which versions of the Go SDK support which versions of the MCP specification:
| SDK Version | Latest MCP Spec | All Supported MCP Specs |
|---|---|---|
| v1.7.0+ | 2026-07-28 | 2026-07-28, 2025-11-25*, 2025-06-18, 2025-03-26, 2024-11-05 |
| v1.4.0 - v1.6.1 | 2025-11-25* | 2025-11-25*, 2025-06-18, 2025-03-26, 2024-11-05 |
| v1.2.0 - v1.3.1 | 2025-11-25** | 2025-11-25**, 2025-06-18, 2025-03-26, 2024-11-05 |
| v1.0.0 - v1.1.0 | 2025-06-18 | 2025-06-18, 2025-03-26, 2024-11-05 |
* Client side OAuth has experimental support.
** Partial support for 2025-11-25 (client side OAuth and Sampling with tools not available).
The roots, sampling, and logging features are deprecated as of protocol version
2026-07-28 by
SEP-2577.
The SDK continues to support them for compatibility during the deprecation
window (at least twelve months). See the individual feature documentation for
migration guidance.
New releases of the SDK target only supported versions of Go. See
https://go.dev/doc/devel/release#policy for more information.
Getting started
To get started creating an MCP server, create an mcp.Server instance, add
features to it, and then run it over an mcp.Transport. For example, this
server adds a single simple tool, and then connects it to clients over
stdin/stdout:
package main
import (
"context"
"log"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
type Input struct {
Name string `json:"name" jsonschema:"the name of the person to greet"`
}
type Output struct {
Greeting string `json:"greeting" jsonschema:"the greeting to tell to the user"`
}
func SayHi(ctx context.Context, req *mcp.CallToolRequest, input Input) (
*mcp.CallToolResult,
Output,
error,
) {
return nil, Output{Greeting: "Hi " + input.Name}, nil
}
func main() {
// Create a server with a single tool.
server := mcp.NewServer(&mcp.Implementation{Name: "greeter", Version: "v1.0.0"}, nil)
mcp.AddTool(server, &mcp.Tool{Name: "greet", Description: "say hi"}, SayHi)
// Run the server over stdin/stdout, until the client disconnects.
if err := server.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
log.Fatal(err)
}
}To communicate with that server, create an mcp.Client and connect it to the
corresponding server, by running the server command and communicating over its
stdin/stdout:
package main
import (
"context"
"log"
"os/exec"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
func main() {
ctx := context.Background()
// Create a new client, with no features.
client := mcp.NewClient(&mcp.Implementation{Name: "mcp-client", Version: "v1.0.0"}, nil)
// Connect to a server over stdin/stdout.
transport := &mcp.CommandTransport{Command: exec.Command("myserver")}
session, err := client.Connect(ctx, transport, nil)
if err != nil {
log.Fatal(err)
}
defer session.Close()
// Call a tool on the server.
params := &mcp.CallToolParams{
Name: "greet",
Arguments: map[string]any{"name": "you"},
}
res, err := session.CallTool(ctx, params)
if err != nil {
log.Fatalf("CallTool failed: %v", err)
}
if res.IsError {
log.Fatal("tool failed")
}
for _, c := range res.Content {
log.Print(c.(*mcp.TextContent).Text)
}
}The examples/ directory contains more example clients and
servers.
Contributing
We welcome contributions to the SDK! Please see
CONTRIBUTING.md for details of how to contribute.
Acknowledgements / Alternatives
Several third party Go MCP SDKs inspired the development and design of this
official SDK, and continue to be viable alternatives, notably
mcp-go, originally authored by Ed Zynda.
We are grateful to Ed as well as the other contributors to mcp-go, and to
authors and contributors of other SDKs such as
mcp-golang and
go-mcp. Thanks to their work, there
is a thriving ecosystem of Go MCP clients and servers.
License
This project is licensed under Apache 2.0 for new contributions, with existing
code under MIT - see the LICENSE file for details.
Overview (`docs/README.md`)
Overview
The SDK consists of several importable packages:
- The
github.com/modelcontextprotocol/go-sdk/mcp
package defines the primary APIs for constructing and using MCP clients and
servers. - The
github.com/modelcontextprotocol/go-sdk/jsonrpcpackage is for users implementing
their own transports. - The
github.com/modelcontextprotocol/go-sdk/auth
package provides some primitives for supporting OAuth. - The
github.com/modelcontextprotocol/go-sdk/oauthex
package provides extensions to the OAuth protocol, such as ProtectedResourceMetadata.
These docs mirror the official MCP spec.
Use the index below to learn how the SDK implements a particular aspect of the
protocol.
Base Protocol
Client Features
Server Features
TroubleShooting
See troubleshooting.md for a troubleshooting guide.
Backwards compatibility
See mcpgodebug.md for a list of backwards incompatible behavior changes
and description how they can be temporarily undone.
Rough edges
See rough_edges.md for a list of rough edges or API
oversights that can't be addressed due to our compatibility promise. We'll
revisit these if/when we move to a v2 of the SDK.
Overview (`examples/http/README.md`)
MCP HTTP Example
This example demonstrates how to use the Model Context Protocol (MCP) over HTTP using the streamable transport. It includes both a server and client implementation.
Overview
The example implements:
- A server that provides a
cityTimetool - A client that connects to the server, lists available tools, and calls the
cityTimetool
Usage
Start the Server
go run . serverThis starts an MCP server on http://localhost:8000 (default) that provides a cityTime tool.
To run a client in another terminal:
go run . clientThe client will:
- Connect to the server
- List available tools
- Call the
cityTimetool for NYC, San Francisco, and Boston - Display the results
At any given time you can pass a custom URL to the program to run it on a custom host/port:
go run . -host 0.0.0.0 -port 9000 serverTesting with real-world MCP Clients
Once the server is started, assuming it's the default localhost:8080, you can try to add it to a popular MCP client:
claude mcp add -t http timezone http://localhost:8080Once added, Claude Code will be able to discover and use the cityTime tool provided by this server.
In Claude Code:
> what's the timezone
⏺ I'll get the current time in a major US city for you.
⏺ timezone - cityTime (MCP)(city: "nyc")
⎿ The current time in New York City is 7:30:16 PM EDT on Wedn
esday, July 23, 2025
⏺ The current timezone is EDT (Eastern Daylight Time), and it's
7:30 PM on Wednesday, July 23, 2025.
> what timezones do you support?
⏺ The timezone tool supports three US cities:
- NYC (Eastern Time)
- SF (Pacific Time)
- Boston (Eastern Time)Overview (`examples/server/auth-middleware/README.md`)
MCP Server with Auth Middleware
This example demonstrates how to integrate the Go MCP SDK's auth.RequireBearerToken middleware with an MCP server to provide authenticated access to MCP tools and resources.
Features
The server provides authentication and authorization capabilities for MCP tools:
1. Authentication Methods
- JWT Token Authentication: JSON Web Token-based authentication
- API Key Authentication: API key-based authentication
- Scope-based Access Control: Permission-based access to MCP tools
2. MCP Integration
- Authenticated MCP Tools: Tools that require authentication and check permissions
- Token Generation: Utility endpoints for generating test tokens
- Middleware Integration: Seamless integration with MCP server handlers
Setup
cd examples/server/auth-middleware
go mod tidy
go run main.goTesting
# Run all tests
go test -v
# Run benchmark tests
go test -bench=.
# Generate coverage report
go test -coverEndpoints
Public Endpoints (No Authentication Required)
GET /health- Health checkGET /.well-known/oauth-protected-resource- OAuth 2.0 Protected Resource Metadata (RFC 9728)
MCP Endpoints (Authentication Required)
POST /mcp/jwt- MCP server with JWT authenticationPOST /mcp/apikey- MCP server with API key authentication
Utility Endpoints
GET /generate-token- Generate JWT tokenPOST /generate-api-key- Generate API key
Available MCP Tools
The server provides three authenticated MCP tools:
1. Say Hi (`say_hi`)
A simple greeting tool that requires authentication.
Parameters:
- None required
Required Scopes:
- Any authenticated user
2. Get User Info (`get_user_info`)
Retrieves user information based on the provided user ID.
Parameters:
user_id(string): The user ID to get information for
Required Scopes:
readpermission
3. Create Resource (`create_resource`)
Creates a new resource with the provided details.
Parameters:
name(string): The name of the resourcedescription(string): The description of the resourcecontent(string): The content of the resource
Required Scopes:
writepermission
Example Usage
1. Generating JWT Token and Using MCP Tools
# Generate a token
curl 'http://localhost:8080/generate-token?user_id=alice&scopes=read,write'
# Use MCP tool with JWT authentication
curl -H 'Authorization: Bearer <generated_token>' \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"say_hi","arguments":{}}}' \
http://localhost:8080/mcp/jwt2. Generating API Key and Using MCP Tools
# Generate an API key
curl -X POST 'http://localhost:8080/generate-api-key?user_id=bob&scopes=read'
# Use MCP tool with API key authentication
curl -H 'Authorization: Bearer <generated_api_key>' \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_user_info","arguments":{"user_id":"test"}}}' \
http://localhost:8080/mcp/apikey3. Testing Scope Restrictions
# Access MCP tool requiring write scope
curl -H 'Authorization: Bearer <token_with_write_scope>' \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"create_resource","arguments":{"name":"test","description":"test resource","content":"test content"}}}' \
http://localhost:8080/mcp/jwtCore Concepts
Authentication Integration
This example demonstrates how to integrate auth.RequireBearerToken middleware with an MCP server to provide authenticated access. The MCP server operates as an HTTP handler protected by authentication middleware.
Key Features
- MCP Server Integration: Create MCP server using
mcp.NewServer - Authentication Middleware: Protect MCP handlers with
auth.RequireBearerToken - Token Verification: Validate tokens using provided
TokenVerifierfunctions - Scope Checking: Verify required permissions (scopes) are present
- Expiration Validation: Check that tokens haven't expired
- Context Injection: Add verified token information to request context
- Authenticated MCP Tools: Tools that operate based on authentication information
- Error Handling: Return appropriate HTTP status codes and error messages on authentication failure
Implementation
// Create MCP server
server := mcp.NewServer(&mcp.Implementation{Name: "authenticated-mcp-server"}, nil)
// Create authentication middleware
authMiddleware := auth.RequireBearerToken(verifier, &auth.RequireBearerTokenOptions{
Scopes: []string{"read", "write"},
})
// Create MCP handler
handler := mcp.NewStreamableHTTPHandler(func(r *http.Request) *mcp.Server {
return server
}, nil)
// Apply authentication middleware to MCP handler
authenticatedHandler := authMiddleware(customMiddleware(handler))Parameters
- verifier: Function to verify tokens (
TokenVerifiertype) - opts: Authentication options
Scopes: List of required permissionsResourceMetadataURL: OAuth 2.0 resource metadata URL
Error Responses
- 401 Unauthorized: Token is invalid, expired, or missing
- 403 Forbidden: Required scopes are insufficient
- WWW-Authenticate Header: Included when resource metadata URL is configured
Implementation Details
1. TokenVerifier Implementation
func jwtVerifier(ctx context.Context, tokenString string) (*auth.TokenInfo, error) {
// JWT token verification logic
// On success: Return TokenInfo
// On failure: Return auth.ErrInvalidToken
}2. Using Authentication Information in MCP Tools
// Get authentication information in MCP tool
func MyTool(ctx context.Context, req *mcp.CallToolRequest, args MyArgs) (*mcp.CallToolResult, any, error) {
// Extract authentication info from request
userInfo := req.Extra.TokenInfo
// Check scopes
if !slices.Contains(userInfo.Scopes, "read") {
return nil, nil, fmt.Errorf("insufficient permissions: read scope required")
}
// Execute tool logic
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{Text: "Tool executed successfully"},
},
}, nil, nil
}3. Middleware Composition
// Combine authentication middleware with custom middleware
authenticatedHandler := authMiddleware(customMiddleware(mcpHandler))Security Best Practices
- Environment Variables: Use environment variables for JWT secrets in production
- Database Storage: Store API keys in a database
- HTTPS Usage: Always use HTTPS in production environments
- Token Expiration: Set appropriate token expiration times
- Principle of Least Privilege: Grant only the minimum required scopes
CORS Support
The OAuth metadata endpoint (/.well-known/oauth-protected-resource) supports CORS to enable
cross-origin client discovery. It sets Access-Control-Allow-Origin: * by default because
OAuth metadata is public information meant for client discovery (RFC 9728 §3.1).
Custom CORS Policies
For more sophisticated CORS requirements (origin validation, credentials, etc.), wrap the handler
with a CORS middleware library:
Using github.com/rs/cors:
import "github.com/rs/cors"
c := cors.New(cors.Options{
AllowedOrigins: []string{"https://example.com"},
AllowedMethods: []string{"GET", "OPTIONS"},
})
http.Handle("/.well-known/oauth-protected-resource",
c.Handler(auth.ProtectedResourceMetadataHandler(metadata)))Using github.com/jub0bs/cors:
import "github.com/jub0bs/cors"
corsMiddleware, err := cors.NewMiddleware(cors.Config{
Origins: []string{"https://example.com"},
Methods: []string{"GET", "OPTIONS"},
})
http.Handle("/.well-known/oauth-protected-resource",
corsMiddleware.Wrap(auth.ProtectedResourceMetadataHandler(metadata)))Use Cases
Ideal for:
- MCP servers requiring authentication and authorization
- Applications needing scope-based access control
- Systems requiring both JWT and API key authentication
- Projects needing secure MCP tool access
- Scenarios requiring audit trails and permission management
Examples:
- Enterprise MCP servers with user management
- Multi-tenant MCP applications
- Secure API gateways with MCP integration
- Development environments with authentication requirements
- Production systems requiring fine-grained access control
Overview (`examples/server/proxy/README.md`)
Proxy Header Propagation Example
This example demonstrates how to propagate HTTP headers from an incoming MCP server request to an outgoing MCP client request.
This is particularly useful when building a gateway or proxy server that needs to forward authentication tokens, trace IDs, or other context-sensitive headers to downstream services.
Architecture
This example runs three components in a single process:
Backend Server (Port 8082):
- Exposes a tool
echo_headersthat returns the headers it received.
- Exposes a tool
Proxy Server (Port 8081):
- Exposes a tool
forward_headers. - Acts as a client to the Backend Server.
- Uses a custom
http.RoundTripper(HeaderForwardingTransport) to inject headers from the context into outgoing requests.
- Exposes a tool
Client:
- Connects to the Proxy Server.
- Calls
forward_headers.
How it works
- The Client calls
forward_headerson the Proxy Server. - The Proxy Server receives the request. The request context contains the HTTP headers in
req.Extra.Header. - The Proxy's tool handler extracts these headers and places them into a new
context.Contextusing a specific key (headerContextKey). - The Proxy uses an
mcp.Clientconfigured with a customHTTPClientthat usesHeaderForwardingTransport. HeaderForwardingTransport.RoundTripinspects the context of outgoing requests. If it finds headers underheaderContextKey, it adds them to the HTTP request.- The Backend Server receives the request with the propagated headers.
Running the Example
Run the example with:
go run main.goYou should see output indicating:
- Backend and Proxy servers starting.
- Gateway receiving headers.
- Client receiving the result, which contains the echoed headers.
Example output:
2025/08/29 10:00:00 Starting Backend Server on :8082
2025/08/29 10:00:00 Starting Gateway Server on :8081
2025/08/29 10:00:00 Gateway received headers: map[...]
2025/08/29 10:00:00 Client received result: ...Overview (`examples/server/sequentialthinking/README.md`)
Sequential Thinking MCP Server
This example shows a Model Context Protocol (MCP) server that enables dynamic and reflective problem-solving through structured thinking processes. It helps break down complex problems into manageable, sequential thought steps with support for revision and branching.
Features
The server provides three main tools for managing thinking sessions:
1. Start Thinking (`start_thinking`)
Begins a new sequential thinking session for a complex problem.
Parameters:
problem(string): The problem or question to think aboutsessionId(string, optional): Custom session identifierestimatedSteps(int, optional): Initial estimate of thinking steps needed
2. Continue Thinking (`continue_thinking`)
Adds the next thought step, revises previous steps, or creates alternative branches.
Parameters:
sessionId(string): The thinking session to continuethought(string): The current thought or analysisnextNeeded(bool, optional): Whether another thinking step is neededreviseStep(int, optional): Step number to revise (1-based)createBranch(bool, optional): Create an alternative reasoning pathestimatedTotal(int, optional): Update total estimated steps
3. Review Thinking (`review_thinking`)
Provides a complete review of the thinking process for a session.
Parameters:
sessionId(string): The session to review
Resources
Thinking History (`thinking://sessions` or `thinking://{sessionId}`)
Access thinking session data and history in JSON format.
thinking://sessions- List all thinking sessionsthinking://{sessionId}- Get specific session details
Core Concepts
Sequential Processing
Problems are broken down into numbered thought steps that build upon each other, maintaining context and allowing for systematic analysis.
Dynamic Revision
Any previous thought step can be revised and updated, with the system tracking which thoughts have been modified.
Alternative Branching
Create alternative reasoning paths to explore different approaches to the same problem, allowing for comparative analysis.
Adaptive Planning
The estimated number of thinking steps can be adjusted dynamically as understanding of the problem evolves.
Running the Server
Standard I/O Mode
go run .HTTP Mode
go run . -http :8080Example Usage
Starting a Thinking Session
{
"method": "tools/call",
"params": {
"name": "start_thinking",
"arguments": {
"problem": "How should I design a scalable microservices architecture?",
"sessionId": "architecture_design",
"estimatedSteps": 8
}
}
}Adding Sequential Thoughts
{
"method": "tools/call",
"params": {
"name": "continue_thinking",
"arguments": {
"sessionId": "architecture_design",
"thought": "First, I need to identify the core business domains and their boundaries to determine service decomposition."
}
}
}Revising a Previous Step
{
"method": "tools/call",
"params": {
"name": "continue_thinking",
"arguments": {
"sessionId": "architecture_design",
"thought": "Actually, before identifying domains, I should analyze the current system's pain points and requirements.",
"reviseStep": 1
}
}
}Creating an Alternative Branch
{
"method": "tools/call",
"params": {
"name": "continue_thinking",
"arguments": {
"sessionId": "architecture_design",
"thought": "Alternative approach: Start with a monolith-first strategy and extract services gradually.",
"createBranch": true
}
}
}Completing the Thinking Process
{
"method": "tools/call",
"params": {
"name": "continue_thinking",
"arguments": {
"sessionId": "architecture_design",
"thought": "Based on this analysis, I recommend starting with 3 core services: User Management, Order Processing, and Inventory Management.",
"nextNeeded": false
}
}
}Reviewing the Complete Process
{
"method": "tools/call",
"params": {
"name": "review_thinking",
"arguments": {
"sessionId": "architecture_design"
}
}
}Session State Management
Each thinking session maintains:
- Session metadata: ID, problem statement, creation time, current status
- Thought sequence: Ordered list of thoughts with timestamps and revision history
- Progress tracking: Current step and estimated total steps
- Branch relationships: Links to alternative reasoning paths
- Status management: Active, completed, or paused sessions
Use Cases
Ideal for:
- Complex problem analysis requiring step-by-step breakdown
- Design decisions needing systematic evaluation
- Scenarios where initial scope is unclear and may evolve
- Problems requiring alternative approach exploration
- Situations needing detailed reasoning documentation
Examples:
- Software architecture design
- Research methodology planning
- Strategic business decisions
- Technical troubleshooting
- Creative problem solving
- Academic research planning
Client (`docs/client.md`)
Support for MCP client features
Roots
Note: The roots feature is deprecated as of protocol version 2026-07-28
(SEP-2577).
It remains fully functional during the deprecation window (at least twelve
months). The SDK continues to support roots for compatibility. New code
should pass paths via tool parameters, resource URIs, or configuration
instead.
MCP allows clients to specify a set of filesystem
"roots".
The SDK supports this as follows:
Client-side: The SDK client always has the roots.listChanged capability.
To add roots to a client, use theClient.AddRoots
andClient.RemoveRoots
methods. If any servers are already connected to the
client, a call to AddRoot or RemoveRoots will result in anotifications/roots/list_changed notification to each connected server.
Server-side: To query roots from the server, use theServerSession.ListRoots
method. To receive notifications about root changes, setServerOptions.RootsListChangedHandler.
For protocol versions 2026-07-28 and later, ListRoots requests are
delivered via the
Multi Round-Trip Requests
pattern.
/* Detailed source-code truncated for AI context efficiency. */Roots list changed
Client.AddRoots and Client.RemoveRoots notify every connected server that
the list changed. Servers observe this throughServerOptions.RootsListChangedHandler;
as with the server-side list-changed notifications, it reports only that
something changed, so read the list back with ServerSession.ListRoots.
func Example_rootsListChanged() {
ctx := context.Background()
changed := make(chan struct{}, 2)
s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, &mcp.ServerOptions{
RootsListChangedHandler: func(context.Context, *mcp.RootsListChangedRequest) {
changed <- struct{}{}
},
})
c := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, nil)
c.AddRoots(&mcp.Root{URI: "file:///project"})
t1, t2 := mcp.NewInMemoryTransports()
ss, err := s.Connect(ctx, t1, nil)
if err != nil {
log.Fatal(err)
}
defer ss.Close()
// ListRoots is a server-initiated request, so this session negotiates a
// protocol version that still allows one.
cs, err := c.Connect(ctx, t2, &mcp.ClientSessionOptions{ProtocolVersion: "2025-11-25"})
if err != nil {
log.Fatal(err)
}
defer cs.Close()
// Roots added after the client connects notify every connected server.
c.AddRoots(&mcp.Root{URI: "file:///scratch"})
<-changed
// The notification says only that the list changed, so read it back.
res, err := ss.ListRoots(ctx, nil)
if err != nil {
log.Fatal(err)
}
for _, root := range res.Roots {
fmt.Println(root.URI)
}
c.RemoveRoots("file:///scratch")
<-changed
fmt.Println("roots changed again")
// Output:
// file:///project
// file:///scratch
// roots changed again
}Sampling
Note: The sampling feature is deprecated as of protocol version
2026-07-28
(SEP-2577).
It remains fully functional during the deprecation window (at least twelve
months). The SDK continues to support sampling for compatibility. Servers
that need LLM completions should call LLM provider APIs directly.
Sampling
is a way for servers to leverage the client's AI capabilities. It is
implemented in the SDK as follows:
Client-side: To add the sampling capability to a client, setClientOptions.CreateMessageHandler.
This function is invoked whenever the server requests sampling.
Server-side: To use sampling from the server, callServerSession.CreateMessage.
For protocol versions 2026-07-28 and later, sampling requests are
delivered via the
Multi Round-Trip Requests
pattern.
/* Detailed source-code truncated for AI context efficiency. */Elicitation
Elicitation
allows servers to request user inputs. It is implemented in the SDK as follows:
Client-side: To add the elicitation capability to a client, setClientOptions.ElicitationHandler.
The elicitation handler must return a result that matches the requested schema;
otherwise, elicitation returns an error. If your handler supports URL mode
elicitation,
you must declare that capability explicitly (see Capabilities)
Server-side: To use elicitation from the server, callServerSession.Elicit.
For protocol versions 2026-07-28 and later, elicitation requests are
delivered via the
Multi Round-Trip Requests
pattern.
/* Detailed source-code truncated for AI context efficiency. */Schema defaults and enums
ElicitParams.RequestedSchema is a flat schema of primitive fields, which the
client renders as a form. Two field keywords shape that form.
A Default (SEP-1034) prefills a
field. When the user accepts without supplying it, the SDK fills the field in
from the schema before the result reaches either side's caller — the client
does so after its elicitation handler returns, and ServerSession.Elicit does
so again on receipt. This is unconditional; there is no opt-in flag. Marking a
defaulted field Required defeats it: accepted content is validated against
the schema before defaults are applied, so an answer that omits the field is
rejected rather than defaulted.
An Enum (SEP-1330) restricts a
field to a fixed set of values, which the client renders as a choice. Enums
are supported only on "string" fields; declaring one on another type is
rejected. To label the choices, set the legacy enumNames keyword throughSchema.Extra, with exactly one name per enum value — a mismatched length is
rejected.
/* Detailed source-code truncated for AI context efficiency. */Completing a URL elicitation
In URL mode the user finishes out of band, in a browser, so nothing in the
elicitation result tells the client when they are done. The server signals that
withServerSession.NotifyElicitationComplete,
passing the same ElicitationID the request carried; the client observes it
throughClientOptions.ElicitationCompleteHandler.
Send it from whatever endpoint the hosted flow redirects back to.
The notification matters most when a handler rejects a request withURLElicitationRequiredError:
the client parks the original request until a notification names thatElicitationID, and then retries it automatically. Until one arrives, the
client waits.
/* Detailed source-code truncated for AI context efficiency. */Multi Round-Trip Requests
SEP-2322
introduces the MRTR pattern: server-to-client requests for sampling,
elicitation, and roots are no longer issued as fresh JSON-RPC requests but
are carried inside the in-flight reply of a tools/call, prompts/get, orresources/read. The client must respond by retrying the original request
with the produced responses.
The SDK installs clientMultiRoundTripMiddleware for every client by
default. The middleware:
- Inspects each
tools/call/prompts/get/resources/readreply. - If the result's
NeedsInput()is true, fans out theInputRequestsmap
concurrently, calling the configured handler for each (elicit,createMessage/createMessageWithTools, orlistRoots). - Threads the server-supplied opaque
RequestStateback unchanged. - Retries the original request with the responses set, repeating until the
result no longer needs input.
The middleware is enabled by default. To opt out, setClientOptions.MultiRoundTrip.Disabled = true;
the client will then surface input-required results directly to the caller
(the returned CallToolResult, GetPromptResult, or ReadResourceResult
will report NeedsInput() == true and expose the server's InputRequests
and opaque RequestState). Your code must fulfil each request and re-issue
the original call with InputResponses set and RequestState echoed
back.
For legacy (<= 2025-11-25) servers, the SDK transparently sends server
requests on the legacy server-initiated channel; the MRTR machinery is a
no-op in that direction. For legacy clients talking to MRTR-style servers,
the server SDK applies the inverse compatibility shim — see the
server-side documentation.
Capabilities
Client capabilities are advertised to servers during the initialization
handshake. By default, the SDK advertises the logging
capability. Additional capabilities are automatically added when server
features are added (e.g. via AddTool), or when handlers are set in theServerOptions struct (e.g., setting CompletionHandler adds thecompletions capability), or may be configured explicitly.
Capability inference
When handlers are set on ClientOptions (e.g., CreateMessageHandler orElicitationHandler), the corresponding capability is automatically added if
not already present, with a default configuration.
For elicitation, if the handler is set but no Capabilities.Elicitation is
specified, the client defaults to form elicitation. To enable URL elicitation
or both modes, configure Capabilities.Elicitation
explicitly.
See the ClientCapabilities
documentation
for further details on inference.
Explicit capabilities
To explicitly declare capabilities, or to override the default inferred
capability, setClientOptions.Capabilities.
This sets the initial client capabilities, before any capabilities are added
based on configured handlers. If a capability is already present inCapabilities, adding a handler will not change its configuration.
This allows you to:
- Disable default capabilities: Pass an empty
&ClientCapabilities{}to
disable all defaults, including roots. - Disable listChanged notifications: Set
ListChanged: falseon a
capability to prevent the client from sending list-changed notifications
when roots are added or removed. - Configure elicitation modes: Specify which elicitation modes (form, URL)
the client supports.
// Configure elicitation modes and disable roots.
client := mcp.NewClient(impl, &mcp.ClientOptions{
Capabilities: &mcp.ClientCapabilities{
Elicitation: &mcp.ElicitationCapabilities{
Form: &mcp.FormElicitationCapabilities{},
URL: &mcp.URLElicitationCapabilities{},
},
},
ElicitationHandler: handler,
})Extensions
SEP-2133
adds an extensions map to ClientCapabilities and ServerCapabilities so
that optional capabilities outside the core protocol can be declared on the
wire. Keys are namespaced as "{vendor-prefix}/{extension-name}"; values
are per-extension settings objects.
Mcpgodebug (`docs/mcpgodebug.md`)
Backwards compatibility and MCPGODEBUG
According to our compatibility promise, we can't break backward compatibility
of the SDK API. However, sometimes we need to change the behavior of the SDK
in a backward-incompatible way in order to fix bugs or security issues.
In those cases we introduce temporary compatibility parameters, that can be
used to opt-out of the new behavior. They are usually maintained for two
minor release cycles and then removed.
The compatibility parameters are provided via the MCPGODEBUG environment
variable. The value of the variable is a comma-separated list of parameter
value assignments, e.g.:
MCPGODEBUG=parameter1=value1,parameter2=value2`MCPGODEBUG` history
1.9.0
Options listed below were added and will be removed in the 1.11.0 version of the SDK.
structuredcontentfloat64added. If set to1, numbers inCallToolResult.StructuredContentare decoded asfloat64, restoring the
previous behavior. The default behavior decodes numbers asjson.Numberso
that large integers and other exact JSON values do not silently lose
precision.disablepromptargsvalidationadded. If set to1,Server.getPromptwill
call the prompt handler even when a required argument of the prompt is
missing from the request, restoring the previous behavior. The default
behavior was changed to reject such requests with-32602(Invalid Params),
as the specification asks.allowmissingclientparamsadded. If set to1, aClientacceptselicitation/create,notifications/elicitation/completeandnotifications/resources/updatedmessages whoseparamsmember is missing
ornull, restoring the previous behavior (the handler then observes nil
params, which crashed the built-in elicitation handlers). The default
behavior was changed to reject such messages as invalid requests, as the
specification listsparamsas required for all three.nopreinitinvalidrequestadded. If set to1, a legacy server session that
has not been initialized refuses a request other thaninitializeorping
with JSON-RPC error code0, restoring the previous behavior. The default
behavior was changed to refuse it with-32600(Invalid Request):0
tells a client nothing and is indistinguishable from an unset code. See
issue #1367.nointernalerrorcodeadded. If set to1, an error returned by a request
handler that carries no JSON-RPC error code is sent with code0, restoring
the previous behavior. The default behavior was changed to send it with-32603(Internal error):0tells the peer nothing and is
indistinguishable from an unset code. The error's message is unchanged.
See issue #1394.fatalnewprotocolerrorsadded. If set to1, a streamable HTTP client
permanently fails its connection when an independent2026-07-28request
fails, restoring the previous behavior. By default, the failure is returned
only to the affected call.
1.8.0
Options listed below were added and will be removed in the 1.9.0 version of the SDK.
plaintextstatefulrejectionadded. If set to1, a statefulStreamableHTTPHandlerwill respond with a plain-texthttp.Error400 body
when it receives a request carrying per-request metadata (i.e. anio.modelcontextprotocol/protocolVersion_metafield, or anMCP-Protocol-Versionheader >=2026-07-28), restoring the previous
behavior. The default behavior was changed so that the server responds with a
JSON-RPC error of codeCodeUnsupportedProtocolVersion(-32022) carrying
anUnsupportedProtocolVersionDatapayload that advertises the legacy
versions the server supports. This lets the client's
existing renegotiation logic recover and prevents the failure from tearing
down the underlying connection.blockingcancelnotifyadded. If set to1, a cancelled call waits
synchronously for the best-effortnotifications/cancelledmessage to be
delivered (up tonotifyCancellationTimeout, currently 5 s) before
returning to the caller, restoring the previous behavior. The delivery
error is joined into the caller's returned error. The default behavior was
changed so that the call is retired immediately and the notification is
sent asynchronously off the caller's return path: the caller returns as
soon as its context is cancelled and cannot be delayed by a slow or
unresponsive peer. See issue #1150.
Options below were removed, according to plan:
seterroroverwrite.SetErrornow always preserves existingContent.enableoriginverification. A nilStreamableHTTPOptions.CrossOriginProtectionnow always means no
cross-origin protection. Wrap the handler with cross-origin protection
middleware instead.disablecontenttypecheck. Content-Type validation on HTTP POST requests can
no longer be disabled.disablelocalhostprotection, whose removal had been postponed to this
release. Set theDisableLocalhostProtectionfield inStreamableHTTPOptionsorSSEOptionsto opt out of DNS rebinding
protection.
1.7.0
Options listed below were added and will be removed in the 1.9.0 version of the SDK.
customresnotfounderrcodeadded. If set to1,ResourceNotFoundErrorwill
use the custom error code-32002instead of the standard-32602(Invalid
Params), restoring the previous behavior. The default behavior was changed to
align with SEP-2164 and the JSON-RPC specification.hintomitemptyadded. If set to1,ToolAnnotationsJSON marshaling
will omitReadOnlyHintandIdempotentHintwhen their value isfalse,
restoring the previous behavior. The default behavior was changed to always
serialize these fields, since their Go types are barebool(not*bool)
and omittingfalsemade it indistinguishable from unset.allowsessionsinstatelessadded. If set to1, stateless streamable HTTP
servers will read theMcp-Session-Idrequest header (or generate one viaGetSessionID), set it on response headers, and acceptDELETErequests,
restoring the previous behavior. The default behavior was changed so that
stateless servers ignore session IDs entirely and rejectDELETEwith 405.nomethodnotfoundcodeinerroradded. If set to1, the jsonrpc2 layer will not
include the MethodNotFound Error (-32601) in the error response when the
requested method in STDIO transport is not found. The default behavior was
changed to include the MethodNotFound Error in the error response when the
requested method in STDIO transport is not found.noprotocolerrorbodyadded. If set to1, the streamable HTTP client will
not attempt to decode the JSON-RPC error body of a non-2xx HTTP response,
and any non-transient error will permanently fail the connection, restoring
the previous behavior. The default behavior was changed so that the client
attempts to decode the JSON-RPC error body of a non-2xx response, surfaces
the underlying JSON-RPC error, and wraps it withjsonrpc2.ErrRejectedso
that per-call rejections do not tear down the session.nowrapinvalidparamsadded. If set to1, the server will not wrap
params-decoding failures withjsonrpc2.ErrInvalidParams, so wire responses
carry the zero-value error code0instead of-32602("invalid params"),
restoring the previous behavior. The default behavior was changed so that
the server always reports params-decoding failures with the standard-32602code.disablecompleteparamsvalidationadded. If set to1,Server.complete
will not validate that the requiredrefandargument.namefields onCompleteParamsare present, restoring the previous behavior of dispatching
the request to the completion handler unconditionally. The default behavior
was changed to reject malformed requests with-32602(Invalid Params).
1.6.1
Options listed below were added and will be removed in the 1.8.0 version of the SDK.
disablecontenttypecheckadded. If set to1, Content-Type validation on
HTTP POST requests will be disabled, allowing requests with non-JSON or missing
Content-Type headers. The default behavior is to validate that HTTP POST
requests have Content-Type: application/json.
1.6.0
Options listed below were added and will be removed in the 1.8.0 version of the SDK.
seterroroverwriteadded. If set to1,SetErrorwill always overwriteContentwith the error text, restoring the previous behavior. The default
behavior was changed to preserve existingContentif it has already been
populated.enableoriginverificationadded. If set to1, default (zero-value)
cross-origin protection will be applied whenStreamableHTTPOptions.CrossOriginProtectionis nil, restoring the
behavior from v1.4.1-v1.5.0. The default behavior was changed to not
enable cross-origin protection.disablelocalhostprotectionremoval was postponed until 1.8.0, as it is now
also used in the SSE transport.
Options below were removed:
jsonescaping, according to plan,disablecrossoriginprotection, it was replaced byenableoriginverificationafter the default was changed to not enable
cross-origin protection.
1.4.1
Options listed below will be removed in the 1.6.0 version of the SDK.
disablecrossoriginprotectionadded. If set to1, newly added cross-origin
protection will be disabled. The default behavior was changed to enable
cross-origin protection.
1.4.0
Options listed below will be removed in the 1.6.0 version of the SDK.
jsonescapingadded. If set to1, JSON marshaling will preserve the previous
behavior of escaping HTML characters in JSON strings. The default behavior
was changed to not escape HTML characters, to be consistent with other SDKs.disablelocalhostprotectionadded. If set to1, newly added DNS rebinding
protection will be disabled. The default behavior was changed to enable DNS rebinding
protection. The protection can also be disabled by setting theDisableLocalhostProtectionfield in theStreamableHTTPOptionsorSSEOptionsstruct totrue, which is the recommended way to disable
the protection long term. Removal of this option was postponed until 1.8.0.
Protocol (`docs/protocol.md`)
Support for the MCP base protocol
Lifecycle
The SDK provides an API for defining both MCP clients and servers, and
connecting them over various transports. When a client and server are
connected, it creates a logical session.
The MCP specification defines two lifecycle models, and the SDK supports both
transparently based on the negotiated protocol version:
- The legacy
initializehandshake
(lifecycle)
used by protocol versions through2025-11-25. - A stateless model introduced in
2026-07-28by
SEP-2575,
in which there is noinitialize/notifications/initializedhandshake, and
each request carries its protocol version and client capabilities in_meta. Clients SHOULD also include their identity (clientInfo) on every
request, and servers SHOULD include their identity (serverInfo) on every
response.
In both models, the SDK exposes the same API:
- A
Clientis a logical MCP client, configured with variousClientOptions. - When a client is connected to a server using
Client.Connect,
it creates aClientSession.
This session is initialized during theConnectmethod, and provides methods
to communicate with the server peer. - A
Serveris a logical MCP server, configured with variousServerOptions. - When a server is connected to a client using
Server.Connect,
it creates aServerSession.
In the legacy model, the server session is not considered initialized until
the client sends the notifications/initialized message. UseServerOptions.InitializedHandler to listen for this event, or just use the
session through various feature handlers (such as a ToolHandler). Requests
to the server are rejected until the client has initialized the session.
In the stateless model (2026-07-28+), there is no handshake. The server
processes the first request the moment it arrives, validates the per-request_meta fields.
Both ClientSession and ServerSession have a Close method to terminate the
session, and a Wait method to await session termination by the peer. Typically,
it is the client's responsibility to end the session.
func Example_lifecycle() {
ctx := context.Background()
// Create a client and server. Under protocol version 2026-07-28, there
// is no dedicated initialize/initialized handshake: the session is
// implicitly live the moment the client issues its first request.
client := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, nil)
server := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil)
// Connect the server and client using in-memory transports.
t1, t2 := mcp.NewInMemoryTransports()
serverSession, err := server.Connect(ctx, t1, nil)
if err != nil {
log.Fatal(err)
}
clientSession, err := client.Connect(ctx, t2, nil)
if err != nil {
log.Fatal(err)
}
// Now shut down the session by closing the client, and waiting for the
// server session to end.
if err := clientSession.Close(); err != nil {
log.Fatal(err)
}
if err := serverSession.Wait(); err != nil {
log.Fatal(err)
}
// Output:
}Discovery
Introduced in 2026-07-28 by
SEP-2575,
the server/discover RPC lets a client discover a server's supported
protocol versions, capabilities, and identity before issuing any other
request. Servers implementing 2026-07-28 MUST implement it.
- Server:
Server.Connectregisters theserver/discoverhandler automatically; the response is computed from the
server's static configuration (capabilities, server info, instructions) and
the transport-filtered list of supported protocol versions. - Client:
Client.Connectcallsserver/discoverfirst and
uses the result to negotiate a mutually supported version. If discovery
fails or the server does not support the latest version, the client falls back to the
legacyinitializehandshake.
A server advertises and negotiates every protocol version the SDK supports;
set ServerOptions.SupportedProtocolVersions
to narrow that set, for example to stop serving a revision your deployment has
retired.
server := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v1.0.0"}, &mcp.ServerOptions{
SupportedProtocolVersions: []string{"2026-07-28", "2025-11-25"},
})The set can only narrow support, never widen it; the field's documentation
covers how each protocol era answers a request at an excluded version.
Per-request metadata keys
When the negotiated protocol version is 2026-07-28 or later, every request
carries these keys inside its _meta map (constants live inmcp/protocol.go):
| Constant | Wire key | Type | Required |
|---|---|---|---|
MetaKeyProtocolVersion |
io.modelcontextprotocol/protocolVersion |
string |
Yes |
MetaKeyClientCapabilities |
io.modelcontextprotocol/clientCapabilities |
*ClientCapabilities |
Yes |
MetaKeyClientInfo |
io.modelcontextprotocol/clientInfo |
*Implementation |
No |
MetaKeyLogLevel |
io.modelcontextprotocol/logLevel |
LoggingLevel (deprecated by SEP-2577) |
No |
The client populates the required keys automatically on every outgoing
request, and populates clientInfo when configured with an *Implementation
(the default). Server-side handlers can read them throughServerRequest[P].ProtocolVersion(), ServerRequest[P].ClientInfo(), andServerRequest[P].ClientCapabilities().
Per-response metadata keys
Under the same protocol version, servers SHOULD identify themselves on every
response. The SDK populates this
key automatically on every outgoing response:
| Constant | Wire key | Type | Required |
|---|---|---|---|
MetaKeyServerInfo |
io.modelcontextprotocol/serverInfo |
*Implementation |
No |
Subscriptions
Introduced in 2026-07-28 by
SEP-2575,subscriptions/listen replaces the legacy resources/subscribe RPC and
the GET-based SSE endpoint with a single long-lived request that
multiplexes every kind of server-to-client change notification a client
opts in to. On the wire the client sends one subscriptions/listen
request whose notifications field enumerates what it wants
(toolsListChanged, promptsListChanged, resourcesListChanged, and/or
a list of resource URIs in resourceSubscriptions); the server replies
first with a notifications/subscriptions/acknowledged notification
reporting the honored subset, then streams every change notification on
the same request, and finally closes with a SubscriptionsListenResult
when it tears the subscription down.
Transports
A
transport
can be used to send JSON-RPC messages from client to server, or vice-versa.
In the SDK, this is achieved by implementing theTransport
interface, which creates a (logical) bidirectional stream of JSON-RPC messages.
Most transport implementations described below are specific to either the
client or server: a "client transport" is something that can be used to connect
a client to a server, and a "server transport" is something that can be used to
connect a server to a client. However, it's possible for a transport to be both
a client and server transport, such as the InMemoryTransport used in the
lifecycle example above.
Transports should not be reused for multiple connections: if you need to create
multiple connections, use different transports.
Stdio Transport
In thestdio
transport clients communicate with an MCP server running in a subprocess using
newline-delimited JSON over its stdin/stdout.
Client-side: the client side of the stdio transport is implemented byCommandTransport,
which starts the a exec.Cmd as a subprocess and communicates over its
stdin/stdout.
Server-side: the server side of the stdio transport is implemented byStdioTransport,
which connects over the current processes os.Stdin and os.Stdout.
Streamable Transport
The streamable
transport
API is implemented across three types:
StreamableHTTPHandler: anhttp.Handlerthat serves streamable MCP
sessions.StreamableServerTransport: aTransportthat implements the server side of
the streamable transport.StreamableClientTransport: aTransportthat implements the client side of
the streamable transport.
To create a streamable MCP server, you create a StreamableHTTPHandler and
pass it an mcp.Server:
// TODO: Until we have a way to clean up abandoned sessions, this test will leak goroutines (see #499)
func ExampleStreamableHTTPHandler() {
// Create a new streamable handler, using the same MCP server for every request.
//
// Here, we configure it to serves application/json responses rather than
// text/event-stream, just so the output below doesn't use random event ids.
server := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.1.0"}, nil)
handler := mcp.NewStreamableHTTPHandler(func(r *http.Request) *mcp.Server {
return server
}, &mcp.StreamableHTTPOptions{JSONResponse: true})
httpServer := httptest.NewServer(handler)
defer httpServer.Close()
// The SDK is currently permissive of some missing keys in "params".
resp := mustPostMessage(`{"jsonrpc": "2.0", "id": 1, "method":"initialize", "params": {"protocolVersion":"2025-11-25"}}`, httpServer.URL)
fmt.Println(resp)
// Output:
// {"jsonrpc":"2.0","id":1,"result":{"capabilities":{"logging":{}},"protocolVersion":"2025-11-25","serverInfo":{"name":"server","version":"v0.1.0"}}}
}The StreamableHTTPHandler handles the HTTP requests and creates a newStreamableServerTransport for each new session. The transport is then used to
communicate with the client.
On the client side, you create a StreamableClientTransport and use it to
connect to the server:
transport := &mcp.StreamableClientTransport{
Endpoint: "http://localhost:8080/mcp",
}
client := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v1.0.0"}, nil)
session, err := client.Connect(ctx, transport, nil)The StreamableClientTransport handles the HTTP requests and communicates with
the server using the streamable transport protocol.
HTTP Headers
SEP-2243
standardises a small set of MCP-specific HTTP headers, sent and validated by
the SDK only for sessions negotiated on protocol version 2026-07-28 or
later.
| Header | Direction | Purpose |
|---|---|---|
Mcp-Protocol-Version |
request | Mirrors _meta.io.modelcontextprotocol/protocolVersion from the body |
Mcp-Session-Id |
request/response | Logical session identifier (removed in stateless mode) |
Mcp-Method |
request | Mirrors the JSON-RPC method field; mismatch ⇒ -32020 |
Mcp-Name |
request | Mirrors the request's principal name (tools/call.params.name, prompts/get.params.name, resources/read.params.uri); mismatch ⇒ -32020 |
Mcp-Param-{Header} |
request | Per-tool parameter passthrough; see below |
The header-name set is exhaustive: for protocol versions earlier than2026-07-28, only Mcp-Protocol-Version and Mcp-Session-Id are recognised.
Body↔header mirroring. When Mcp-Method or Mcp-Name is present but
does not match the JSON-RPC body, the server returnsCodeHeaderMismatch (-32020). The same applies to a
mismatch between Mcp-Protocol-Version and the body's_meta.io.modelcontextprotocol/protocolVersion.
Mcp-Param-* passthrough. A tool may annotate properties of itsInputSchema with x-mcp-header: "<HeaderName>". When a client invokes that
tool over HTTP, the SDK serializes the matching argument(s) asMcp-Param-<HeaderName> request headers (using =?base64?...?= encoding for
non-ASCII values). On the server, the headers are validated against the body
and either accepted or rejected with -32020. Properties so annotated must
be typed string, integer, or boolean; tools that fail validation at
registration time are silently dropped from tools/list.
Resumability and Redelivery
By default, the streamable server does not support resumability or
redelivery
of messages, because doing so requires either a persistent storage solution or
unbounded memory usage (see also
#580).
To enable resumability, set StreamableHTTPOptions.EventStore to a non-nil
value. The SDK provides a MemoryEventStore for testing or simple use cases;
for production use it is generally advisable to use a more sophisticated
implementation.
Note: SEP-2575
removes SSE stream resumability (Last-Event-ID, SSE event IDs) for protocol
version2026-07-28. The SDK preserves theEventStoreandLast-Event-ID
code paths for backward compatibility with2025-11-25and earlier; on2026-07-28sessions, a broken response stream loses the in-flight request
and the client must re-issue it as a new request with a new ID.
Stateless Mode
The streamable server supports a stateless mode by settingStreamableHTTPOptions.Stateless,
which is where the server does not perform any validation of the session id,
and uses a temporary session to handle requests. In this mode, it is impossible
for the server to make client requests, as there is no way for the client's
response to reach the session.
However, it is still possible for the server to access the ServerSession.ID
to see the logical session
[!WARNING]
Stateless mode is not directly discussed in the spec, and is still being
defined. See modelcontextprotocol/modelcontextprotocol#1364,
modelcontextprotocol/modelcontextprotocol#1372, or
modelcontextprotocol/modelcontextprotocol#1442 for potential refinements.
Required for
2026-07-28: the streamable HTTP transport accepts
requests at protocol version2026-07-28only whenStateless = true.
Requests at that version against a non-stateless handler are rejected.
See examples/server/distributed for
an example using stateless mode to implement a server distributed across
multiple processes.
Legacy SSE Transport
Before the streamable transport, the
2024-11-05
spec defined an HTTP transport built from two endpoints: a hanging GET that
streams server-to-client messages as server-sent events, and a per-session
endpoint the client POSTs its messages to. The SDK still implements it, for
talking to peers that predate the streamable transport. New deployments should
use the streamable transport instead.
It is implemented across three types, mirroring the streamable ones:
SSEHandler:
anhttp.Handlerthat creates a session per incoming GET and routes POSTs to
the right session. Construct it withNewSSEHandler,
which takes a function returning theServerto serve a given request.SSEServerTransport:
one such session. Reach for it directly only when serving the two endpoints
yourself;SSEHandlercreates these for you.SSEClientTransport:
the client side, which needs only theEndpointof the GET.
func ExampleSSEHandler() {
server := mcp.NewServer(&mcp.Implementation{Name: "adder", Version: "v0.0.1"}, nil)
mcp.AddTool(server, &mcp.Tool{Name: "add", Description: "add two numbers"}, Add)
handler := mcp.NewSSEHandler(func(*http.Request) *mcp.Server { return server }, nil)
httpServer := httptest.NewServer(handler)
defer httpServer.Close()
ctx := context.Background()
transport := &mcp.SSEClientTransport{Endpoint: httpServer.URL}
client := mcp.NewClient(&mcp.Implementation{Name: "test", Version: "v1.0.0"}, nil)
cs, err := client.Connect(ctx, transport, nil)
if err != nil {
log.Fatal(err)
}
defer cs.Close()
res, err := cs.CallTool(ctx, &mcp.CallToolParams{
Name: "add",
Arguments: map[string]any{"x": 1, "y": 2},
})
if err != nil {
log.Fatal(err)
}
fmt.Println(res.Content[0].(*mcp.TextContent).Text)
// Output: 3
}See examples/server/sse
for a standalone server.
Custom transports
The SDK supports custom
transports
by implementing theTransport
interface: a logical bidirectional stream of JSON-RPC messages.
Full example: examples/server/custom-transport.
Concurrency
In general, MCP offers no guarantees about concurrency semantics: if a client
or server sends a notification, the spec says nothing about when the peer
observes that notification relative to other request. However, the Go SDK
implements the following heuristics:
- If a notifying method (such as
notifications/progressornotifications/initialized) returns, then it is guaranteed that the peer
observes that notification before other notifications or calls from the same
client goroutine. - Calls (such as
tools/call) are handled asynchronously with respect to
each other.
See
modelcontextprotocol/go-sdk#26
for more background.
Authorization
Server
To write an MCP server that performs authorization,
use RequireBearerToken.
This function is middleware that wraps an HTTP handler, such as the one returned
by NewStreamableHTTPHandler, to provide support for verifying bearer tokens.
The middleware function checks every request for an Authorization header with a bearer token,
and invokes theTokenVerifier
passed to RequireBearerToken to parse the token and perform validation.
The middleware function checks expiration and scopes (if they are provided inRequireBearerTokenOptions.Scopes), so theTokenVerifier doesn't have to.
If RequireBearerTokenOptions.ResourceMetadataURL is set and verification fails,
the middleware function sets the WWW-Authenticate header as required by the Protected Resource
Metadata spec.
Server handlers, such as tool handlers, can obtain the TokenInfo returned by the TokenVerifier
from req.Extra.TokenInfo, where req is the handler's request. (For example, aCallToolRequest.)
HTTP handlers wrapped by the RequireBearerToken middleware can obtain the TokenInfo from the context
with auth.TokenInfoFromContext.
OAuth Protected Resource Metadata
Servers implementing OAuth 2.0 authorization should expose a protected resource metadata endpoint
as specified in RFC 9728. This endpoint provides
clients with information about the resource server's OAuth configuration, including which
authorization servers can be used and what scopes are supported.
The SDK provides ProtectedResourceMetadataHandler
to serve this metadata. The handler automatically sets CORS headers (Access-Control-Allow-Origin: *)
to support cross-origin client discovery, as the metadata contains only public configuration information.
Example usage:
metadata := &oauthex.ProtectedResourceMetadata{
Resource: "https://example.com/mcp",
AuthorizationServers: []string{
"https://auth.example.com/.well-known/openid-configuration",
},
ScopesSupported: []string{"read", "write"},
}
http.Handle("/.well-known/oauth-protected-resource",
auth.ProtectedResourceMetadataHandler(metadata))For more sophisticated CORS policies, wrap the handler with a CORS middleware like
github.com/rs/cors or github.com/jub0bs/cors.
The auth middleware example shows how to implement authorization for both JWT tokens and API keys.
Client
Client-side authorization is supported via theStreamableClientTransport.OAuthHandler
field. If the handler is provided, the transport will automatically use it to
add an Authorization: Bearer <token> header to every request. The transport
will also call the handler's Authorize method if the server returns401 Unauthorized or 403 Forbidden errors to perform the authorization flow
or facilitate scope step-up authorization.
The SDK implements the Authorization Code flow inauth.AuthorizationCodeHandler.
This handler supports:
- Client ID Metadata Documents
- Pre-registered clients
- Dynamic Client Registration
- RFC 9207 Authorization Server Issuer Identification
To use it, configure the handler and assign it to the transport:
authHandler, _ := auth.NewAuthorizationCodeHandler(&auth.AuthorizationCodeHandlerConfig{
RedirectURL: "https://myapp.com/oauth2-callback",
// Configure one of the following:
// ClientIDMetadataDocumentConfig: ...
// PreregisteredClientConfig: ...
// DynamicClientRegistrationConfig: ...
AuthorizationCodeFetcher: func(ctx context.Context, args *auth.AuthorizationArgs) (*auth.AuthorizationResult, error) {
// Open the args.URL in a browser and return the resulting code, state, and iss.
// See full example in examples/auth/client/main.go.
code := ...
state := ...
iss := ... // "iss" query parameter from the redirect URI (RFC 9207)
return &auth.AuthorizationResult{Code: code, State: state, Iss: iss}, nil
},
})
transport := &mcp.StreamableClientTransport{
Endpoint: "https://example.com/mcp",
OAuthHandler: authHandler,
}
client := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, nil)
session, err := client.Connect(ctx, transport, nil)The auth.AuthorizationCodeHandler automatically manages token refreshing (if the server provides a refresh token) and step-up authentication (when the server returns insufficient_scope error).
Enterprise Managed Authorization (SEP-990)
For enterprise SSO scenarios where users authenticate with an enterprise Identity Provider (IdP),
the SDK providesextauth.EnterpriseHandler,
an implementation of OAuthHandler that automates the Enterprise Managed Authorization flow:
- OIDC Login: User authenticates with enterprise IdP → ID Token
- Token Exchange (RFC 8693): ID Token → ID-JAG at IdP
- JWT Bearer Grant (RFC 7523): ID-JAG → Access Token at MCP Server
To use enterprise managed authorization, create an EnterpriseHandler and assign it to your transport:
/* Detailed source-code truncated for AI context efficiency. */The EnterpriseHandler automatically manages the token exchange flow. Note that it intentionally does not support refresh tokens - when an access token expires, the entire authorization flow is repeated to ensure enterprise policies are consistently enforced.
For a complete working example, see examples/auth/enterprise.
Security
Here we discuss the mitigations described under
the MCP's Security Best Practices section, and how we handle them.
Confused Deputy
The mitigation,
obtaining user consent for dynamically registered clients, is mostly the
responsibility of the MCP Proxy server implementation. The SDK client does
generate cryptographically secure random state values for each authorization
request by default and validates them when the authorization code is returned.
Mismatched state values will result in an error.
Token Passthrough
The mitigation, accepting only tokens that were issued for the server, depends on the structure
of tokens and is the responsibility of theTokenVerifier
provided toRequireBearerToken.
Server-Side Request Forgery
The OAuth discovery helpers apply these mitigations by default:
Enforce HTTPS. Reject
http://URLs (except loopback:localhost,127.0.0.1,::1)
and redirects that downgradehttpsto a non-httpsscheme.Block Private IP Ranges. Reject targets that resolve to a non-public address
(private, link-local including the169.254.169.254metadata endpoint, CGNAT, multicast,
unspecified). The check runs on the initial URL and at dial time, guarding against DNS rebinding.Validate Redirect Targets. Reject redirects to non-public addresses and cap the redirect count.
Use Egress Proxies. Out of scope for the SDK; configure separately.
Opting out. Passing a custom http.Client can bypass these checks, making SSRF
protection your responsibility. Specifically: a custom Transport.DialContext/DialTLSContext,
a non-*http.Transport, or a configured Proxy disables the dial-time IP check; settingCheckRedirect replaces all redirect validation. In these cases, implement your own IP
blocking, redirect validation, and DNS pinning.
Session Hijacking
The mitigations are as follows:
Verify all inbound requests. The
RequireBearerToken
middleware function will verify all HTTP requests that it receives. It is the
user's responsibility to wrap that function around all handlers in their server.Secure session IDs. This SDK generates cryptographically secure session IDs by default.
If you create your own withServerOptions.GetSessionID, it is your responsibility to ensure they are secure.
We recommend usingcrypto/rand.Text.Binding session IDs to user information. The SDK supports this mitigation through
TokenInfo.UserID.
When aTokenVerifier
setsUserIDon the returnedTokenInfo, the streamable transport will:- Store the user ID when a new session is created.
- Verify that subsequent requests to that session include a token with the same
UserID. - Reject requests with a 403 Forbidden if the user ID doesn't match.
Recommendation: If your
TokenVerifiercan extract a user identifier from the token
(such as asubclaim in a JWT, or a user ID associated with an API key), setTokenInfo.UserIDto enable this protection. This prevents an attacker with a valid
token from hijacking another user's session by guessing or obtaining their session ID.
Issuer Mix-Up
The mitigation against issuer mix-up attacks is
implemented per RFC 9207. The SDK client validates
the iss parameter in authorization responses to ensure they originated from the expected
authorization server:
- If
issis present in the redirect URI, the SDK verifies it matches the issuer from the
authorization server's metadata. A mismatch results in an error. - If
issis absent but the authorization server advertisesauthorization_response_iss_parameter_supported: truein its RFC 8414
metadata, the SDK rejects the response with an error.
The AuthorizationCodeFetcher is responsible for extracting the iss query parameter from
the redirect URI and returning it in AuthorizationResult.Iss.
Utilities
Cancellation
Cancellation is implemented with context cancellation. Cancelling a context
used in a method on ClientSession or ServerSession will terminate the RPC
and send a "notifications/cancelled" message to the peer.
When an RPC exits due to a cancellation error, there's a guarantee that the
cancellation notification has been sent, but there's no guarantee that the
server has observed it (see concurrency).
On a session using protocol version 2026-07-28 or later over a stdio-style
transport (StdioTransport, CommandTransport, IOTransport orInMemoryTransport), a server that receives a cancellation notification sends
no response for the cancelled request, as the spec requires. Over the
streamable HTTP transport, a 2026-07-28 request is cancelled by closing its
HTTP request instead (see StreamableHTTPOptions.PropagateRequestCancellation).
/* Detailed source-code truncated for AI context efficiency. */Ping
Ping
support is symmetrical for client and server.
To initiate a ping, callClientSession.Ping
orServerSession.Ping.
To have the client or server session automatically ping its peer, and close the
session if the ping fails, setClientOptions.KeepAlive
orServerOptions.KeepAlive.
Note:
pingis removed from the protocol as of2026-07-28by
SEP-2575.
The SDK preserves the API for backward compatibility with legacy clients;
when a session negotiates2026-07-28or later,PingreturnsMethodNotFound(-32601) locally, without a round trip, andKeepAliveshould not be enabled.
Progress
Progress
reporting is possible by reading the progress token from request metadata and
calling eitherClientSession.NotifyProgress
orServerSession.NotifyProgress.
To listen to progress notifications, setClientOptions.ProgressNotificationHandler
orServerOptions.ProgressNotificationHandler.
Issue #460 discusses some potential ergonomic improvements to this API.
func Example_progress() {
server := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil)
mcp.AddTool(server, &mcp.Tool{Name: "makeProgress"}, func(ctx context.Context, req *mcp.CallToolRequest, _ any) (*mcp.CallToolResult, any, error) {
if token := req.Params.GetProgressToken(); token != nil {
for i := range 3 {
params := &mcp.ProgressNotificationParams{
Message: "frobbing widgets",
ProgressToken: token,
Progress: float64(i),
Total: 2,
}
req.Session.NotifyProgress(ctx, params) // ignore error
}
}
return &mcp.CallToolResult{}, nil, nil
})
client := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, &mcp.ClientOptions{
ProgressNotificationHandler: func(_ context.Context, req *mcp.ProgressNotificationClientRequest) {
fmt.Printf("%s %.0f/%.0f\n", req.Params.Message, req.Params.Progress, req.Params.Total)
},
})
ctx := context.Background()
t1, t2 := mcp.NewInMemoryTransports()
if _, err := server.Connect(ctx, t1, nil); err != nil {
log.Fatal(err)
}
session, err := client.Connect(ctx, t2, nil)
if err != nil {
log.Fatal(err)
}
defer session.Close()
if _, err := session.CallTool(ctx, &mcp.CallToolParams{
Name: "makeProgress",
Meta: mcp.Meta{"progressToken": "abc123"},
}); err != nil {
log.Fatal(err)
}
// Output:
// frobbing widgets 0/2
// frobbing widgets 1/2
// frobbing widgets 2/2
}Error codes
The SDK uses the standard JSON-RPC base codes (parse error -32700,
invalid request -32600, method not found -32601, invalid params-32602, internal error -32603) plus the following MCP-specific codes:
| Constant | Code | Meaning |
|---|---|---|
CodeHeaderMismatch |
-32020 |
An MCP HTTP header (Mcp-Method, Mcp-Name, Mcp-Protocol-Version, or Mcp-Param-*) does not match the JSON-RPC body |
CodeMissingRequiredClientCapabilities |
-32021 |
Server requires client capabilities the client did not declare |
CodeUnsupportedProtocolVersion |
-32022 |
Requested protocol version not supported. Data: UnsupportedProtocolVersionData{Supported []string, Requested string} |
CodeResourceNotFound |
-32602 |
Resource URI not found (variable; was -32002 before SEP-2164) |
The error code allocation policy defined in 2026-07-28
(SEP-2575)
partitions the JSON-RPC server-error range:
-32000to-32019: implementation-defined; existing SDK usage is grandfathered.-32020to-32099: reserved for the MCP specification.
Quick Start (`docs/quick_start.md`)
Quick Start
Installation
go get github.com/modelcontextprotocol/go-sdk/mcpGetting started
To get started creating an MCP server, create an mcp.Server instance, add
features to it, and then run it over an mcp.Transport. For example, this
server adds a single simple tool, and then connects it to clients over
stdin/stdout:
package main
import (
"context"
"log"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
type Input struct {
Name string `json:"name" jsonschema:"the name of the person to greet"`
}
type Output struct {
Greeting string `json:"greeting" jsonschema:"the greeting to tell to the user"`
}
func SayHi(ctx context.Context, req *mcp.CallToolRequest, input Input) (
*mcp.CallToolResult,
Output,
error,
) {
return nil, Output{Greeting: "Hi " + input.Name}, nil
}
func main() {
// Create a server with a single tool.
server := mcp.NewServer(&mcp.Implementation{Name: "greeter", Version: "v1.0.0"}, nil)
mcp.AddTool(server, &mcp.Tool{Name: "greet", Description: "say hi"}, SayHi)
// Run the server over stdin/stdout, until the client disconnects.
if err := server.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
log.Fatal(err)
}
}To communicate with that server, create an mcp.Client and connect it to the
corresponding server, by running the server command and communicating over its
stdin/stdout:
package main
import (
"context"
"log"
"os/exec"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
func main() {
ctx := context.Background()
// Create a new client, with no features.
client := mcp.NewClient(&mcp.Implementation{Name: "mcp-client", Version: "v1.0.0"}, nil)
// Connect to a server over stdin/stdout.
transport := &mcp.CommandTransport{Command: exec.Command("myserver")}
session, err := client.Connect(ctx, transport, nil)
if err != nil {
log.Fatal(err)
}
defer session.Close()
// Call a tool on the server.
params := &mcp.CallToolParams{
Name: "greet",
Arguments: map[string]any{"name": "you"},
}
res, err := session.CallTool(ctx, params)
if err != nil {
log.Fatalf("CallTool failed: %v", err)
}
if res.IsError {
log.Fatal("tool failed")
}
for _, c := range res.Content {
log.Print(c.(*mcp.TextContent).Text)
}
}The examples/ directory contains more example clients and
servers.
Rough Edges (`docs/rough_edges.md`)
Rough edges: API decisions to reconsider for v2
This file collects a list of API oversights or rough edges that we've uncovered
post v1.0.0, along with their current workarounds. These issues can't be
addressed without breaking backward compatibility, but we'll revisit them for
v2.
EventStore.Openis unnecessary. This was an artifact of an earlier version
of the SDK where event persistence and delivery were combined.Workaround:
Openmay be implemented as a no-op.Eventneed not have been exported: it's an implementation detail of the SSE
and streamable transports. Also the 'Name' field is a misnomer: it should be
'event'.Enforcing valid tool names: with
SEP-986
landing after the SDK was at v1, we missed an opportunity to panic on invalid
tool names. Instead, we have to simply produce an error log. In v2, we should
panic.Inconsistent naming.
ResourceUpdatedNotificationsParamsshould probably have just beenResourceUpdatedParams, as we don't include the word 'notification' in
other notification param types.- Similarly,
ProgressNotificationParamsshould probably have beenProgressParams.
AudioContent.MarshalJSONshould have had a pointer receiver, to be
consistent with other content types.ClientCapabilities.Rootsshould have been a distinguished struct pointer
(see #607).Workaround: use
ClientCapabilities.RootsV2, which aligns with the
semantics of other capability fields.Default capabilities should have been empty. Instead, servers default to
advertisinglogging, and clients default to advertisingrootswithlistChanged: true. This is confusing because a nilCapabilitiesfield
does not mean "no capabilities".Workaround: to advertise no capabilities, set
ServerOptions.CapabilitiesorClientOptions.Capabilitiesto an empty&ServerCapabilities{}or&ClientCapabilities{}respectively.CreateMessageResult.Contentis singularContent, but the 2025-11-25 spec
allowscontentto be a single block or an array (for parallel tool calls).
We addedCreateMessageResultWithTools(withContent []Content) as a
workaround, matching the TypeScript SDK's approach. In v2,CreateMessageResultshould use[]Contentdirectly. Similarly,SamplingMessage.Contentshould become[]Contentto support sending
multiple tool_result blocks in a single user message.We didn't actually need CallToolParams and CallToolParamsRaw, since even when
we're unmarshalling into a custom Go type (for the mcp.AddTool convenience
wrapper) we need to first unmarshal into amap[string]anyin order to do
server-side validation of required fields. CallToolParams could have just had
a map[string]any.StreamableHTTPOptions.CrossOriginProtectionshould not have been part of
the SDK API. Cross-origin protection is a general HTTP concern, not specific
to MCP, and can be applied as standard HTTP middleware.ToolAnnotations(mcp/protocol.go) should have all fields typed as*bool
for full control to define what is being sent over the wire. Different
MCP clients have different requirements, and some of them require all fields
to be explicitly set to eithertrueorfalse.
Server (`docs/server.md`)
Support for MCP server features
- Prompts
- Resources
- Tools
- List changed notifications
- Multi Round-Trip Requests
- Cacheable list results
- Utilities
- Capabilities
Prompts
MCP servers can provide LLM prompt templates (called simply
prompts)
to clients. Every prompt has a required name which identifies it, and a set of
named arguments, which are strings.
Client-side: To list the server's prompts, use theClientSession.Prompts
iterator, or the lower-levelClientSession.ListPrompts
(see pagination below). SetClientOptions.PromptListChangedHandler
to be notified of changes in the list of prompts.
CallClientSession.GetPrompt
to retrieve a prompt by name, providing arguments for expansion.
Server-side: UseServer.AddPrompt
to add a prompt to the server along with its handler.
The server will have the prompts capability if any prompt is added before the
server is connected to a client, or ifServerOptions.HasPrompts
is explicitly set. When a prompt is added, any clients already connected to the
server will be notified via a notifications/prompts/list_changed
notification.
/* Detailed source-code truncated for AI context efficiency. */Prompt message content
APromptMessage
has two fields: a Role, either "user" or "assistant", and a singleContent. That is the same interface a tool result uses (see Tool result
content), so a prompt message can hold an image, audio,
or a resource's contents as readily as text.
Media on its own says nothing about what the model should do with it, so send a
text message alongside it. Embedding a resource puts its contents directly in
the message, sparing the client a separate resources/read. Note thatResourceContents.URI is not checked against the resources the server has
registered: it records where the contents came from, so use the URI of a real
resource when there is one, as in the example below.
/* Detailed source-code truncated for AI context efficiency. */Resources
In MCP terms, a resource is some data referenced by a URI.
MCP servers can serve resources to clients.
They can register resources individually, or register a resource template
that uses a URI pattern to describe a collection of resources.
Client-side:
Call ClientSession.ReadResource
to read a resource.
The SDK ensures that a read succeeds only if the URI matches a registered resource exactly,
or matches the URI pattern of a resource template.
To list a server's resources and resource templates, use theClientSession.Resources
andClientSession.ResourceTemplates
iterators, or the lower-level ListXXX calls (see pagination).
SetClientOptions.ResourceListChangedHandler
to be notified of changes in the lists of resources or resource templates.
Clients can be notified when the contents of a resource changes by subscribing to the resource's URI.
CallClientSession.Subscribe
to subscribe to a resource
andClientSession.Unsubscribe
to unsubscribe.
SetClientOptions.ResourceUpdatedHandler
to be notified of changes to subscribed resources. On 2026-07-28 and
later sessions the SDK delivers these notifications over asubscriptions/listen stream instead of the legacy resources/subscribe
RPC; see Subscriptions
(subscriptions/listen)
for the wire-level details.
Server-side:
UseServer.AddResource
orServer.AddResourceTemplate
to add a resource or resource template to the server along with its handler.
AResourceHandler
maps a URI to the contents of a resource, which can include text, binary data,
or both.
If AddResource or AddResourceTemplate is called before a server is connected, the server will have theresources capability.
The server will have the resources capability if any resource or resource template is added before the
server is connected to a client, or ifServerOptions.HasResources
is explicitly set. When a prompt is added, any clients already connected to the
server will be notified via a notifications/resources/list_changed
notification.
/* Detailed source-code truncated for AI context efficiency. */Binary resources
AResourceContents
carries its data in either Text or Blob. Binary data goes in Blob as
plain bytes — the SDK base64-encodes it on the wire — and leaves Text empty,
so a client tells the two apart by which field is populated.
func Example_binaryResource() {
ctx := context.Background()
s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil)
s.AddResource(&mcp.Resource{URI: "file:///logo.png", MIMEType: "image/png"},
func(context.Context, *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
return &mcp.ReadResourceResult{
Contents: []*mcp.ResourceContents{{
URI: "file:///logo.png",
MIMEType: "image/png",
Blob: []byte("\x89PNG\r\n\x1a\n"),
}},
}, nil
})
c := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, nil)
t1, t2 := mcp.NewInMemoryTransports()
if _, err := s.Connect(ctx, t1, nil); err != nil {
log.Fatal(err)
}
cs, err := c.Connect(ctx, t2, nil)
if err != nil {
log.Fatal(err)
}
defer cs.Close()
res, err := cs.ReadResource(ctx, &mcp.ReadResourceParams{URI: "file:///logo.png"})
if err != nil {
log.Fatal(err)
}
contents := res.Contents[0]
fmt.Printf("%s: %d bytes of %s, text is %q\n",
contents.URI, len(contents.Blob), contents.MIMEType, contents.Text)
// Output:
// file:///logo.png: 8 bytes of image/png, text is ""
}Resource subscriptions
A client interested in changes to one resource subscribes to its URI withClientSession.Subscribe
and stops withClientSession.Unsubscribe.
SetServerOptions.SubscribeHandler
andUnsubscribeHandler
to track who is listening; setting either one gives the server theresources.subscribe capability.
Publish a change withServer.ResourceUpdated,
which notifies every session subscribed to that URI. The notification carries
only the URI, not the new contents, so a client that wants them reads the
resource again.
/* Detailed source-code truncated for AI context efficiency. */Tools
MCP servers can provide
tools
to allow clients to interact with external systems or functionality. Tools are
effectively remote function calls, and the Go SDK provides mechanisms to bind
them to ordinary Go functions.
Client-side: To list the server's tools, use theClientSession.Tools
iterator, or the lower-levelClientSession.ListTools
(see pagination). SetClientOptions.ToolListChangedHandler
to be notified of changes in the list of tools.
To call a tool, useClientSession.CallTool
with CallToolParams holding the name and arguments of the tool to call.
res, err := session.CallTool(ctx, &mcp.CallToolParams{
Name: "my_tool",
Arguments: map[string]any{"name": "user"},
})Arguments may be any value that can be marshaled to JSON.
Server-side: the basic API for adding a tool is symmetrical with the API
for prompts or resources:Server.AddTool
adds aTool to
the server along with itsToolHandler
to handle it. The server will have the tools capability if any tool is added
before the server is connected to a client, or ifServerOptions.HasTools
is explicitly set. When a tool is added, any clients already connected to the
server will be notified via a notifications/tools/list_changed notification.
However, the Server.AddTool API leaves it to the user to implement the tool
handler correctly according to the spec, providing very little out of the box.
In order to implement a tool, the user must do all of the following:
- Provide a tool input and output schema.
- Validate the tool arguments against its input schema.
- Unmarshal the input schema into a Go value
- Execute the tool logic.
- Marshal the tool's structured output (if any) to JSON, and store it in the
result'sStructuredContentfield as well as the unstructuredContentfield. - Validate that output JSON against the tool's output schema.
- If any tool errors occurred, pack them into the unstructured content and set
IsErrortotrue.
For this reason, the SDK provides a genericAddTool
function that handles this for you. It can bind a tool to any function with the
following shape:
func(_ context.Context, request *CallToolRequest, input In) (result *CallToolResult, output Out, _ error)This is like a ToolHandler, but with an extra arbitrary In input parameter,
and Out output parameter.
Such a function can then be bound to the server using AddTool:
mcp.AddTool(server, &mcp.Tool{Name: "my_tool"}, handler)This does the following automatically:
- If
Tool.InputSchemais unset, the input schema is inferred from theIn
type, which must be a struct or map. - If
Tool.OutputSchemais unset and theOuttype is notany, the output
schema is inferred from theOuttype. Per SEP-2106,Outmay be any Go
type whose inferred schema is a valid JSON Schema (struct, map, slice,
primitive, etc.). - Optional
jsonschemastruct tags provide argument and output descriptions. - Tool arguments are validated against the input schema.
- Tool arguments are marshaled into the
Invalue. - Tool output (the
Outvalue) is marshaled into the result'sStructuredContent, as well as the unstructuredContent. - Output is validated against the tool's output schema.
- If an ordinary error is returned, it is stored int the
CallToolResultandIsErroris set totrue.
In fact, under ordinary circumstances, the user can ignore CallToolRequest
and CallToolResult.
For a more realistic example, consider a tool that retrieves the weather:
type WeatherInput struct {
Location Location `json:"location" jsonschema:"user location"`
Days int `json:"days" jsonschema:"number of days to forecast"`
}
type WeatherOutput struct {
Summary string `json:"summary" jsonschema:"a summary of the weather forecast"`
Confidence Probability `json:"confidence" jsonschema:"confidence, between 0 and 1"`
AsOf time.Time `json:"asOf" jsonschema:"the time the weather was computed"`
DailyForecast []Forecast `json:"dailyForecast" jsonschema:"the daily forecast"`
Source string `json:"source,omitempty" jsonschema:"the organization providing the weather forecast"`
}
func WeatherTool(ctx context.Context, req *mcp.CallToolRequest, in WeatherInput) (*mcp.CallToolResult, WeatherOutput, error) {
perfectWeather := WeatherOutput{
Summary: "perfect",
Confidence: 1.0,
AsOf: time.Now(),
}
for range in.Days {
perfectWeather.DailyForecast = append(perfectWeather.DailyForecast, Forecast{
Forecast: "another perfect day",
Type: Sunny,
Rain: 0.0,
High: 72.0,
Low: 72.0,
})
}
return nil, perfectWeather, nil
}In this case, we want to customize part of the inferred schema, though we can
still infer the rest. Since we want to control the inference ourselves, we set
the Tool.InputSchema explicitly:
// Distinguished Go types allow custom schemas to be reused during inference.
customSchemas := map[reflect.Type]*jsonschema.Schema{
reflect.TypeFor[Probability](): {Type: "number", Minimum: jsonschema.Ptr(0.0), Maximum: jsonschema.Ptr(1.0)},
reflect.TypeFor[WeatherType](): {Type: "string", Enum: []any{Sunny, PartlyCloudy, Cloudy, Rainy, Snowy}},
}
opts := &jsonschema.ForOptions{TypeSchemas: customSchemas}
in, err := jsonschema.For[WeatherInput](opts)
if err != nil {
log.Fatal(err)
}
// Furthermore, we can tweak the inferred schema, in this case limiting
// forecasts to 0-10 days.
daysSchema := in.Properties["days"]
daysSchema.Minimum = jsonschema.Ptr(0.0)
daysSchema.Maximum = jsonschema.Ptr(10.0)
// Output schema inference can reuse our custom schemas from input inference.
out, err := jsonschema.For[WeatherOutput](opts)
if err != nil {
log.Fatal(err)
}
// Now add our tool to a server. Since we've customized the schemas, we need
// to override the default schema inference.
server := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil)
mcp.AddTool(server, &mcp.Tool{
Name: "weather",
InputSchema: in,
OutputSchema: out,
}, WeatherTool)See mcp/tool_example_test.go for the full
example, or examples/server/toolschemas
for more examples of customizing tool schemas.
Tool result content
Alongside its structured output, a tool result carries a list ofContent
blocks in CallToolResult.Content, and may mix as many kinds as it needs:
| Type | Carries |
|---|---|
TextContent |
plain text |
ImageContent |
image data, with a MIME type |
AudioContent |
audio data, with a MIME type |
EmbeddedResource |
the contents of a resource, inline |
ResourceLink |
a reference to a resource the client can read separately |
ImageContent.Data and AudioContent.Data are plain []byte; the SDK
base64-encodes them on the wire, so handlers never encode by hand, and base64
carried over from another MCP SDK must be decoded before it is assigned. AnEmbeddedResource wraps aResourceContents,
which holds either Text or, for binary data, Blob. Prefer a ResourceLink
when the client may not need the contents, since an embedded resource is
transferred whether it is used or not.
When a handler bound with AddTool leaves Content unset, the SDK fills it
with the JSON encoding of the output value; set Content explicitly, as below,
to return anything else. StructuredContent is still populated from the output
value either way.
/* Detailed source-code truncated for AI context efficiency. */ToolUseContent and ToolResultContent also implement Content, but are only
valid in sampling messages, not in tool results.
Stateless server deployments: Some deployments create a newServer
for each incoming request, re-registering tools every time. To avoid repeated
schema generation, create aSchemaCache
and share it across server instances:
var schemaCache = mcp.NewSchemaCache() // create once at startup
func handleRequest(w http.ResponseWriter, r *http.Request) {
s := mcp.NewServer(impl, &mcp.ServerOptions{SchemaCache: schemaCache})
mcp.AddTool(s, myTool, myHandler)
// ...
}List changed notifications
Adding or removing a feature on a connected server sends the matchingnotifications/*/list_changed to every client, which dispatches it toClientOptions.ToolListChangedHandler,PromptListChangedHandler,
orResourceListChangedHandler.
A notification reports only that the list changed, so a client that needs the
new contents lists them again.
These notifications ride on capabilities, and capabilities are inferred from
what is registered before Server.Connect
(see Capabilities). A server that registers its features
afterwards, as the example below does, has to declareServerOptions.HasTools, HasPrompts, or HasResources itself, or the
client is never told.
/* Detailed source-code truncated for AI context efficiency. */Multi Round-Trip Requests
SEP-2322
defines a new pattern for server-to-client requests (sampling, elicitation,
roots). Instead of issuing a fresh JSON-RPC request mid-flight, the server
returns an InputRequiredResult from its in-flight handler — theInputRequests field of CallToolResult, GetPromptResult, orReadResourceResult carries the requests for additional information. The
client responds by retrying the original call with InputResponses set.
The SDK supports this pattern from both sides without requiring callers to
choose:
- Returning input requests from a handler. Set
InputRequestson the
result you return. To have the client call again without asking it for
input, return only aRequestState; the client echoes it on the retry.
LeaveContent/StructuredContentempty — returning
content together withInputRequestsorRequestStateis a server bug and
the SDK returns-32603 InternalError. - Calling the legacy APIs.
ServerSession.Elicit,ServerSession.CreateMessage(WithTools), andServerSession.ListRoots
remain available and work for both new and legacy clients.
Talking to legacy clients
The server installs serverMultiRoundTripMiddleware automatically. For
clients on a protocol version earlier than 2026-07-28, the middleware
intercepts any InputRequiredResult your handler returns, fulfils each
input request itself by calling the legacy server-initiated APIs
(Elicit, CreateMessage, ListRoots), and re-invokes your handler
with the responses already populated. A result with a RequestState and no
input requests re-invokes it with that state and no responses. If the handler asks for more input,
the middleware repeats this until the handler completes, up to the same
limit of 10 rounds that the client-side middleware applies. This means a
handler written in the MRTR style works against both old and new clients
without code changes.
Example
The following example shows a "greet" tool whose handler asks the user
for their name via elicitation before producing the final greeting. The
handler runs twice — once to issue the elicitation, once to consume the
response — but the client call site sees a single CallTool returning the
final result, because the SDK's MRTR middleware handles the round trip on
either side of the wire (depending on the negotiated protocol version).
/* Detailed source-code truncated for AI context efficiency. */Cacheable list results
SEP-2549
adds ttlMs and cacheScope fields to the results of tools/list,prompts/list, resources/list, resources/templates/list, andresources/read. Both fields complement existing list
results: clients use ttlMs as a freshness hint to reduce polling,
and cacheScope ("public" or "private") controls whether shared
intermediaries may cache the response.
Server-side defaults. The SDK fills in CacheScope = "public" whenever a
result leaves it empty, since the field is required on the wire. Everything
else is left as produced: a ResourceHandler that sets TTLMs or CacheScope
on its ReadResourceResult keeps those values. The list results andserver/discover are built by the SDK itself, so they start out withTTLMs = 0, which a client cache treats as "immediately stale".
Setting a policy. ServerOptions.SetCacheable decides the fields for every
result that carries them. It runs after the corresponding handler, receives
what that handler produced, and may keep, adjust, or replace it. The request is
passed in so that policy can vary by method, session, or authorization context.
server := mcp.NewServer(impl, &mcp.ServerOptions{
SetCacheable: func(_ context.Context, req mcp.Request, c *mcp.Cacheable) {
// A 30s freshness hint, except where the handler chose its own.
if c.TTLMs == 0 {
c.TTLMs = 30_000
}
},
})SetCacheable may run with the server's lock held, so it must not call back
into the Server: adding or removing a feature, or ranging overServer.Sessions, deadlocks.
Utilities
Completion
To support the
completion
capability, the server needs a completion handler.
Client-side: completion is called using theClientSession.Complete
method.
Server-side: completion is enabled by settingServerOptions.CompletionHandler.
If this field is set to a non-nil value, the server will advertise thecompletions server capability, and use this handler to respond to completion
requests.
myCompletionHandler := func(_ context.Context, req *mcp.CompleteRequest) (*mcp.CompleteResult, error) {
// In a real application, you'd implement actual completion logic here.
// For this example, we return a fixed set of suggestions.
var suggestions []string
switch req.Params.Ref.Type {
case "ref/prompt":
suggestions = []string{"suggestion1", "suggestion2", "suggestion3"}
case "ref/resource":
suggestions = []string{"suggestion4", "suggestion5", "suggestion6"}
default:
return nil, fmt.Errorf("unrecognized content type %s", req.Params.Ref.Type)
}
return &mcp.CompleteResult{
Completion: mcp.CompletionResultDetails{
HasMore: false,
Total: len(suggestions),
Values: suggestions,
},
}, nil
}
// Create the MCP Server instance and assign the handler.
// No server running, just showing the configuration.
_ = mcp.NewServer(&mcp.Implementation{Name: "server"}, &mcp.ServerOptions{
CompletionHandler: myCompletionHandler,
})Logging
Note: The logging feature is deprecated as of protocol version
2026-07-28
(SEP-2577).
It remains fully functional during the deprecation window (at least twelve
months). The SDK continues to support logging for compatibility. Servers
should migrate to stderr logging (for STDIO transports) or OpenTelemetry.
MCP servers can send logging messages to MCP clients.
(This form of logging is distinct from server-side logging, where the
server produces logs that remain server-side, for use by server maintainers.)
Server-side:
The minimum log level is part of the server state.
For stateful sessions, there is no default log level: no log messages will be sent
until the client calls SetLoggingLevel (see below).
For legacy stateless sessions the level defaults to "info".
For sessionless 2026-07-28 requests there is no server-side default: each request
carries its level in _meta (io.modelcontextprotocol/logLevel), and nothing is
logged until the client sends one.
(Note: logging is deprecated as of 2026-07-28 / SEP-2577.)
ServerSession.Log is the low-level way for servers to log to clients.
It sends a logging notification to the client if the level of the message
is at least the minimum log level.
For a simpler API, use NewLoggingHandler to obtain a slog.Handler.
By setting LoggingHandlerOptions.MinInterval, the handler can be rate-limited
to avoid spamming clients with too many messages.
Servers always report the logging capability.
Client-side:
Set ClientOptions.LoggingMessageHandler to receive log messages.
Call ClientSession.SetLoggingLevel to change the log level for a session.
/* Detailed source-code truncated for AI context efficiency. */Capabilities
Server capabilities are advertised to clients during the initialization
handshake. By default, the SDK advertises only the logging capability.
Additional capabilities are automatically added when features are registered
(e.g., adding a tool adds the tools capability).
Capability inference
When features such as tools, prompts, or resources are added to the server
(e.g., via Server.AddTool), their capability is automatically inferred, with
default value {listChanged:true}. Similarly, if theServerOptions.SubscribeHandler or ServerOptions.CompletionHandler are set,
the corresponding capability is added.
Explicit capabilities
To explicitly declare capabilities, or to override the default inferred
capability, setServerOptions.Capabilities.
This sets the default server capabilities, before any capabilities are added
based on configured handlers. If a capability is already present as a field inCapabilities, adding a feature or handler will not change its configuration.
This allows you to:
- Disable default capabilities: Pass an empty
&ServerCapabilities{}to
disable all defaults, including logging. - Disable listChanged notifications: Set
ListChanged: falseon a
capability to prevent the server from sending list-changed notifications
when features are added or removed. - Pre-declare capabilities: Declare capabilities before features are
registered, useful for servers that load features dynamically.
// Disable listChanged notifications for tools
server := mcp.NewServer(impl, &mcp.ServerOptions{
Capabilities: &mcp.ServerCapabilities{
Logging: &mcp.LoggingCapabilities{},
Tools: &mcp.ToolCapabilities{ListChanged: false},
},
})Deprecated: The HasPrompts, HasResources, and HasTools fields onServerOptions are deprecated. Use Capabilities instead.
Extensions
SEP-2133
adds an extensions map to ServerCapabilities so that optional
capabilities outside the core protocol can be declared on the wire. Keys
are namespaced as "{vendor-prefix}/{extension-name}"; values are
per-extension settings objects.
Pagination
Server-side feature lists may be
paginated,
using cursors. The SDK supports this by default.
Client-side: The ClientSession provides methods returning
iterators for each feature type.
These iterators are an iter.Seq2[Feature, error], where the error value
indicates whether page retrieval failed.
ClientSession.Prompts
iterates prompts.ClientSession.Resource
iterates resources.ClientSession.ResourceTemplates
iterates resource templates.ClientSession.Tools
iterates tools.
The ClientSession also exposes ListXXX methods for fine-grained control
over pagination.
Server-side: pagination is on by default, so in general nothing is required
server-side. However, you may useServerOptions.PageSize
to customize the page size.
Troubleshooting (`docs/troubleshooting.md`)
Troubleshooting
The Model Context Protocol is a complicated spec that leaves some room for
interpretation. Client and server SDKs can behave differently, or can be more
or less strict about their inputs. And of course, bugs happen.
When you encounter a problem using the Go SDK, these instructions can help
collect information that will be useful in debugging. Please try to provide
this information in a bug report, so that maintainers can more quickly
understand what's going wrong.
And most of all, please do file bugs.
Using the MCP inspector
To debug an MCP server, you can use the MCP
inspector. This is
useful for testing your server and verifying that it works with the typescript
SDK, as well as inspecting MCP traffic.
Collecting MCP logs
For stdio transport connections, you can also
inspect MCP traffic using a LoggingTransport:
func ExampleLoggingTransport() {
ctx := context.Background()
t1, t2 := mcp.NewInMemoryTransports()
server := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil)
serverSession, err := server.Connect(ctx, t1, nil)
if err != nil {
log.Fatal(err)
}
defer serverSession.Close()
client := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, nil)
var b bytes.Buffer
logTransport := &mcp.LoggingTransport{Transport: t2, Writer: &b}
clientSession, err := client.Connect(ctx, logTransport, nil)
if err != nil {
log.Fatal(err)
}
defer clientSession.Close()
// Sort for stability: reads are concurrent to writes.
for _, line := range slices.Sorted(strings.SplitSeq(b.String(), "\n")) {
fmt.Println(line)
}
// Output:
// read: {"jsonrpc":"2.0","id":1,"result":{"resultType":"complete","_meta":{"io.modelcontextprotocol/serverInfo":{"name":"server","version":"v0.0.1"}},"ttlMs":0,"cacheScope":"public","supportedVersions":["2026-07-28","2025-11-25","2025-06-18","2025-03-26","2024-11-05"],"capabilities":{"logging":{}}}}
// write: {"jsonrpc":"2.0","id":1,"method":"server/discover","params":{"_meta":{"io.modelcontextprotocol/clientCapabilities":{"roots":{"listChanged":true}},"io.modelcontextprotocol/clientInfo":{"name":"client","version":"v0.0.1"},"io.modelcontextprotocol/protocolVersion":"2026-07-28"}}}
}That example uses a bytes.Buffer, but you can also log to a file, or toos.Stderr.
Inspecting HTTP traffic
There are a couple different ways to investigate traffic to an HTTP transport
(streamable or legacy SSE).
The first is to use an HTTP middleware:
func ExampleStreamableHTTPHandler_middleware() {
server := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.1.0"}, nil)
handler := mcp.NewStreamableHTTPHandler(func(r *http.Request) *mcp.Server {
return server
}, &mcp.StreamableHTTPOptions{Stateless: true})
loggingHandler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
// Example debugging; you could also capture the response.
body, err := io.ReadAll(req.Body)
if err != nil {
log.Fatal(err)
}
req.Body.Close() // ignore error
req.Body = io.NopCloser(bytes.NewBuffer(body))
fmt.Println(req.Method, string(body))
handler.ServeHTTP(w, req)
})
httpServer := httptest.NewServer(loggingHandler)
defer httpServer.Close()
// The SDK is currently permissive of some missing keys in "params".
mustPostMessage(`{"jsonrpc": "2.0", "id": 1, "method":"initialize", "params": {}}`, httpServer.URL)
// Output:
// POST {"jsonrpc": "2.0", "id": 1, "method":"initialize", "params": {}}
}The second is to use a general purpose tool to inspect http traffic, such as
wireshark or
tcpdump.
Internal Docs README.Src (`internal/docs/README.src.md`)
Overview
The SDK consists of several importable packages:
- The
github.com/modelcontextprotocol/go-sdk/mcp
package defines the primary APIs for constructing and using MCP clients and
servers. - The
github.com/modelcontextprotocol/go-sdk/jsonrpcpackage is for users implementing
their own transports. - The
github.com/modelcontextprotocol/go-sdk/auth
package provides some primitives for supporting OAuth. - The
github.com/modelcontextprotocol/go-sdk/oauthex
package provides extensions to the OAuth protocol, such as ProtectedResourceMetadata.
These docs mirror the official MCP spec.
Use the index below to learn how the SDK implements a particular aspect of the
protocol.
Base Protocol
Client Features
Server Features
TroubleShooting
See troubleshooting.md for a troubleshooting guide.
Backwards compatibility
See mcpgodebug.md for a list of backwards incompatible behavior changes
and description how they can be temporarily undone.
Rough edges
See rough_edges.md for a list of rough edges or API
oversights that can't be addressed due to our compatibility promise. We'll
revisit these if/when we move to a v2 of the SDK.
Internal Docs Client.Src (`internal/docs/client.src.md`)
Support for MCP client features
%toc
Roots
Note: The roots feature is deprecated as of protocol version 2026-07-28
(SEP-2577).
It remains fully functional during the deprecation window (at least twelve
months). The SDK continues to support roots for compatibility. New code
should pass paths via tool parameters, resource URIs, or configuration
instead.
MCP allows clients to specify a set of filesystem
"roots".
The SDK supports this as follows:
Client-side: The SDK client always has the roots.listChanged capability.
To add roots to a client, use theClient.AddRoots
andClient.RemoveRoots
methods. If any servers are already connected to the
client, a call to AddRoot or RemoveRoots will result in anotifications/roots/list_changed notification to each connected server.
Server-side: To query roots from the server, use theServerSession.ListRoots
method. To receive notifications about root changes, setServerOptions.RootsListChangedHandler.
For protocol versions 2026-07-28 and later, ListRoots requests are
delivered via the
Multi Round-Trip Requests
pattern.
%include ../../mcp/client_example_test.go roots -
Roots list changed
Client.AddRoots and Client.RemoveRoots notify every connected server that
the list changed. Servers observe this throughServerOptions.RootsListChangedHandler;
as with the server-side list-changed notifications, it reports only that
something changed, so read the list back with ServerSession.ListRoots.
%include ../../mcp/client_example_test.go rootslistchanged -
Sampling
Note: The sampling feature is deprecated as of protocol version
2026-07-28
(SEP-2577).
It remains fully functional during the deprecation window (at least twelve
months). The SDK continues to support sampling for compatibility. Servers
that need LLM completions should call LLM provider APIs directly.
Sampling
is a way for servers to leverage the client's AI capabilities. It is
implemented in the SDK as follows:
Client-side: To add the sampling capability to a client, setClientOptions.CreateMessageHandler.
This function is invoked whenever the server requests sampling.
Server-side: To use sampling from the server, callServerSession.CreateMessage.
For protocol versions 2026-07-28 and later, sampling requests are
delivered via the
Multi Round-Trip Requests
pattern.
%include ../../mcp/client_example_test.go sampling -
Elicitation
Elicitation
allows servers to request user inputs. It is implemented in the SDK as follows:
Client-side: To add the elicitation capability to a client, setClientOptions.ElicitationHandler.
The elicitation handler must return a result that matches the requested schema;
otherwise, elicitation returns an error. If your handler supports URL mode
elicitation,
you must declare that capability explicitly (see Capabilities)
Server-side: To use elicitation from the server, callServerSession.Elicit.
For protocol versions 2026-07-28 and later, elicitation requests are
delivered via the
Multi Round-Trip Requests
pattern.
%include ../../mcp/client_example_test.go elicitation -
Schema defaults and enums
ElicitParams.RequestedSchema is a flat schema of primitive fields, which the
client renders as a form. Two field keywords shape that form.
A Default (SEP-1034) prefills a
field. When the user accepts without supplying it, the SDK fills the field in
from the schema before the result reaches either side's caller — the client
does so after its elicitation handler returns, and ServerSession.Elicit does
so again on receipt. This is unconditional; there is no opt-in flag. Marking a
defaulted field Required defeats it: accepted content is validated against
the schema before defaults are applied, so an answer that omits the field is
rejected rather than defaulted.
An Enum (SEP-1330) restricts a
field to a fixed set of values, which the client renders as a choice. Enums
are supported only on "string" fields; declaring one on another type is
rejected. To label the choices, set the legacy enumNames keyword throughSchema.Extra, with exactly one name per enum value — a mismatched length is
rejected.
%include ../../mcp/client_example_test.go elicitationschema -
Completing a URL elicitation
In URL mode the user finishes out of band, in a browser, so nothing in the
elicitation result tells the client when they are done. The server signals that
withServerSession.NotifyElicitationComplete,
passing the same ElicitationID the request carried; the client observes it
throughClientOptions.ElicitationCompleteHandler.
Send it from whatever endpoint the hosted flow redirects back to.
The notification matters most when a handler rejects a request withURLElicitationRequiredError:
the client parks the original request until a notification names thatElicitationID, and then retries it automatically. Until one arrives, the
client waits.
%include ../../mcp/client_example_test.go elicitationcomplete -
Multi Round-Trip Requests
SEP-2322
introduces the MRTR pattern: server-to-client requests for sampling,
elicitation, and roots are no longer issued as fresh JSON-RPC requests but
are carried inside the in-flight reply of a tools/call, prompts/get, orresources/read. The client must respond by retrying the original request
with the produced responses.
The SDK installs clientMultiRoundTripMiddleware for every client by
default. The middleware:
- Inspects each
tools/call/prompts/get/resources/readreply. - If the result's
NeedsInput()is true, fans out theInputRequestsmap
concurrently, calling the configured handler for each (elicit,createMessage/createMessageWithTools, orlistRoots). - Threads the server-supplied opaque
RequestStateback unchanged. - Retries the original request with the responses set, repeating until the
result no longer needs input.
The middleware is enabled by default. To opt out, setClientOptions.MultiRoundTrip.Disabled = true;
the client will then surface input-required results directly to the caller
(the returned CallToolResult, GetPromptResult, or ReadResourceResult
will report NeedsInput() == true and expose the server's InputRequests
and opaque RequestState). Your code must fulfil each request and re-issue
the original call with InputResponses set and RequestState echoed
back.
For legacy (<= 2025-11-25) servers, the SDK transparently sends server
requests on the legacy server-initiated channel; the MRTR machinery is a
no-op in that direction. For legacy clients talking to MRTR-style servers,
the server SDK applies the inverse compatibility shim — see the
server-side documentation.
Capabilities
Client capabilities are advertised to servers during the initialization
handshake. By default, the SDK advertises the logging
capability. Additional capabilities are automatically added when server
features are added (e.g. via AddTool), or when handlers are set in theServerOptions struct (e.g., setting CompletionHandler adds thecompletions capability), or may be configured explicitly.
Capability inference
When handlers are set on ClientOptions (e.g., CreateMessageHandler orElicitationHandler), the corresponding capability is automatically added if
not already present, with a default configuration.
For elicitation, if the handler is set but no Capabilities.Elicitation is
specified, the client defaults to form elicitation. To enable URL elicitation
or both modes, configure Capabilities.Elicitation
explicitly.
See the ClientCapabilities
documentation
for further details on inference.
Explicit capabilities
To explicitly declare capabilities, or to override the default inferred
capability, setClientOptions.Capabilities.
This sets the initial client capabilities, before any capabilities are added
based on configured handlers. If a capability is already present inCapabilities, adding a handler will not change its configuration.
This allows you to:
- Disable default capabilities: Pass an empty
&ClientCapabilities{}to
disable all defaults, including roots. - Disable listChanged notifications: Set
ListChanged: falseon a
capability to prevent the client from sending list-changed notifications
when roots are added or removed. - Configure elicitation modes: Specify which elicitation modes (form, URL)
the client supports.
// Configure elicitation modes and disable roots.
client := mcp.NewClient(impl, &mcp.ClientOptions{
Capabilities: &mcp.ClientCapabilities{
Elicitation: &mcp.ElicitationCapabilities{
Form: &mcp.FormElicitationCapabilities{},
URL: &mcp.URLElicitationCapabilities{},
},
},
ElicitationHandler: handler,
})Extensions
SEP-2133
adds an extensions map to ClientCapabilities and ServerCapabilities so
that optional capabilities outside the core protocol can be declared on the
wire. Keys are namespaced as "{vendor-prefix}/{extension-name}"; values
are per-extension settings objects.
Internal Docs Mcpgodebug.Src (`internal/docs/mcpgodebug.src.md`)
Backwards compatibility and MCPGODEBUG
According to our compatibility promise, we can't break backward compatibility
of the SDK API. However, sometimes we need to change the behavior of the SDK
in a backward-incompatible way in order to fix bugs or security issues.
In those cases we introduce temporary compatibility parameters, that can be
used to opt-out of the new behavior. They are usually maintained for two
minor release cycles and then removed.
The compatibility parameters are provided via the MCPGODEBUG environment
variable. The value of the variable is a comma-separated list of parameter
value assignments, e.g.:
MCPGODEBUG=parameter1=value1,parameter2=value2`MCPGODEBUG` history
1.9.0
Options listed below were added and will be removed in the 1.11.0 version of the SDK.
structuredcontentfloat64added. If set to1, numbers inCallToolResult.StructuredContentare decoded asfloat64, restoring the
previous behavior. The default behavior decodes numbers asjson.Numberso
that large integers and other exact JSON values do not silently lose
precision.disablepromptargsvalidationadded. If set to1,Server.getPromptwill
call the prompt handler even when a required argument of the prompt is
missing from the request, restoring the previous behavior. The default
behavior was changed to reject such requests with-32602(Invalid Params),
as the specification asks.allowmissingclientparamsadded. If set to1, aClientacceptselicitation/create,notifications/elicitation/completeandnotifications/resources/updatedmessages whoseparamsmember is missing
ornull, restoring the previous behavior (the handler then observes nil
params, which crashed the built-in elicitation handlers). The default
behavior was changed to reject such messages as invalid requests, as the
specification listsparamsas required for all three.nopreinitinvalidrequestadded. If set to1, a legacy server session that
has not been initialized refuses a request other thaninitializeorping
with JSON-RPC error code0, restoring the previous behavior. The default
behavior was changed to refuse it with-32600(Invalid Request):0
tells a client nothing and is indistinguishable from an unset code. See
issue #1367.nointernalerrorcodeadded. If set to1, an error returned by a request
handler that carries no JSON-RPC error code is sent with code0, restoring
the previous behavior. The default behavior was changed to send it with-32603(Internal error):0tells the peer nothing and is
indistinguishable from an unset code. The error's message is unchanged.
See issue #1394.fatalnewprotocolerrorsadded. If set to1, a streamable HTTP client
permanently fails its connection when an independent2026-07-28request
fails, restoring the previous behavior. By default, the failure is returned
only to the affected call.
1.8.0
Options listed below were added and will be removed in the 1.9.0 version of the SDK.
plaintextstatefulrejectionadded. If set to1, a statefulStreamableHTTPHandlerwill respond with a plain-texthttp.Error400 body
when it receives a request carrying per-request metadata (i.e. anio.modelcontextprotocol/protocolVersion_metafield, or anMCP-Protocol-Versionheader >=2026-07-28), restoring the previous
behavior. The default behavior was changed so that the server responds with a
JSON-RPC error of codeCodeUnsupportedProtocolVersion(-32022) carrying
anUnsupportedProtocolVersionDatapayload that advertises the legacy
versions the server supports. This lets the client's
existing renegotiation logic recover and prevents the failure from tearing
down the underlying connection.blockingcancelnotifyadded. If set to1, a cancelled call waits
synchronously for the best-effortnotifications/cancelledmessage to be
delivered (up tonotifyCancellationTimeout, currently 5 s) before
returning to the caller, restoring the previous behavior. The delivery
error is joined into the caller's returned error. The default behavior was
changed so that the call is retired immediately and the notification is
sent asynchronously off the caller's return path: the caller returns as
soon as its context is cancelled and cannot be delayed by a slow or
unresponsive peer. See issue #1150.
Options below were removed, according to plan:
seterroroverwrite.SetErrornow always preserves existingContent.enableoriginverification. A nilStreamableHTTPOptions.CrossOriginProtectionnow always means no
cross-origin protection. Wrap the handler with cross-origin protection
middleware instead.disablecontenttypecheck. Content-Type validation on HTTP POST requests can
no longer be disabled.disablelocalhostprotection, whose removal had been postponed to this
release. Set theDisableLocalhostProtectionfield inStreamableHTTPOptionsorSSEOptionsto opt out of DNS rebinding
protection.
1.7.0
Options listed below were added and will be removed in the 1.9.0 version of the SDK.
customresnotfounderrcodeadded. If set to1,ResourceNotFoundErrorwill
use the custom error code-32002instead of the standard-32602(Invalid
Params), restoring the previous behavior. The default behavior was changed to
align with SEP-2164 and the JSON-RPC specification.hintomitemptyadded. If set to1,ToolAnnotationsJSON marshaling
will omitReadOnlyHintandIdempotentHintwhen their value isfalse,
restoring the previous behavior. The default behavior was changed to always
serialize these fields, since their Go types are barebool(not*bool)
and omittingfalsemade it indistinguishable from unset.allowsessionsinstatelessadded. If set to1, stateless streamable HTTP
servers will read theMcp-Session-Idrequest header (or generate one viaGetSessionID), set it on response headers, and acceptDELETErequests,
restoring the previous behavior. The default behavior was changed so that
stateless servers ignore session IDs entirely and rejectDELETEwith 405.nomethodnotfoundcodeinerroradded. If set to1, the jsonrpc2 layer will not
include the MethodNotFound Error (-32601) in the error response when the
requested method in STDIO transport is not found. The default behavior was
changed to include the MethodNotFound Error in the error response when the
requested method in STDIO transport is not found.noprotocolerrorbodyadded. If set to1, the streamable HTTP client will
not attempt to decode the JSON-RPC error body of a non-2xx HTTP response,
and any non-transient error will permanently fail the connection, restoring
the previous behavior. The default behavior was changed so that the client
attempts to decode the JSON-RPC error body of a non-2xx response, surfaces
the underlying JSON-RPC error, and wraps it withjsonrpc2.ErrRejectedso
that per-call rejections do not tear down the session.nowrapinvalidparamsadded. If set to1, the server will not wrap
params-decoding failures withjsonrpc2.ErrInvalidParams, so wire responses
carry the zero-value error code0instead of-32602("invalid params"),
restoring the previous behavior. The default behavior was changed so that
the server always reports params-decoding failures with the standard-32602code.disablecompleteparamsvalidationadded. If set to1,Server.complete
will not validate that the requiredrefandargument.namefields onCompleteParamsare present, restoring the previous behavior of dispatching
the request to the completion handler unconditionally. The default behavior
was changed to reject malformed requests with-32602(Invalid Params).
1.6.1
Options listed below were added and will be removed in the 1.8.0 version of the SDK.
disablecontenttypecheckadded. If set to1, Content-Type validation on
HTTP POST requests will be disabled, allowing requests with non-JSON or missing
Content-Type headers. The default behavior is to validate that HTTP POST
requests have Content-Type: application/json.
1.6.0
Options listed below were added and will be removed in the 1.8.0 version of the SDK.
seterroroverwriteadded. If set to1,SetErrorwill always overwriteContentwith the error text, restoring the previous behavior. The default
behavior was changed to preserve existingContentif it has already been
populated.enableoriginverificationadded. If set to1, default (zero-value)
cross-origin protection will be applied whenStreamableHTTPOptions.CrossOriginProtectionis nil, restoring the
behavior from v1.4.1-v1.5.0. The default behavior was changed to not
enable cross-origin protection.disablelocalhostprotectionremoval was postponed until 1.8.0, as it is now
also used in the SSE transport.
Options below were removed:
jsonescaping, according to plan,disablecrossoriginprotection, it was replaced byenableoriginverificationafter the default was changed to not enable
cross-origin protection.
1.4.1
Options listed below will be removed in the 1.6.0 version of the SDK.
disablecrossoriginprotectionadded. If set to1, newly added cross-origin
protection will be disabled. The default behavior was changed to enable
cross-origin protection.
1.4.0
Options listed below will be removed in the 1.6.0 version of the SDK.
jsonescapingadded. If set to1, JSON marshaling will preserve the previous
behavior of escaping HTML characters in JSON strings. The default behavior
was changed to not escape HTML characters, to be consistent with other SDKs.disablelocalhostprotectionadded. If set to1, newly added DNS rebinding
protection will be disabled. The default behavior was changed to enable DNS rebinding
protection. The protection can also be disabled by setting theDisableLocalhostProtectionfield in theStreamableHTTPOptionsorSSEOptionsstruct totrue, which is the recommended way to disable
the protection long term. Removal of this option was postponed until 1.8.0.
Internal Docs Protocol.Src (`internal/docs/protocol.src.md`)
Support for the MCP base protocol
%toc
Lifecycle
The SDK provides an API for defining both MCP clients and servers, and
connecting them over various transports. When a client and server are
connected, it creates a logical session.
The MCP specification defines two lifecycle models, and the SDK supports both
transparently based on the negotiated protocol version:
- The legacy
initializehandshake
(lifecycle)
used by protocol versions through2025-11-25. - A stateless model introduced in
2026-07-28by
SEP-2575,
in which there is noinitialize/notifications/initializedhandshake, and
each request carries its protocol version and client capabilities in_meta. Clients SHOULD also include their identity (clientInfo) on every
request, and servers SHOULD include their identity (serverInfo) on every
response.
In both models, the SDK exposes the same API:
- A
Clientis a logical MCP client, configured with variousClientOptions. - When a client is connected to a server using
Client.Connect,
it creates aClientSession.
This session is initialized during theConnectmethod, and provides methods
to communicate with the server peer. - A
Serveris a logical MCP server, configured with variousServerOptions. - When a server is connected to a client using
Server.Connect,
it creates aServerSession.
In the legacy model, the server session is not considered initialized until
the client sends the notifications/initialized message. UseServerOptions.InitializedHandler to listen for this event, or just use the
session through various feature handlers (such as a ToolHandler). Requests
to the server are rejected until the client has initialized the session.
In the stateless model (2026-07-28+), there is no handshake. The server
processes the first request the moment it arrives, validates the per-request_meta fields.
Both ClientSession and ServerSession have a Close method to terminate the
session, and a Wait method to await session termination by the peer. Typically,
it is the client's responsibility to end the session.
%include ../../mcp/mcp_example_test.go lifecycle -
Discovery
Introduced in 2026-07-28 by
SEP-2575,
the server/discover RPC lets a client discover a server's supported
protocol versions, capabilities, and identity before issuing any other
request. Servers implementing 2026-07-28 MUST implement it.
- Server:
Server.Connectregisters theserver/discoverhandler automatically; the response is computed from the
server's static configuration (capabilities, server info, instructions) and
the transport-filtered list of supported protocol versions. - Client:
Client.Connectcallsserver/discoverfirst and
uses the result to negotiate a mutually supported version. If discovery
fails or the server does not support the latest version, the client falls back to the
legacyinitializehandshake.
A server advertises and negotiates every protocol version the SDK supports;
set ServerOptions.SupportedProtocolVersions
to narrow that set, for example to stop serving a revision your deployment has
retired.
server := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v1.0.0"}, &mcp.ServerOptions{
SupportedProtocolVersions: []string{"2026-07-28", "2025-11-25"},
})The set can only narrow support, never widen it; the field's documentation
covers how each protocol era answers a request at an excluded version.
Per-request metadata keys
When the negotiated protocol version is 2026-07-28 or later, every request
carries these keys inside its _meta map (constants live inmcp/protocol.go):
| Constant | Wire key | Type | Required |
|---|---|---|---|
MetaKeyProtocolVersion |
io.modelcontextprotocol/protocolVersion |
string |
Yes |
MetaKeyClientCapabilities |
io.modelcontextprotocol/clientCapabilities |
*ClientCapabilities |
Yes |
MetaKeyClientInfo |
io.modelcontextprotocol/clientInfo |
*Implementation |
No |
MetaKeyLogLevel |
io.modelcontextprotocol/logLevel |
LoggingLevel (deprecated by SEP-2577) |
No |
The client populates the required keys automatically on every outgoing
request, and populates clientInfo when configured with an *Implementation
(the default). Server-side handlers can read them throughServerRequest[P].ProtocolVersion(), ServerRequest[P].ClientInfo(), andServerRequest[P].ClientCapabilities().
Per-response metadata keys
Under the same protocol version, servers SHOULD identify themselves on every
response. The SDK populates this
key automatically on every outgoing response:
| Constant | Wire key | Type | Required |
|---|---|---|---|
MetaKeyServerInfo |
io.modelcontextprotocol/serverInfo |
*Implementation |
No |
Subscriptions
Introduced in 2026-07-28 by
SEP-2575,subscriptions/listen replaces the legacy resources/subscribe RPC and
the GET-based SSE endpoint with a single long-lived request that
multiplexes every kind of server-to-client change notification a client
opts in to. On the wire the client sends one subscriptions/listen
request whose notifications field enumerates what it wants
(toolsListChanged, promptsListChanged, resourcesListChanged, and/or
a list of resource URIs in resourceSubscriptions); the server replies
first with a notifications/subscriptions/acknowledged notification
reporting the honored subset, then streams every change notification on
the same request, and finally closes with a SubscriptionsListenResult
when it tears the subscription down.
Transports
A
transport
can be used to send JSON-RPC messages from client to server, or vice-versa.
In the SDK, this is achieved by implementing theTransport
interface, which creates a (logical) bidirectional stream of JSON-RPC messages.
Most transport implementations described below are specific to either the
client or server: a "client transport" is something that can be used to connect
a client to a server, and a "server transport" is something that can be used to
connect a server to a client. However, it's possible for a transport to be both
a client and server transport, such as the InMemoryTransport used in the
lifecycle example above.
Transports should not be reused for multiple connections: if you need to create
multiple connections, use different transports.
Stdio Transport
In thestdio
transport clients communicate with an MCP server running in a subprocess using
newline-delimited JSON over its stdin/stdout.
Client-side: the client side of the stdio transport is implemented byCommandTransport,
which starts the a exec.Cmd as a subprocess and communicates over its
stdin/stdout.
Server-side: the server side of the stdio transport is implemented byStdioTransport,
which connects over the current processes os.Stdin and os.Stdout.
Streamable Transport
The streamable
transport
API is implemented across three types:
StreamableHTTPHandler: anhttp.Handlerthat serves streamable MCP
sessions.StreamableServerTransport: aTransportthat implements the server side of
the streamable transport.StreamableClientTransport: aTransportthat implements the client side of
the streamable transport.
To create a streamable MCP server, you create a StreamableHTTPHandler and
pass it an mcp.Server:
%include ../../mcp/streamable_example_test.go streamablehandler -
The StreamableHTTPHandler handles the HTTP requests and creates a newStreamableServerTransport for each new session. The transport is then used to
communicate with the client.
On the client side, you create a StreamableClientTransport and use it to
connect to the server:
transport := &mcp.StreamableClientTransport{
Endpoint: "http://localhost:8080/mcp",
}
client := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v1.0.0"}, nil)
session, err := client.Connect(ctx, transport, nil)The StreamableClientTransport handles the HTTP requests and communicates with
the server using the streamable transport protocol.
HTTP Headers
SEP-2243
standardises a small set of MCP-specific HTTP headers, sent and validated by
the SDK only for sessions negotiated on protocol version 2026-07-28 or
later.
| Header | Direction | Purpose |
|---|---|---|
Mcp-Protocol-Version |
request | Mirrors _meta.io.modelcontextprotocol/protocolVersion from the body |
Mcp-Session-Id |
request/response | Logical session identifier (removed in stateless mode) |
Mcp-Method |
request | Mirrors the JSON-RPC method field; mismatch ⇒ -32020 |
Mcp-Name |
request | Mirrors the request's principal name (tools/call.params.name, prompts/get.params.name, resources/read.params.uri); mismatch ⇒ -32020 |
Mcp-Param-{Header} |
request | Per-tool parameter passthrough; see below |
The header-name set is exhaustive: for protocol versions earlier than2026-07-28, only Mcp-Protocol-Version and Mcp-Session-Id are recognised.
Body↔header mirroring. When Mcp-Method or Mcp-Name is present but
does not match the JSON-RPC body, the server returnsCodeHeaderMismatch (-32020). The same applies to a
mismatch between Mcp-Protocol-Version and the body's_meta.io.modelcontextprotocol/protocolVersion.
Mcp-Param-* passthrough. A tool may annotate properties of itsInputSchema with x-mcp-header: "<HeaderName>". When a client invokes that
tool over HTTP, the SDK serializes the matching argument(s) asMcp-Param-<HeaderName> request headers (using =?base64?...?= encoding for
non-ASCII values). On the server, the headers are validated against the body
and either accepted or rejected with -32020. Properties so annotated must
be typed string, integer, or boolean; tools that fail validation at
registration time are silently dropped from tools/list.
Resumability and Redelivery
By default, the streamable server does not support resumability or
redelivery
of messages, because doing so requires either a persistent storage solution or
unbounded memory usage (see also
#580).
To enable resumability, set StreamableHTTPOptions.EventStore to a non-nil
value. The SDK provides a MemoryEventStore for testing or simple use cases;
for production use it is generally advisable to use a more sophisticated
implementation.
Note: SEP-2575
removes SSE stream resumability (Last-Event-ID, SSE event IDs) for protocol
version2026-07-28. The SDK preserves theEventStoreandLast-Event-ID
code paths for backward compatibility with2025-11-25and earlier; on2026-07-28sessions, a broken response stream loses the in-flight request
and the client must re-issue it as a new request with a new ID.
Stateless Mode
The streamable server supports a stateless mode by settingStreamableHTTPOptions.Stateless,
which is where the server does not perform any validation of the session id,
and uses a temporary session to handle requests. In this mode, it is impossible
for the server to make client requests, as there is no way for the client's
response to reach the session.
However, it is still possible for the server to access the ServerSession.ID
to see the logical session
[!WARNING]
Stateless mode is not directly discussed in the spec, and is still being
defined. See modelcontextprotocol/modelcontextprotocol#1364,
modelcontextprotocol/modelcontextprotocol#1372, or
modelcontextprotocol/modelcontextprotocol#1442 for potential refinements.
Required for
2026-07-28: the streamable HTTP transport accepts
requests at protocol version2026-07-28only whenStateless = true.
Requests at that version against a non-stateless handler are rejected.
See examples/server/distributed for
an example using stateless mode to implement a server distributed across
multiple processes.
Legacy SSE Transport
Before the streamable transport, the
2024-11-05
spec defined an HTTP transport built from two endpoints: a hanging GET that
streams server-to-client messages as server-sent events, and a per-session
endpoint the client POSTs its messages to. The SDK still implements it, for
talking to peers that predate the streamable transport. New deployments should
use the streamable transport instead.
It is implemented across three types, mirroring the streamable ones:
SSEHandler:
anhttp.Handlerthat creates a session per incoming GET and routes POSTs to
the right session. Construct it withNewSSEHandler,
which takes a function returning theServerto serve a given request.SSEServerTransport:
one such session. Reach for it directly only when serving the two endpoints
yourself;SSEHandlercreates these for you.SSEClientTransport:
the client side, which needs only theEndpointof the GET.
%include ../../mcp/sse_example_test.go ssehandler -
See examples/server/sse
for a standalone server.
Custom transports
The SDK supports custom
transports
by implementing theTransport
interface: a logical bidirectional stream of JSON-RPC messages.
Full example: examples/server/custom-transport.
Concurrency
In general, MCP offers no guarantees about concurrency semantics: if a client
or server sends a notification, the spec says nothing about when the peer
observes that notification relative to other request. However, the Go SDK
implements the following heuristics:
- If a notifying method (such as
notifications/progressornotifications/initialized) returns, then it is guaranteed that the peer
observes that notification before other notifications or calls from the same
client goroutine. - Calls (such as
tools/call) are handled asynchronously with respect to
each other.
See
modelcontextprotocol/go-sdk#26
for more background.
Authorization
Server
To write an MCP server that performs authorization,
use RequireBearerToken.
This function is middleware that wraps an HTTP handler, such as the one returned
by NewStreamableHTTPHandler, to provide support for verifying bearer tokens.
The middleware function checks every request for an Authorization header with a bearer token,
and invokes theTokenVerifier
passed to RequireBearerToken to parse the token and perform validation.
The middleware function checks expiration and scopes (if they are provided inRequireBearerTokenOptions.Scopes), so theTokenVerifier doesn't have to.
If RequireBearerTokenOptions.ResourceMetadataURL is set and verification fails,
the middleware function sets the WWW-Authenticate header as required by the Protected Resource
Metadata spec.
Server handlers, such as tool handlers, can obtain the TokenInfo returned by the TokenVerifier
from req.Extra.TokenInfo, where req is the handler's request. (For example, aCallToolRequest.)
HTTP handlers wrapped by the RequireBearerToken middleware can obtain the TokenInfo from the context
with auth.TokenInfoFromContext.
OAuth Protected Resource Metadata
Servers implementing OAuth 2.0 authorization should expose a protected resource metadata endpoint
as specified in RFC 9728. This endpoint provides
clients with information about the resource server's OAuth configuration, including which
authorization servers can be used and what scopes are supported.
The SDK provides ProtectedResourceMetadataHandler
to serve this metadata. The handler automatically sets CORS headers (Access-Control-Allow-Origin: *)
to support cross-origin client discovery, as the metadata contains only public configuration information.
Example usage:
metadata := &oauthex.ProtectedResourceMetadata{
Resource: "https://example.com/mcp",
AuthorizationServers: []string{
"https://auth.example.com/.well-known/openid-configuration",
},
ScopesSupported: []string{"read", "write"},
}
http.Handle("/.well-known/oauth-protected-resource",
auth.ProtectedResourceMetadataHandler(metadata))For more sophisticated CORS policies, wrap the handler with a CORS middleware like
github.com/rs/cors or github.com/jub0bs/cors.
The auth middleware example shows how to implement authorization for both JWT tokens and API keys.
Client
Client-side authorization is supported via theStreamableClientTransport.OAuthHandler
field. If the handler is provided, the transport will automatically use it to
add an Authorization: Bearer <token> header to every request. The transport
will also call the handler's Authorize method if the server returns401 Unauthorized or 403 Forbidden errors to perform the authorization flow
or facilitate scope step-up authorization.
The SDK implements the Authorization Code flow inauth.AuthorizationCodeHandler.
This handler supports:
- Client ID Metadata Documents
- Pre-registered clients
- Dynamic Client Registration
- RFC 9207 Authorization Server Issuer Identification
To use it, configure the handler and assign it to the transport:
authHandler, _ := auth.NewAuthorizationCodeHandler(&auth.AuthorizationCodeHandlerConfig{
RedirectURL: "https://myapp.com/oauth2-callback",
// Configure one of the following:
// ClientIDMetadataDocumentConfig: ...
// PreregisteredClientConfig: ...
// DynamicClientRegistrationConfig: ...
AuthorizationCodeFetcher: func(ctx context.Context, args *auth.AuthorizationArgs) (*auth.AuthorizationResult, error) {
// Open the args.URL in a browser and return the resulting code, state, and iss.
// See full example in examples/auth/client/main.go.
code := ...
state := ...
iss := ... // "iss" query parameter from the redirect URI (RFC 9207)
return &auth.AuthorizationResult{Code: code, State: state, Iss: iss}, nil
},
})
transport := &mcp.StreamableClientTransport{
Endpoint: "https://example.com/mcp",
OAuthHandler: authHandler,
}
client := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, nil)
session, err := client.Connect(ctx, transport, nil)The auth.AuthorizationCodeHandler automatically manages token refreshing (if the server provides a refresh token) and step-up authentication (when the server returns insufficient_scope error).
Enterprise Managed Authorization (SEP-990)
For enterprise SSO scenarios where users authenticate with an enterprise Identity Provider (IdP),
the SDK providesextauth.EnterpriseHandler,
an implementation of OAuthHandler that automates the Enterprise Managed Authorization flow:
- OIDC Login: User authenticates with enterprise IdP → ID Token
- Token Exchange (RFC 8693): ID Token → ID-JAG at IdP
- JWT Bearer Grant (RFC 7523): ID-JAG → Access Token at MCP Server
To use enterprise managed authorization, create an EnterpriseHandler and assign it to your transport:
/* Detailed source-code truncated for AI context efficiency. */The EnterpriseHandler automatically manages the token exchange flow. Note that it intentionally does not support refresh tokens - when an access token expires, the entire authorization flow is repeated to ensure enterprise policies are consistently enforced.
For a complete working example, see examples/auth/enterprise.
Security
Here we discuss the mitigations described under
the MCP's Security Best Practices section, and how we handle them.
Confused Deputy
The mitigation,
obtaining user consent for dynamically registered clients, is mostly the
responsibility of the MCP Proxy server implementation. The SDK client does
generate cryptographically secure random state values for each authorization
request by default and validates them when the authorization code is returned.
Mismatched state values will result in an error.
Token Passthrough
The mitigation, accepting only tokens that were issued for the server, depends on the structure
of tokens and is the responsibility of theTokenVerifier
provided toRequireBearerToken.
Server-Side Request Forgery
The OAuth discovery helpers apply these mitigations by default:
Enforce HTTPS. Reject
http://URLs (except loopback:localhost,127.0.0.1,::1)
and redirects that downgradehttpsto a non-httpsscheme.Block Private IP Ranges. Reject targets that resolve to a non-public address
(private, link-local including the169.254.169.254metadata endpoint, CGNAT, multicast,
unspecified). The check runs on the initial URL and at dial time, guarding against DNS rebinding.Validate Redirect Targets. Reject redirects to non-public addresses and cap the redirect count.
Use Egress Proxies. Out of scope for the SDK; configure separately.
Opting out. Passing a custom http.Client can bypass these checks, making SSRF
protection your responsibility. Specifically: a custom Transport.DialContext/DialTLSContext,
a non-*http.Transport, or a configured Proxy disables the dial-time IP check; settingCheckRedirect replaces all redirect validation. In these cases, implement your own IP
blocking, redirect validation, and DNS pinning.
Session Hijacking
The mitigations are as follows:
Verify all inbound requests. The
RequireBearerToken
middleware function will verify all HTTP requests that it receives. It is the
user's responsibility to wrap that function around all handlers in their server.Secure session IDs. This SDK generates cryptographically secure session IDs by default.
If you create your own withServerOptions.GetSessionID, it is your responsibility to ensure they are secure.
We recommend usingcrypto/rand.Text.Binding session IDs to user information. The SDK supports this mitigation through
TokenInfo.UserID.
When aTokenVerifier
setsUserIDon the returnedTokenInfo, the streamable transport will:- Store the user ID when a new session is created.
- Verify that subsequent requests to that session include a token with the same
UserID. - Reject requests with a 403 Forbidden if the user ID doesn't match.
Recommendation: If your
TokenVerifiercan extract a user identifier from the token
(such as asubclaim in a JWT, or a user ID associated with an API key), setTokenInfo.UserIDto enable this protection. This prevents an attacker with a valid
token from hijacking another user's session by guessing or obtaining their session ID.
Issuer Mix-Up
The mitigation against issuer mix-up attacks is
implemented per RFC 9207. The SDK client validates
the iss parameter in authorization responses to ensure they originated from the expected
authorization server:
- If
issis present in the redirect URI, the SDK verifies it matches the issuer from the
authorization server's metadata. A mismatch results in an error. - If
issis absent but the authorization server advertisesauthorization_response_iss_parameter_supported: truein its RFC 8414
metadata, the SDK rejects the response with an error.
The AuthorizationCodeFetcher is responsible for extracting the iss query parameter from
the redirect URI and returning it in AuthorizationResult.Iss.
Utilities
Cancellation
Cancellation is implemented with context cancellation. Cancelling a context
used in a method on ClientSession or ServerSession will terminate the RPC
and send a "notifications/cancelled" message to the peer.
When an RPC exits due to a cancellation error, there's a guarantee that the
cancellation notification has been sent, but there's no guarantee that the
server has observed it (see concurrency).
On a session using protocol version 2026-07-28 or later over a stdio-style
transport (StdioTransport, CommandTransport, IOTransport orInMemoryTransport), a server that receives a cancellation notification sends
no response for the cancelled request, as the spec requires. Over the
streamable HTTP transport, a 2026-07-28 request is cancelled by closing its
HTTP request instead (see StreamableHTTPOptions.PropagateRequestCancellation).
%include ../../mcp/mcp_example_test.go cancellation -
Ping
Ping
support is symmetrical for client and server.
To initiate a ping, callClientSession.Ping
orServerSession.Ping.
To have the client or server session automatically ping its peer, and close the
session if the ping fails, setClientOptions.KeepAlive
orServerOptions.KeepAlive.
Note:
pingis removed from the protocol as of2026-07-28by
SEP-2575.
The SDK preserves the API for backward compatibility with legacy clients;
when a session negotiates2026-07-28or later,PingreturnsMethodNotFound(-32601) locally, without a round trip, andKeepAliveshould not be enabled.
Progress
Progress
reporting is possible by reading the progress token from request metadata and
calling eitherClientSession.NotifyProgress
orServerSession.NotifyProgress.
To listen to progress notifications, setClientOptions.ProgressNotificationHandler
orServerOptions.ProgressNotificationHandler.
Issue #460 discusses some potential ergonomic improvements to this API.
%include ../../mcp/mcp_example_test.go progress -
Error codes
The SDK uses the standard JSON-RPC base codes (parse error -32700,
invalid request -32600, method not found -32601, invalid params-32602, internal error -32603) plus the following MCP-specific codes:
| Constant | Code | Meaning |
|---|---|---|
CodeHeaderMismatch |
-32020 |
An MCP HTTP header (Mcp-Method, Mcp-Name, Mcp-Protocol-Version, or Mcp-Param-*) does not match the JSON-RPC body |
CodeMissingRequiredClientCapabilities |
-32021 |
Server requires client capabilities the client did not declare |
CodeUnsupportedProtocolVersion |
-32022 |
Requested protocol version not supported. Data: UnsupportedProtocolVersionData{Supported []string, Requested string} |
CodeResourceNotFound |
-32602 |
Resource URI not found (variable; was -32002 before SEP-2164) |
The error code allocation policy defined in 2026-07-28
(SEP-2575)
partitions the JSON-RPC server-error range:
-32000to-32019: implementation-defined; existing SDK usage is grandfathered.-32020to-32099: reserved for the MCP specification.
Internal Docs Quick Start.Src (`internal/docs/quick_start.src.md`)
Quick Start
%toc
Installation
go get github.com/modelcontextprotocol/go-sdk/mcpGetting started
To get started creating an MCP server, create an mcp.Server instance, add
features to it, and then run it over an mcp.Transport. For example, this
server adds a single simple tool, and then connects it to clients over
stdin/stdout:
%include ../readme/server/server.go -
To communicate with that server, create an mcp.Client and connect it to the
corresponding server, by running the server command and communicating over its
stdin/stdout:
%include ../readme/client/client.go -
The examples/ directory contains more example clients and
servers.
Internal Docs Rough Edges.Src (`internal/docs/rough_edges.src.md`)
Rough edges: API decisions to reconsider for v2
This file collects a list of API oversights or rough edges that we've uncovered
post v1.0.0, along with their current workarounds. These issues can't be
addressed without breaking backward compatibility, but we'll revisit them for
v2.
EventStore.Openis unnecessary. This was an artifact of an earlier version
of the SDK where event persistence and delivery were combined.Workaround:
Openmay be implemented as a no-op.Eventneed not have been exported: it's an implementation detail of the SSE
and streamable transports. Also the 'Name' field is a misnomer: it should be
'event'.Enforcing valid tool names: with
SEP-986
landing after the SDK was at v1, we missed an opportunity to panic on invalid
tool names. Instead, we have to simply produce an error log. In v2, we should
panic.Inconsistent naming.
ResourceUpdatedNotificationsParamsshould probably have just beenResourceUpdatedParams, as we don't include the word 'notification' in
other notification param types.- Similarly,
ProgressNotificationParamsshould probably have beenProgressParams.
AudioContent.MarshalJSONshould have had a pointer receiver, to be
consistent with other content types.ClientCapabilities.Rootsshould have been a distinguished struct pointer
(see #607).Workaround: use
ClientCapabilities.RootsV2, which aligns with the
semantics of other capability fields.Default capabilities should have been empty. Instead, servers default to
advertisinglogging, and clients default to advertisingrootswithlistChanged: true. This is confusing because a nilCapabilitiesfield
does not mean "no capabilities".Workaround: to advertise no capabilities, set
ServerOptions.CapabilitiesorClientOptions.Capabilitiesto an empty&ServerCapabilities{}or&ClientCapabilities{}respectively.CreateMessageResult.Contentis singularContent, but the 2025-11-25 spec
allowscontentto be a single block or an array (for parallel tool calls).
We addedCreateMessageResultWithTools(withContent []Content) as a
workaround, matching the TypeScript SDK's approach. In v2,CreateMessageResultshould use[]Contentdirectly. Similarly,SamplingMessage.Contentshould become[]Contentto support sending
multiple tool_result blocks in a single user message.We didn't actually need CallToolParams and CallToolParamsRaw, since even when
we're unmarshalling into a custom Go type (for the mcp.AddTool convenience
wrapper) we need to first unmarshal into amap[string]anyin order to do
server-side validation of required fields. CallToolParams could have just had
a map[string]any.StreamableHTTPOptions.CrossOriginProtectionshould not have been part of
the SDK API. Cross-origin protection is a general HTTP concern, not specific
to MCP, and can be applied as standard HTTP middleware.ToolAnnotations(mcp/protocol.go) should have all fields typed as*bool
for full control to define what is being sent over the wire. Different
MCP clients have different requirements, and some of them require all fields
to be explicitly set to eithertrueorfalse.
Internal Docs Server.Src (`internal/docs/server.src.md`)
Support for MCP server features
%toc
Prompts
MCP servers can provide LLM prompt templates (called simply
prompts)
to clients. Every prompt has a required name which identifies it, and a set of
named arguments, which are strings.
Client-side: To list the server's prompts, use theClientSession.Prompts
iterator, or the lower-levelClientSession.ListPrompts
(see pagination below). SetClientOptions.PromptListChangedHandler
to be notified of changes in the list of prompts.
CallClientSession.GetPrompt
to retrieve a prompt by name, providing arguments for expansion.
Server-side: UseServer.AddPrompt
to add a prompt to the server along with its handler.
The server will have the prompts capability if any prompt is added before the
server is connected to a client, or ifServerOptions.HasPrompts
is explicitly set. When a prompt is added, any clients already connected to the
server will be notified via a notifications/prompts/list_changed
notification.
%include ../../mcp/server_example_test.go prompts -
Prompt message content
APromptMessage
has two fields: a Role, either "user" or "assistant", and a singleContent. That is the same interface a tool result uses (see Tool result
content), so a prompt message can hold an image, audio,
or a resource's contents as readily as text.
Media on its own says nothing about what the model should do with it, so send a
text message alongside it. Embedding a resource puts its contents directly in
the message, sparing the client a separate resources/read. Note thatResourceContents.URI is not checked against the resources the server has
registered: it records where the contents came from, so use the URI of a real
resource when there is one, as in the example below.
%include ../../mcp/server_example_test.go promptcontent -
Resources
In MCP terms, a resource is some data referenced by a URI.
MCP servers can serve resources to clients.
They can register resources individually, or register a resource template
that uses a URI pattern to describe a collection of resources.
Client-side:
Call ClientSession.ReadResource
to read a resource.
The SDK ensures that a read succeeds only if the URI matches a registered resource exactly,
or matches the URI pattern of a resource template.
To list a server's resources and resource templates, use theClientSession.Resources
andClientSession.ResourceTemplates
iterators, or the lower-level ListXXX calls (see pagination).
SetClientOptions.ResourceListChangedHandler
to be notified of changes in the lists of resources or resource templates.
Clients can be notified when the contents of a resource changes by subscribing to the resource's URI.
CallClientSession.Subscribe
to subscribe to a resource
andClientSession.Unsubscribe
to unsubscribe.
SetClientOptions.ResourceUpdatedHandler
to be notified of changes to subscribed resources. On 2026-07-28 and
later sessions the SDK delivers these notifications over asubscriptions/listen stream instead of the legacy resources/subscribe
RPC; see Subscriptions
(subscriptions/listen)
for the wire-level details.
Server-side:
UseServer.AddResource
orServer.AddResourceTemplate
to add a resource or resource template to the server along with its handler.
AResourceHandler
maps a URI to the contents of a resource, which can include text, binary data,
or both.
If AddResource or AddResourceTemplate is called before a server is connected, the server will have theresources capability.
The server will have the resources capability if any resource or resource template is added before the
server is connected to a client, or ifServerOptions.HasResources
is explicitly set. When a prompt is added, any clients already connected to the
server will be notified via a notifications/resources/list_changed
notification.
%include ../../mcp/server_example_test.go resources -
Binary resources
AResourceContents
carries its data in either Text or Blob. Binary data goes in Blob as
plain bytes — the SDK base64-encodes it on the wire — and leaves Text empty,
so a client tells the two apart by which field is populated.
%include ../../mcp/server_example_test.go binaryresource -
Resource subscriptions
A client interested in changes to one resource subscribes to its URI withClientSession.Subscribe
and stops withClientSession.Unsubscribe.
SetServerOptions.SubscribeHandler
andUnsubscribeHandler
to track who is listening; setting either one gives the server theresources.subscribe capability.
Publish a change withServer.ResourceUpdated,
which notifies every session subscribed to that URI. The notification carries
only the URI, not the new contents, so a client that wants them reads the
resource again.
%include ../../mcp/server_example_test.go subscribe -
Tools
MCP servers can provide
tools
to allow clients to interact with external systems or functionality. Tools are
effectively remote function calls, and the Go SDK provides mechanisms to bind
them to ordinary Go functions.
Client-side: To list the server's tools, use theClientSession.Tools
iterator, or the lower-levelClientSession.ListTools
(see pagination). SetClientOptions.ToolListChangedHandler
to be notified of changes in the list of tools.
To call a tool, useClientSession.CallTool
with CallToolParams holding the name and arguments of the tool to call.
res, err := session.CallTool(ctx, &mcp.CallToolParams{
Name: "my_tool",
Arguments: map[string]any{"name": "user"},
})Arguments may be any value that can be marshaled to JSON.
Server-side: the basic API for adding a tool is symmetrical with the API
for prompts or resources:Server.AddTool
adds aTool to
the server along with itsToolHandler
to handle it. The server will have the tools capability if any tool is added
before the server is connected to a client, or ifServerOptions.HasTools
is explicitly set. When a tool is added, any clients already connected to the
server will be notified via a notifications/tools/list_changed notification.
However, the Server.AddTool API leaves it to the user to implement the tool
handler correctly according to the spec, providing very little out of the box.
In order to implement a tool, the user must do all of the following:
- Provide a tool input and output schema.
- Validate the tool arguments against its input schema.
- Unmarshal the input schema into a Go value
- Execute the tool logic.
- Marshal the tool's structured output (if any) to JSON, and store it in the
result'sStructuredContentfield as well as the unstructuredContentfield. - Validate that output JSON against the tool's output schema.
- If any tool errors occurred, pack them into the unstructured content and set
IsErrortotrue.
For this reason, the SDK provides a genericAddTool
function that handles this for you. It can bind a tool to any function with the
following shape:
func(_ context.Context, request *CallToolRequest, input In) (result *CallToolResult, output Out, _ error)This is like a ToolHandler, but with an extra arbitrary In input parameter,
and Out output parameter.
Such a function can then be bound to the server using AddTool:
mcp.AddTool(server, &mcp.Tool{Name: "my_tool"}, handler)This does the following automatically:
- If
Tool.InputSchemais unset, the input schema is inferred from theIn
type, which must be a struct or map. - If
Tool.OutputSchemais unset and theOuttype is notany, the output
schema is inferred from theOuttype. Per SEP-2106,Outmay be any Go
type whose inferred schema is a valid JSON Schema (struct, map, slice,
primitive, etc.). - Optional
jsonschemastruct tags provide argument and output descriptions. - Tool arguments are validated against the input schema.
- Tool arguments are marshaled into the
Invalue. - Tool output (the
Outvalue) is marshaled into the result'sStructuredContent, as well as the unstructuredContent. - Output is validated against the tool's output schema.
- If an ordinary error is returned, it is stored int the
CallToolResultandIsErroris set totrue.
In fact, under ordinary circumstances, the user can ignore CallToolRequest
and CallToolResult.
For a more realistic example, consider a tool that retrieves the weather:
%include ../../mcp/tool_example_test.go weathertool -
In this case, we want to customize part of the inferred schema, though we can
still infer the rest. Since we want to control the inference ourselves, we set
the Tool.InputSchema explicitly:
%include ../../mcp/tool_example_test.go customschemas -
See mcp/tool_example_test.go for the full
example, or examples/server/toolschemas
for more examples of customizing tool schemas.
Tool result content
Alongside its structured output, a tool result carries a list ofContent
blocks in CallToolResult.Content, and may mix as many kinds as it needs:
| Type | Carries |
|---|---|
TextContent |
plain text |
ImageContent |
image data, with a MIME type |
AudioContent |
audio data, with a MIME type |
EmbeddedResource |
the contents of a resource, inline |
ResourceLink |
a reference to a resource the client can read separately |
ImageContent.Data and AudioContent.Data are plain []byte; the SDK
base64-encodes them on the wire, so handlers never encode by hand, and base64
carried over from another MCP SDK must be decoded before it is assigned. AnEmbeddedResource wraps aResourceContents,
which holds either Text or, for binary data, Blob. Prefer a ResourceLink
when the client may not need the contents, since an embedded resource is
transferred whether it is used or not.
When a handler bound with AddTool leaves Content unset, the SDK fills it
with the JSON encoding of the output value; set Content explicitly, as below,
to return anything else. StructuredContent is still populated from the output
value either way.
%include ../../mcp/tool_example_test.go contenttypes -
ToolUseContent and ToolResultContent also implement Content, but are only
valid in sampling messages, not in tool results.
Stateless server deployments: Some deployments create a newServer
for each incoming request, re-registering tools every time. To avoid repeated
schema generation, create aSchemaCache
and share it across server instances:
var schemaCache = mcp.NewSchemaCache() // create once at startup
func handleRequest(w http.ResponseWriter, r *http.Request) {
s := mcp.NewServer(impl, &mcp.ServerOptions{SchemaCache: schemaCache})
mcp.AddTool(s, myTool, myHandler)
// ...
}List changed notifications
Adding or removing a feature on a connected server sends the matchingnotifications/*/list_changed to every client, which dispatches it toClientOptions.ToolListChangedHandler,PromptListChangedHandler,
orResourceListChangedHandler.
A notification reports only that the list changed, so a client that needs the
new contents lists them again.
These notifications ride on capabilities, and capabilities are inferred from
what is registered before Server.Connect
(see Capabilities). A server that registers its features
afterwards, as the example below does, has to declareServerOptions.HasTools, HasPrompts, or HasResources itself, or the
client is never told.
%include ../../mcp/server_example_test.go listchanged -
Multi Round-Trip Requests
SEP-2322
defines a new pattern for server-to-client requests (sampling, elicitation,
roots). Instead of issuing a fresh JSON-RPC request mid-flight, the server
returns an InputRequiredResult from its in-flight handler — theInputRequests field of CallToolResult, GetPromptResult, orReadResourceResult carries the requests for additional information. The
client responds by retrying the original call with InputResponses set.
The SDK supports this pattern from both sides without requiring callers to
choose:
- Returning input requests from a handler. Set
InputRequestson the
result you return. To have the client call again without asking it for
input, return only aRequestState; the client echoes it on the retry.
LeaveContent/StructuredContentempty — returning
content together withInputRequestsorRequestStateis a server bug and
the SDK returns-32603 InternalError. - Calling the legacy APIs.
ServerSession.Elicit,ServerSession.CreateMessage(WithTools), andServerSession.ListRoots
remain available and work for both new and legacy clients.
Talking to legacy clients
The server installs serverMultiRoundTripMiddleware automatically. For
clients on a protocol version earlier than 2026-07-28, the middleware
intercepts any InputRequiredResult your handler returns, fulfils each
input request itself by calling the legacy server-initiated APIs
(Elicit, CreateMessage, ListRoots), and re-invokes your handler
with the responses already populated. A result with a RequestState and no
input requests re-invokes it with that state and no responses. If the handler asks for more input,
the middleware repeats this until the handler completes, up to the same
limit of 10 rounds that the client-side middleware applies. This means a
handler written in the MRTR style works against both old and new clients
without code changes.
Example
The following example shows a "greet" tool whose handler asks the user
for their name via elicitation before producing the final greeting. The
handler runs twice — once to issue the elicitation, once to consume the
response — but the client call site sees a single CallTool returning the
final result, because the SDK's MRTR middleware handles the round trip on
either side of the wire (depending on the negotiated protocol version).
%include ../../mcp/server_example_test.go mrtr -
Cacheable list results
SEP-2549
adds ttlMs and cacheScope fields to the results of tools/list,prompts/list, resources/list, resources/templates/list, andresources/read. Both fields complement existing list
results: clients use ttlMs as a freshness hint to reduce polling,
and cacheScope ("public" or "private") controls whether shared
intermediaries may cache the response.
Server-side defaults. The SDK fills in CacheScope = "public" whenever a
result leaves it empty, since the field is required on the wire. Everything
else is left as produced: a ResourceHandler that sets TTLMs or CacheScope
on its ReadResourceResult keeps those values. The list results andserver/discover are built by the SDK itself, so they start out withTTLMs = 0, which a client cache treats as "immediately stale".
Setting a policy. ServerOptions.SetCacheable decides the fields for every
result that carries them. It runs after the corresponding handler, receives
what that handler produced, and may keep, adjust, or replace it. The request is
passed in so that policy can vary by method, session, or authorization context.
server := mcp.NewServer(impl, &mcp.ServerOptions{
SetCacheable: func(_ context.Context, req mcp.Request, c *mcp.Cacheable) {
// A 30s freshness hint, except where the handler chose its own.
if c.TTLMs == 0 {
c.TTLMs = 30_000
}
},
})SetCacheable may run with the server's lock held, so it must not call back
into the Server: adding or removing a feature, or ranging overServer.Sessions, deadlocks.
Utilities
Completion
To support the
completion
capability, the server needs a completion handler.
Client-side: completion is called using theClientSession.Complete
method.
Server-side: completion is enabled by settingServerOptions.CompletionHandler.
If this field is set to a non-nil value, the server will advertise thecompletions server capability, and use this handler to respond to completion
requests.
%include ../../examples/server/completion/main.go completionhandler -
Logging
Note: The logging feature is deprecated as of protocol version
2026-07-28
(SEP-2577).
It remains fully functional during the deprecation window (at least twelve
months). The SDK continues to support logging for compatibility. Servers
should migrate to stderr logging (for STDIO transports) or OpenTelemetry.
MCP servers can send logging messages to MCP clients.
(This form of logging is distinct from server-side logging, where the
server produces logs that remain server-side, for use by server maintainers.)
Server-side:
The minimum log level is part of the server state.
For stateful sessions, there is no default log level: no log messages will be sent
until the client calls SetLoggingLevel (see below).
For legacy stateless sessions the level defaults to "info".
For sessionless 2026-07-28 requests there is no server-side default: each request
carries its level in _meta (io.modelcontextprotocol/logLevel), and nothing is
logged until the client sends one.
(Note: logging is deprecated as of 2026-07-28 / SEP-2577.)
ServerSession.Log is the low-level way for servers to log to clients.
It sends a logging notification to the client if the level of the message
is at least the minimum log level.
For a simpler API, use NewLoggingHandler to obtain a slog.Handler.
By setting LoggingHandlerOptions.MinInterval, the handler can be rate-limited
to avoid spamming clients with too many messages.
Servers always report the logging capability.
Client-side:
Set ClientOptions.LoggingMessageHandler to receive log messages.
Call ClientSession.SetLoggingLevel to change the log level for a session.
%include ../../mcp/server_example_test.go logging -
Capabilities
Server capabilities are advertised to clients during the initialization
handshake. By default, the SDK advertises only the logging capability.
Additional capabilities are automatically added when features are registered
(e.g., adding a tool adds the tools capability).
Capability inference
When features such as tools, prompts, or resources are added to the server
(e.g., via Server.AddTool), their capability is automatically inferred, with
default value {listChanged:true}. Similarly, if theServerOptions.SubscribeHandler or ServerOptions.CompletionHandler are set,
the corresponding capability is added.
Explicit capabilities
To explicitly declare capabilities, or to override the default inferred
capability, setServerOptions.Capabilities.
This sets the default server capabilities, before any capabilities are added
based on configured handlers. If a capability is already present as a field inCapabilities, adding a feature or handler will not change its configuration.
This allows you to:
- Disable default capabilities: Pass an empty
&ServerCapabilities{}to
disable all defaults, including logging. - Disable listChanged notifications: Set
ListChanged: falseon a
capability to prevent the server from sending list-changed notifications
when features are added or removed. - Pre-declare capabilities: Declare capabilities before features are
registered, useful for servers that load features dynamically.
// Disable listChanged notifications for tools
server := mcp.NewServer(impl, &mcp.ServerOptions{
Capabilities: &mcp.ServerCapabilities{
Logging: &mcp.LoggingCapabilities{},
Tools: &mcp.ToolCapabilities{ListChanged: false},
},
})Deprecated: The HasPrompts, HasResources, and HasTools fields onServerOptions are deprecated. Use Capabilities instead.
Extensions
SEP-2133
adds an extensions map to ServerCapabilities so that optional
capabilities outside the core protocol can be declared on the wire. Keys
are namespaced as "{vendor-prefix}/{extension-name}"; values are
per-extension settings objects.
Pagination
Server-side feature lists may be
paginated,
using cursors. The SDK supports this by default.
Client-side: The ClientSession provides methods returning
iterators for each feature type.
These iterators are an iter.Seq2[Feature, error], where the error value
indicates whether page retrieval failed.
ClientSession.Prompts
iterates prompts.ClientSession.Resource
iterates resources.ClientSession.ResourceTemplates
iterates resource templates.ClientSession.Tools
iterates tools.
The ClientSession also exposes ListXXX methods for fine-grained control
over pagination.
Server-side: pagination is on by default, so in general nothing is required
server-side. However, you may useServerOptions.PageSize
to customize the page size.
Internal Docs Troubleshooting.Src (`internal/docs/troubleshooting.src.md`)
Troubleshooting
The Model Context Protocol is a complicated spec that leaves some room for
interpretation. Client and server SDKs can behave differently, or can be more
or less strict about their inputs. And of course, bugs happen.
When you encounter a problem using the Go SDK, these instructions can help
collect information that will be useful in debugging. Please try to provide
this information in a bug report, so that maintainers can more quickly
understand what's going wrong.
And most of all, please do file bugs.
Using the MCP inspector
To debug an MCP server, you can use the MCP
inspector. This is
useful for testing your server and verifying that it works with the typescript
SDK, as well as inspecting MCP traffic.
Collecting MCP logs
For stdio transport connections, you can also
inspect MCP traffic using a LoggingTransport:
%include ../../mcp/transport_example_test.go loggingtransport -
That example uses a bytes.Buffer, but you can also log to a file, or toos.Stderr.
Inspecting HTTP traffic
There are a couple different ways to investigate traffic to an HTTP transport
(streamable or legacy SSE).
The first is to use an HTTP middleware:
%include ../../mcp/streamable_example_test.go httpmiddleware -
The second is to use a general purpose tool to inspect http traffic, such as
wireshark or
tcpdump.
--- METRICS ---
- Files Extracted: 21
- Estimated Token Budget: ~55394 tokens
- Canonical Reference: https://codewiki.google/github.com/modelcontextprotocol/go-sdk