mcp: add docs for tier 1 assessment (#1195)

Closes open points in
[comment](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/3220#issuecomment-5373089321)
This commit is contained in:
Guglielmo Colombo 2026-08-24 12:00:59 +02:00 • committed by GitHub
commit b8c53fedd6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 1461 additions and 5 deletions

View file

@ -6,6 +6,7 @@ package mcp_test
import (
"context"
"encoding/json"
"fmt"
"log"
@ -65,6 +66,61 @@ func Example_roots() {
// !-roots
// !+rootslistchanged
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
}
// !-rootslistchanged
// !+sampling
func Example_sampling() {
@ -167,3 +223,117 @@ func Example_elicitation() {
}
// !-elicitation
// !+elicitationschema
func Example_elicitationSchema() {
ctx := context.Background()
ct, st := mcp.NewInMemoryTransports()
s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil)
mcp.AddTool(s, &mcp.Tool{Name: "export_report"}, func(_ context.Context, req *mcp.CallToolRequest, _ struct{}) (*mcp.CallToolResult, any, error) {
if len(req.Params.InputResponses) == 0 {
return &mcp.CallToolResult{
InputRequests: mcp.InputRequestMap{"format": &mcp.ElicitParams{
Message: "Export quarterly-sales as which format?",
RequestedSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"format": {
Type: "string",
Title: "Format",
Enum: []any{"pdf", "csv"},
Default: json.RawMessage(`"pdf"`),
Extra: map[string]any{"enumNames": []any{"PDF document", "CSV spreadsheet"}},
},
},
},
}},
}, nil, nil
}
res := req.Params.InputResponses["format"].(*mcp.ElicitResult)
return &mcp.CallToolResult{
Content: []mcp.Content{&mcp.TextContent{Text: "Exported as " + res.Content["format"].(string)}},
}, nil, nil
})
if _, err := s.Connect(ctx, st, nil); err != nil {
log.Fatal(err)
}
// The user accepts without filling anything in.
c := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, &mcp.ClientOptions{
ElicitationHandler: func(context.Context, *mcp.ElicitRequest) (*mcp.ElicitResult, error) {
return &mcp.ElicitResult{Action: "accept", Content: map[string]any{}}, nil
},
})
cs, err := c.Connect(ctx, ct, nil)
if err != nil {
log.Fatal(err)
}
defer cs.Close()
res, err := cs.CallTool(ctx, &mcp.CallToolParams{Name: "export_report"})
if err != nil {
log.Fatal(err)
}
fmt.Println(res.Content[0].(*mcp.TextContent).Text)
// Output: Exported as pdf
}
// !-elicitationschema
// !+elicitationcomplete
func Example_elicitationComplete() {
ctx := context.Background()
ct, st := mcp.NewInMemoryTransports()
s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil)
ss, err := s.Connect(ctx, st, nil)
if err != nil {
log.Fatal(err)
}
defer ss.Close()
done := make(chan struct{})
c := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, &mcp.ClientOptions{
Capabilities: &mcp.ClientCapabilities{
Elicitation: &mcp.ElicitationCapabilities{URL: &mcp.URLElicitationCapabilities{}},
},
ElicitationHandler: func(_ context.Context, req *mcp.ElicitRequest) (*mcp.ElicitResult, error) {
fmt.Println("opening", req.Params.URL)
return &mcp.ElicitResult{Action: "accept"}, nil
},
ElicitationCompleteHandler: func(_ context.Context, req *mcp.ElicitationCompleteNotificationRequest) {
fmt.Println("flow finished:", req.Params.ElicitationID)
close(done)
},
})
cs, err := c.Connect(ctx, ct, &mcp.ClientSessionOptions{ProtocolVersion: "2025-11-25"})
if err != nil {
log.Fatal(err)
}
defer cs.Close()
const elicitationID = "connect-calendar-1"
if _, err := ss.Elicit(ctx, &mcp.ElicitParams{
Message: "Grant calendar access",
URL: "https://calendar.example.com/consent?state=" + elicitationID,
ElicitationID: elicitationID,
}); err != nil {
log.Fatal(err)
}
// The hosted page redirects back to the server, whose callback endpoint
// signals that the user is done.
if err := ss.NotifyElicitationComplete(ctx, &mcp.ElicitationCompleteParams{ElicitationID: elicitationID}); err != nil {
log.Fatal(err)
}
<-done
// Output:
// opening https://calendar.example.com/consent?state=connect-calendar-1
// flow finished: connect-calendar-1
}
// !-elicitationcomplete

View file

@ -88,6 +88,77 @@ func Example_prompts() {
// !-prompts
// !+promptcontent
func Example_promptContent() {
ctx := context.Background()
const styleGuide = "- Prefer clarity over cleverness.\n"
screenshotPNG := []byte("\x89PNG\r\n\x1a\n")
s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil)
s.AddResource(&mcp.Resource{URI: "doc://style-guide", MIMEType: "text/markdown"},
func(context.Context, *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
return &mcp.ReadResourceResult{
Contents: []*mcp.ResourceContents{{
URI: "doc://style-guide",
MIMEType: "text/markdown",
Text: styleGuide,
}},
}, nil
})
s.AddPrompt(&mcp.Prompt{Name: "review_screenshot"},
func(context.Context, *mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
return &mcp.GetPromptResult{
Messages: []*mcp.PromptMessage{
{Role: "user", Content: &mcp.EmbeddedResource{
Resource: &mcp.ResourceContents{
URI: "doc://style-guide",
MIMEType: "text/markdown",
Text: styleGuide,
},
}},
{Role: "user", Content: &mcp.ImageContent{Data: screenshotPNG, MIMEType: "image/png"}},
{Role: "user", Content: &mcp.TextContent{Text: "Review the screenshot against the style guide."}},
},
}, 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.GetPrompt(ctx, &mcp.GetPromptParams{Name: "review_screenshot"})
if err != nil {
log.Fatal(err)
}
for _, msg := range res.Messages {
switch content := msg.Content.(type) {
case *mcp.TextContent:
fmt.Println(msg.Role, "text:", content.Text)
case *mcp.ImageContent:
fmt.Printf("%s image: %s, %d bytes\n", msg.Role, content.MIMEType, len(content.Data))
case *mcp.EmbeddedResource:
fmt.Printf("%s embedded resource: %s (%s)\n", msg.Role, content.Resource.URI, content.Resource.MIMEType)
}
}
// Output:
// user embedded resource: doc://style-guide (text/markdown)
// user image: image/png, 8 bytes
// user text: Review the screenshot against the style guide.
}
// !-promptcontent
// !+logging
func Example_logging() {
@ -222,6 +293,186 @@ func Example_resources() {
// !-resources
// !+listchanged
func Example_listChanged() {
ctx := context.Background()
changed := make(chan string, 3)
c := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, &mcp.ClientOptions{
ToolListChangedHandler: func(context.Context, *mcp.ToolListChangedRequest) {
changed <- "tools"
},
PromptListChangedHandler: func(context.Context, *mcp.PromptListChangedRequest) {
changed <- "prompts"
},
ResourceListChangedHandler: func(context.Context, *mcp.ResourceListChangedRequest) {
changed <- "resources"
},
})
// Nothing is registered before Connect, so the capabilities that carry these
// notifications have to be declared explicitly.
s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, &mcp.ServerOptions{
HasTools: true,
HasPrompts: true,
HasResources: true,
})
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()
mcp.AddTool(s, &mcp.Tool{Name: "greet"}, func(context.Context, *mcp.CallToolRequest, struct{}) (*mcp.CallToolResult, any, error) {
return &mcp.CallToolResult{}, nil, nil
})
fmt.Println("changed:", <-changed)
s.AddPrompt(&mcp.Prompt{Name: "review"}, func(context.Context, *mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
return &mcp.GetPromptResult{}, nil
})
fmt.Println("changed:", <-changed)
s.AddResource(&mcp.Resource{URI: "file:///a"}, func(context.Context, *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
return &mcp.ReadResourceResult{}, nil
})
fmt.Println("changed:", <-changed)
// Removing a feature notifies the client just as adding one does.
s.RemoveTools("greet")
fmt.Println("changed:", <-changed)
// Output:
// changed: tools
// changed: prompts
// changed: resources
// changed: tools
}
// !-listchanged
// !+subscribe
func Example_resourceSubscription() {
ctx := context.Background()
config := "theme=light\n"
updated := make(chan string, 1)
c := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, &mcp.ClientOptions{
// The notification carries only the URI, so read the resource again to
// see what it now holds.
ResourceUpdatedHandler: func(context.Context, *mcp.ResourceUpdatedNotificationRequest) {
updated <- "config://app"
},
})
subscribed := make(chan string, 2)
s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, &mcp.ServerOptions{
SubscribeHandler: func(_ context.Context, req *mcp.SubscribeRequest) error {
subscribed <- "subscribed to " + req.Params.URI
return nil
},
UnsubscribeHandler: func(_ context.Context, req *mcp.UnsubscribeRequest) error {
subscribed <- "unsubscribed from " + req.Params.URI
return nil
},
})
s.AddResource(&mcp.Resource{URI: "config://app"},
func(context.Context, *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
return &mcp.ReadResourceResult{
Contents: []*mcp.ResourceContents{{URI: "config://app", Text: config}},
}, 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()
if err := cs.Subscribe(ctx, &mcp.SubscribeParams{URI: "config://app"}); err != nil {
log.Fatal(err)
}
fmt.Println(<-subscribed)
config = "theme=dark\n"
if err := s.ResourceUpdated(ctx, &mcp.ResourceUpdatedNotificationParams{URI: "config://app"}); err != nil {
log.Fatal(err)
}
uri := <-updated
res, err := cs.ReadResource(ctx, &mcp.ReadResourceParams{URI: uri})
if err != nil {
log.Fatal(err)
}
fmt.Print("re-read: ", res.Contents[0].Text)
if err := cs.Unsubscribe(ctx, &mcp.UnsubscribeParams{URI: "config://app"}); err != nil {
log.Fatal(err)
}
fmt.Println(<-subscribed)
// Output:
// subscribed to config://app
// re-read: theme=dark
// unsubscribed from config://app
}
// !-subscribe
// !+binaryresource
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 ""
}
// !-binaryresource
// !+mrtr
// Example_mrtr demonstrates the [Multi Round-Trip Requests] pattern

View file

@ -27,6 +27,8 @@ func Add(ctx context.Context, req *mcp.CallToolRequest, args AddParams) (*mcp.Ca
}, nil, nil
}
// !+ssehandler
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)
@ -55,3 +57,5 @@ func ExampleSSEHandler() {
// Output: 3
}
// !-ssehandler

View file

@ -43,7 +43,7 @@ type ToolHandler func(context.Context, *CallToolRequest) (*CallToolResult, error
// - If the Out type is not the empty interface [any], it provides the
// default output schema for the tool (which again may be overridden in
// [AddTool]).
// - The Out value is used to populate result.StructuredOutput.
// - The Out value is used to populate result.StructuredContent.
// - If [CallToolResult.Content] is unset, it is populated with the JSON
// content of the output.
// - An error result is treated as a tool error, rather than a protocol

View file

@ -253,6 +253,74 @@ func ExampleAddTool_complexSchema() {
// weather types: [sun partly_cloudy clouds rain snow]
}
// !+contenttypes
func ExampleAddTool_contentTypes() {
chartPNG := []byte("\x89PNG\r\n\x1a\n")
summaryWAV := []byte("RIFF....WAVE")
server := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil)
mcp.AddTool(server, &mcp.Tool{Name: "report"}, func(context.Context, *mcp.CallToolRequest, struct{}) (*mcp.CallToolResult, any, error) {
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{Text: "Q3 revenue by region."},
&mcp.ImageContent{Data: chartPNG, MIMEType: "image/png"},
&mcp.AudioContent{Data: summaryWAV, MIMEType: "audio/wav"},
// An embedded resource inlines the contents, so the client
// needs no follow-up resources/read. Binary contents go in
// ResourceContents.Blob instead of Text.
&mcp.EmbeddedResource{
Resource: &mcp.ResourceContents{
URI: "file:///reports/q3.csv",
MIMEType: "text/csv",
Text: "region,revenue\nEMEA,42\n",
},
},
// A resource link only points at a resource. Prefer it when the
// client may not need the contents, since an embedded resource
// is transferred either way.
&mcp.ResourceLink{URI: "file:///reports/q3.pdf", MIMEType: "application/pdf"},
},
}, nil, nil
})
ctx := context.Background()
session, err := connect(ctx, server)
if err != nil {
log.Fatal(err)
}
defer session.Close()
res, err := session.CallTool(ctx, &mcp.CallToolParams{Name: "report"})
if err != nil {
log.Fatal(err)
}
// Clients type-switch over the content they receive.
for _, content := range res.Content {
switch c := content.(type) {
case *mcp.TextContent:
fmt.Println("text:", c.Text)
case *mcp.ImageContent:
fmt.Printf("image: %s, %d bytes\n", c.MIMEType, len(c.Data))
case *mcp.AudioContent:
fmt.Printf("audio: %s, %d bytes\n", c.MIMEType, len(c.Data))
case *mcp.EmbeddedResource:
fmt.Printf("embedded resource: %s (%s)\n", c.Resource.URI, c.Resource.MIMEType)
case *mcp.ResourceLink:
fmt.Printf("resource link: %s (%s)\n", c.URI, c.MIMEType)
}
}
// Output:
// text: Q3 revenue by region.
// image: image/png, 8 bytes
// audio: audio/wav, 12 bytes
// embedded resource: file:///reports/q3.csv (text/csv)
// resource link: file:///reports/q3.pdf (application/pdf)
}
// !-contenttypes
func connect(ctx context.Context, server *mcp.Server) (*mcp.ClientSession, error) {
t1, t2 := mcp.NewInMemoryTransports()
if _, err := server.Connect(ctx, t1, nil); err != nil {