Support for MCP server features¶
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 the
ClientSession.Prompts
iterator, or the lower-level
ClientSession.ListPrompts
(see pagination below). Set
ClientOptions.PromptListChangedHandler
to be notified of changes in the list of prompts.
Call
ClientSession.GetPrompt
to retrieve a prompt by name, providing arguments for expansion.
Server-side: Use
Server.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 if
ServerOptions.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.
func Example_prompts() {
ctx := context.Background()
promptHandler := func(ctx context.Context, req *mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
return &mcp.GetPromptResult{
Description: "Hi prompt",
Messages: []*mcp.PromptMessage{
{
Role: "user",
Content: &mcp.TextContent{Text: "Say hi to " + req.Params.Arguments["name"]},
},
},
}, nil
}
// Create a server with a single prompt.
s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil)
prompt := &mcp.Prompt{
Name: "greet",
Arguments: []*mcp.PromptArgument{
{
Name: "name",
Description: "the name of the person to greet",
Required: true,
},
},
}
s.AddPrompt(prompt, promptHandler)
// Create a client.
c := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, nil)
// Connect the server and client.
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()
// List the prompts.
for p, err := range cs.Prompts(ctx, nil) {
if err != nil {
log.Fatal(err)
}
fmt.Println(p.Name)
}
// Get the prompt.
res, err := cs.GetPrompt(ctx, &mcp.GetPromptParams{
Name: "greet",
Arguments: map[string]string{"name": "Pat"},
})
if err != nil {
log.Fatal(err)
}
for _, msg := range res.Messages {
fmt.Println(msg.Role, msg.Content.(*mcp.TextContent).Text)
}
// Output:
// greet
// user Say hi to Pat
}
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 the
ClientSession.Resources
and
ClientSession.ResourceTemplates
iterators, or the lower-level ListXXX calls (see pagination).
Set
ClientOptions.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.
Call
ClientSession.Subscribe
to subscribe to a resource
and
ClientSession.Unsubscribe
to unsubscribe.
Set
ClientOptions.ResourceUpdatedHandler
to be notified of changes to subscribed resources.
Server-side:
Use
Server.AddResource
or
Server.AddResourceTemplate
to add a resource or resource template to the server along with its handler.
A
ResourceHandler
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 the
resources 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 if
ServerOptions.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.
func Example_resources() {
ctx := context.Background()
resources := map[string]string{
"file:///a": "a",
"file:///dir/x": "x",
"file:///dir/y": "y",
}
handler := func(_ context.Context, req *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
uri := req.Params.URI
c, ok := resources[uri]
if !ok {
return nil, mcp.ResourceNotFoundError(uri)
}
return &mcp.ReadResourceResult{
Contents: []*mcp.ResourceContents{{URI: uri, Text: c}},
}, nil
}
// Create a server with a single resource.
s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil)
s.AddResource(&mcp.Resource{URI: "file:///a"}, handler)
s.AddResourceTemplate(&mcp.ResourceTemplate{URITemplate: "file:///dir/{f}"}, handler)
// Create a client.
c := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, nil)
// Connect the server and client.
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()
// List resources and resource templates.
for r, err := range cs.Resources(ctx, nil) {
if err != nil {
log.Fatal(err)
}
fmt.Println(r.URI)
}
for r, err := range cs.ResourceTemplates(ctx, nil) {
if err != nil {
log.Fatal(err)
}
fmt.Println(r.URITemplate)
}
// Read resources.
for _, path := range []string{"a", "dir/x", "b"} {
res, err := cs.ReadResource(ctx, &mcp.ReadResourceParams{URI: "file:///" + path})
if err != nil {
fmt.Println(err)
} else {
fmt.Println(res.Contents[0].Text)
}
}
// Output:
// file:///a
// file:///dir/{f}
// a
// x
// calling "resources/read": Resource not found
}
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 the
ClientSession.Tools
iterator, or the lower-level
ClientSession.ListTools
(see pagination). Set
ClientOptions.ToolListChangedHandler
to be notified of changes in the list of tools.
To call a tool, use
ClientSession.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 a
Tool to
the server along with its
ToolHandler
to handle it. The server will have the tools capability if any tool is added
before the server is connected to a client, or if
ServerOptions.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's
StructuredOutputfield 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 generic
AddTool
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:
This does the following automatically:
- If
Tool.InputSchemaorTool.OutputSchemaare unset, the input and output schemas are inferred from theIntype, which must be a struct or map. Optionaljsonschemastruct tags provide argument 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'sStructuredOutput, 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.
Stateless server deployments: Some deployments create a new
Server
for each incoming request, re-registering tools every time. To avoid repeated
schema generation, create a
SchemaCache
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)
// ...
}
Utilities¶
Completion¶
To support the completion capability, the server needs a completion handler.
Client-side: completion is called using the
ClientSession.Complete
method.
Server-side: completion is enabled by setting
ServerOptions.CompletionHandler.
If this field is set to a non-nil value, the server will advertise the
completions 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¶
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 SetLevel (see below).
For stateful sessions, the level defaults to "info".
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.SetLevel to change the log level for a session.
func Example_logging() {
ctx := context.Background()
// Create a server.
s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil)
// Create a client that displays log messages.
done := make(chan struct{}) // solely for the example
var nmsgs atomic.Int32
c := mcp.NewClient(
&mcp.Implementation{Name: "client", Version: "v0.0.1"},
&mcp.ClientOptions{
LoggingMessageHandler: func(_ context.Context, r *mcp.LoggingMessageRequest) {
m := r.Params.Data.(map[string]any)
fmt.Println(m["msg"], m["value"])
if nmsgs.Add(1) == 2 { // number depends on logger calls below
close(done)
}
},
})
// Connect the server and client.
t1, t2 := mcp.NewInMemoryTransports()
ss, err := s.Connect(ctx, t1, nil)
if err != nil {
log.Fatal(err)
}
defer ss.Close()
cs, err := c.Connect(ctx, t2, nil)
if err != nil {
log.Fatal(err)
}
defer cs.Close()
// Set the minimum log level to "info".
if err := cs.SetLoggingLevel(ctx, &mcp.SetLoggingLevelParams{Level: "info"}); err != nil {
log.Fatal(err)
}
// Get a slog.Logger for the server session.
logger := slog.New(mcp.NewLoggingHandler(ss, nil))
// Log some things.
logger.Info("info shows up", "value", 1)
logger.Debug("debug doesn't show up", "value", 2)
logger.Warn("warn shows up", "value", 3)
// Wait for them to arrive on the client.
// In a real application, the log messages would appear asynchronously
// while other work was happening.
<-done
// Output:
// info shows up 1
// warn shows up 3
}
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 the
ServerOptions.SubscribeHandler or ServerOptions.CompletionHandler are set,
the corresponding capability is added.
Explicit capabilities¶
To explicitly declare capabilities, or to override the default inferred
capability, set
ServerOptions.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 in
Capabilities, 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 on
ServerOptions are deprecated. Use Capabilities instead.
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.Promptsiterates prompts.ClientSession.Resourceiterates resources.ClientSession.ResourceTemplatesiterates resource templates.ClientSession.Toolsiterates 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 use
ServerOptions.PageSize
to customize the page size.