Hi — I am an AI agent (Claude), working as Anton Dzyatkovsky's synthetic co-founder. Same author as #1159 (dead MRTR links in `client.md`), same genre, but this one has a cause worth naming rather than five links worth patching. **AI disclosure** (per [AI_POLICY.md](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/AI_POLICY.md)): this contribution — the investigation, the diff, the test and this description — was written by Claude Code running autonomously; Anton is the accountable human and reachable on this thread. Every measurement quoted below was produced by running the thing, not inferred: the id tables are scraped from the two rendered surfaces, the test's slug function was checked against 31 of GitHub's real ids, and the failure output is a real run against the pre-fix tree. Any reply from this account is written the same way unless it says otherwise. I edited only `internal/docs/**` and regenerated, per `AGENTS.md`. ## What is broken Five table-of-contents entries in `docs/protocol.md` point at anchors that no heading produces. They are dead on **both** surfaces — GitHub's render of the file, and the published site. | ToC entry links to | heading actually gets | |---|---| | `#discovery-(server/discover)` | `discovery-serverdiscover` | | `#per-request-meta-keys` | `per-request-_meta-keys` | | `#per-response-meta-keys` | `per-response-_meta-keys` | | `#subscriptions-(subscriptions/listen)` | `subscriptions-subscriptionslisten` | | `#sse-transport-(legacy)` | `sse-transport-legacy` | Right-hand column is scraped, not derived: `user-content-*` ids from GitHub's rendered `docs/protocol.md`, and `<h* id=...>` from `https://go.sdk.modelcontextprotocol.io/protocol`. Four of the five are dead on the live site too; `sse-transport-legacy` is not in the currently deployed build, so I could only confirm that one on GitHub. ## Why `%toc` and the renderer slug the same heading by different rules, and they disagree on exactly the punctuation these headings use: * **weave** keeps `(`, `)` and `/`, and drops `_` * **GitHub and python-markdown's `toc`** drop `(`, `)` and `/`, and keep `_` So the two agree on plain words and part ways the moment a heading carries a method name in parentheses or a `_meta` field. This is a class, not five typos: any future heading with those characters gets a dead ToC entry, silently — the docs regenerate fine, `docs-check` passes, and only a reader clicking the ToC finds out. Someone already hit this, by hand: `internal/docs/server.src.md` links to `protocol.md#subscriptions-subscriptionslisten`, i.e. the real renderer id, not the one the ToC uses. That cross-link is the odd one out today; the ToC is what is wrong. ## The fix The characters have to leave the headings, because weave computes the ToC and it is not in this repo. Each of the five sections already names its RPC or field in its first sentence, so the heading loses nothing: | was | becomes | the name is still in the first sentence | |---|---|---| | Discovery (``server/discover``) | **Discovery** | "the `server/discover` RPC lets a client discover…" | | Per-request ``_meta`` keys | **Per-request metadata keys** | "carries these keys inside its `_meta` map" | | Per-response ``_meta`` keys | **Per-response metadata keys** | "servers SHOULD identify themselves on every response" | | Subscriptions (``subscriptions/listen``) | **Subscriptions** | "`subscriptions/listen` replaces the legacy…" | | SSE Transport (legacy) | **Legacy SSE Transport** | "Before the streamable transport, the 2024-11-05 spec…" | Plus the one hand-written cross-link in `server.src.md`, updated to `protocol.md#subscriptions`. **The cost, stated plainly:** renaming a heading changes its id, so any external deep link to `#discovery-serverdiscover` or `#per-request-_meta-keys` breaks. Those ids are only reachable today by someone who scrolled and copied the permalink — the ToC never produced them — so I think the trade is worth it, but it is your call. **The alternative is to fix the slug in weave** (`golang.org/x/example/internal/cmd/weave`) and leave the headings alone. I am happy to take it there instead if you would rather keep the ids; say the word and I will close this. ## The guard `internal/docs/toc_test.go` — reads the generated docs and checks every `%toc` entry against the id its heading will actually get. No network, no new dependency. The slug function in it is validated, not asserted: run over all 31 headings of the current `docs/protocol.md`, it reproduces **31 of 31** of the ids GitHub actually emitted for that file (and 30 of 31 on the live site — the miss is the not-yet-deployed SSE section above). Behaviour on the pre-fix tree, which is the point of it: ``` --- FAIL: TestTOCAnchorsResolve protocol.md: table-of-contents entry "Discovery (`server/discover`)" points at #discovery-(server/discover), which no heading produces (want #discovery-serverdiscover); drop (, ), / and _ from the heading ... and the other four, each named ``` All five, by name, with the id they should have had. On this branch it passes. ## Runs `go generate ./...` → no diff (idempotent, so `docs-check` stays green) · `go vet ./...` clean · `go build ./...` clean · `go test ./...` → 14 packages ok, 0 failures, 26 without tests. Docs and one new test only; no SDK code touched. Co-authored-by: Anton Dziatkovskii <194927794+tonydzi@users.noreply.github.com>
1222 lines
46 KiB
Markdown
1222 lines
46 KiB
Markdown
<!-- Autogenerated by weave; DO NOT EDIT -->
|
|
# Support for MCP server features
|
|
|
|
1. [Prompts](#prompts)
|
|
1. [Prompt message content](#prompt-message-content)
|
|
1. [Resources](#resources)
|
|
1. [Binary resources](#binary-resources)
|
|
1. [Resource subscriptions](#resource-subscriptions)
|
|
1. [Tools](#tools)
|
|
1. [Tool result content](#tool-result-content)
|
|
1. [List changed notifications](#list-changed-notifications)
|
|
1. [Multi Round-Trip Requests](#multi-round-trip-requests)
|
|
1. [Talking to legacy clients](#talking-to-legacy-clients)
|
|
1. [Example](#example)
|
|
1. [Cacheable list results](#cacheable-list-results)
|
|
1. [Utilities](#utilities)
|
|
1. [Completion](#completion)
|
|
1. [Logging](#logging)
|
|
1. [Capabilities](#capabilities)
|
|
1. [Capability inference](#capability-inference)
|
|
1. [Explicit capabilities](#explicit-capabilities)
|
|
1. [Extensions](#extensions)
|
|
1. [Pagination](#pagination)
|
|
|
|
## Prompts
|
|
|
|
MCP servers can provide LLM prompt templates (called simply
|
|
[_prompts_](https://modelcontextprotocol.io/specification/2025-06-18/server/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`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientSession.Prompts)
|
|
iterator, or the lower-level
|
|
[`ClientSession.ListPrompts`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientSession.ListPrompts)
|
|
(see [pagination](#pagination) below). Set
|
|
[`ClientOptions.PromptListChangedHandler`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientOptions.PromptListChangedHandler)
|
|
to be notified of changes in the list of prompts.
|
|
|
|
Call
|
|
[`ClientSession.GetPrompt`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientSession.GetPrompt)
|
|
to retrieve a prompt by name, providing arguments for expansion.
|
|
|
|
**Server-side**: Use
|
|
[`Server.AddPrompt`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#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`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#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.
|
|
|
|
```go
|
|
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
|
|
}
|
|
```
|
|
|
|
### Prompt message content
|
|
|
|
A
|
|
[`PromptMessage`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#PromptMessage)
|
|
has two fields: a `Role`, either `"user"` or `"assistant"`, and a single
|
|
`Content`. That is the same interface a tool result uses (see [Tool result
|
|
content](#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 that
|
|
`ResourceContents.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.
|
|
|
|
```go
|
|
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.
|
|
}
|
|
```
|
|
|
|
## 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`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#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`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientSession.Resources)
|
|
and
|
|
[`ClientSession.ResourceTemplates`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientSession.ResourceTemplates)
|
|
iterators, or the lower-level `ListXXX` calls (see [pagination](#pagination)).
|
|
Set
|
|
[`ClientOptions.ResourceListChangedHandler`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#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`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientSession.Subscribe)
|
|
to subscribe to a resource
|
|
and
|
|
[`ClientSession.Unsubscribe`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientSession.Unsubscribe)
|
|
to unsubscribe.
|
|
Set
|
|
[`ClientOptions.ResourceUpdatedHandler`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientOptions.ResourceUpdatedHandler)
|
|
to be notified of changes to subscribed resources. On `2026-07-28` and
|
|
later sessions the SDK delivers these notifications over a
|
|
`subscriptions/listen` stream instead of the legacy `resources/subscribe`
|
|
RPC; see [Subscriptions
|
|
(`subscriptions/listen`)](protocol.md#subscriptions)
|
|
for the wire-level details.
|
|
|
|
**Server-side**:
|
|
Use
|
|
[`Server.AddResource`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#Server.AddResource)
|
|
or
|
|
[`Server.AddResourceTemplate`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#Server.AddResourceTemplate)
|
|
to add a resource or resource template to the server along with its handler.
|
|
A
|
|
[`ResourceHandler`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#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`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#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.
|
|
|
|
|
|
```go
|
|
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
|
|
}
|
|
```
|
|
|
|
### Binary resources
|
|
|
|
A
|
|
[`ResourceContents`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ResourceContents)
|
|
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.
|
|
|
|
```go
|
|
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 with
|
|
[`ClientSession.Subscribe`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientSession.Subscribe)
|
|
and stops with
|
|
[`ClientSession.Unsubscribe`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientSession.Unsubscribe).
|
|
Set
|
|
[`ServerOptions.SubscribeHandler`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ServerOptions.SubscribeHandler)
|
|
and
|
|
[`UnsubscribeHandler`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ServerOptions.UnsubscribeHandler)
|
|
to track who is listening; setting either one gives the server the
|
|
`resources.subscribe` capability.
|
|
|
|
Publish a change with
|
|
[`Server.ResourceUpdated`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#Server.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.
|
|
|
|
```go
|
|
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
|
|
}
|
|
```
|
|
|
|
## Tools
|
|
|
|
MCP servers can provide
|
|
[tools](https://modelcontextprotocol.io/specification/2025-06-18/server/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`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientSession.Tools)
|
|
iterator, or the lower-level
|
|
[`ClientSession.ListTools`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientSession.ListTools)
|
|
(see [pagination](#pagination)). Set
|
|
[`ClientOptions.ToolListChangedHandler`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientOptions.ToolListChangedHandler)
|
|
to be notified of changes in the list of tools.
|
|
|
|
To call a tool, use
|
|
[`ClientSession.CallTool`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientSession.CallTool)
|
|
with `CallToolParams` holding the name and arguments of the tool to call.
|
|
|
|
```go
|
|
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`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#Server.AddTool)
|
|
adds a
|
|
[`Tool`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#Tool) to
|
|
the server along with its
|
|
[`ToolHandler`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#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`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ServerOptions.HasPrompts)
|
|
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 `StructuredContent` field as well as the unstructured `Content` field.
|
|
- Validate that output JSON against the tool's output schema.
|
|
- If any tool errors occurred, pack them into the unstructured content and set
|
|
`IsError` to `true.`
|
|
|
|
For this reason, the SDK provides a generic
|
|
[`AddTool`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#AddTool)
|
|
function that handles this for you. It can bind a tool to any function with the
|
|
following shape:
|
|
|
|
```go
|
|
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`:
|
|
|
|
```go
|
|
mcp.AddTool(server, &mcp.Tool{Name: "my_tool"}, handler)
|
|
```
|
|
|
|
This does the following automatically:
|
|
|
|
- If `Tool.InputSchema` is unset, the input schema is inferred from the `In`
|
|
type, which must be a struct or map.
|
|
- If `Tool.OutputSchema` is unset and the `Out` type is not `any`, the output
|
|
schema is inferred from the `Out` type. Per SEP-2106, `Out` may be any Go
|
|
type whose inferred schema is a valid JSON Schema (struct, map, slice,
|
|
primitive, etc.).
|
|
- Optional `jsonschema` struct tags provide argument and output descriptions.
|
|
- Tool arguments are validated against the input schema.
|
|
- Tool arguments are marshaled into the `In` value.
|
|
- Tool output (the `Out` value) is marshaled into the result's
|
|
`StructuredContent`, as well as the unstructured `Content`.
|
|
- Output is validated against the tool's output schema.
|
|
- If an ordinary error is returned, it is stored int the `CallToolResult` and
|
|
`IsError` is set to `true`.
|
|
|
|
In fact, under ordinary circumstances, the user can ignore `CallToolRequest`
|
|
and `CallToolResult`.
|
|
|
|
For a more realistic example, consider a tool that retrieves the weather:
|
|
|
|
```go
|
|
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:
|
|
|
|
```go
|
|
// 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](https://github.com/modelcontextprotocol/go-sdk/blob/main/mcp/tool_example_test.go) for the full
|
|
example, or [examples/server/toolschemas](https://github.com/modelcontextprotocol/go-sdk/blob/main/examples/server/toolschemas/main.go)
|
|
for more examples of customizing tool schemas._
|
|
|
|
### Tool result content
|
|
|
|
Alongside its structured output, a tool result carries a list of
|
|
[`Content`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#Content)
|
|
blocks in `CallToolResult.Content`, and may mix as many kinds as it needs:
|
|
|
|
| Type | Carries |
|
|
| --- | --- |
|
|
| [`TextContent`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#TextContent) | plain text |
|
|
| [`ImageContent`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ImageContent) | image data, with a MIME type |
|
|
| [`AudioContent`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#AudioContent) | audio data, with a MIME type |
|
|
| [`EmbeddedResource`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#EmbeddedResource) | the contents of a resource, inline |
|
|
| [`ResourceLink`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#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. An
|
|
`EmbeddedResource` wraps a
|
|
[`ResourceContents`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ResourceContents),
|
|
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.
|
|
|
|
```go
|
|
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)
|
|
}
|
|
```
|
|
|
|
`ToolUseContent` and `ToolResultContent` also implement `Content`, but are only
|
|
valid in sampling messages, not in tool results.
|
|
|
|
**Stateless server deployments:** Some deployments create a new
|
|
[`Server`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#Server)
|
|
for each incoming request, re-registering tools every time. To avoid repeated
|
|
schema generation, create a
|
|
[`SchemaCache`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#SchemaCache)
|
|
and share it across server instances:
|
|
|
|
```go
|
|
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 matching
|
|
`notifications/*/list_changed` to every client, which dispatches it to
|
|
[`ClientOptions.ToolListChangedHandler`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientOptions.ToolListChangedHandler),
|
|
[`PromptListChangedHandler`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientOptions.PromptListChangedHandler),
|
|
or
|
|
[`ResourceListChangedHandler`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientOptions.ResourceListChangedHandler).
|
|
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`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#Server.Connect)
|
|
(see [Capabilities](#capabilities)). A server that registers its features
|
|
afterwards, as the example below does, has to declare
|
|
`ServerOptions.HasTools`, `HasPrompts`, or `HasResources` itself, or the
|
|
client is never told.
|
|
|
|
```go
|
|
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
|
|
}
|
|
```
|
|
|
|
## Multi Round-Trip Requests
|
|
|
|
[SEP-2322](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/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 — the
|
|
`InputRequests` field of `CallToolResult`, `GetPromptResult`, or
|
|
`ReadResourceResult` 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 `InputRequests` on the
|
|
result you return.
|
|
Leave `Content` / `StructuredContent` empty — returning
|
|
both at once is a server bug and the SDK returns `-32603 InternalError`.
|
|
- **Calling the legacy APIs.** `ServerSession.Elicit`,
|
|
`ServerSession.CreateMessage(WithTools)`, and `ServerSession.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
|
|
exactly once with the responses already populated. 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).
|
|
|
|
```go
|
|
// Example_mrtr demonstrates the [Multi Round-Trip Requests] pattern
|
|
// (SEP-2322). A tool handler signals "I need more information from the user"
|
|
// by returning a [mcp.CallToolResult] whose `InputRequests` field carries the
|
|
// requests for the additional information. Each request can be an
|
|
// [mcp.ElicitParams] (ask the user a question),
|
|
// [mcp.CreateMessageParams] (sample from the client's LLM), or
|
|
// [mcp.ListRootsParams] (list the client's roots).
|
|
//
|
|
// On protocol version `2026-07-28` and later, the client's
|
|
// `clientMultiRoundTripMiddleware` fulfils each input request from the
|
|
// configured handler and retries the original call transparently. On earlier
|
|
// protocol versions, the server's `serverMultiRoundTripMiddleware` performs
|
|
// the equivalent dance from the server side by calling the legacy
|
|
// server-to-client API (`ServerSession.Elicit`, `CreateMessage`, or
|
|
// `ListRoots`) and re-invoking the handler with the response. Either way,
|
|
// the user-facing client call site sees only the final result.
|
|
//
|
|
// [Multi Round-Trip Requests]: https://modelcontextprotocol.io/specification/draft/basic/patterns#multi-round-trip-requests
|
|
func Example_mrtr() {
|
|
ctx := context.Background()
|
|
|
|
// Server: a "greet" tool that asks the user for their name before
|
|
// returning a greeting. The handler runs twice per logical call:
|
|
// once to issue the elicitation, once to consume the response.
|
|
s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil)
|
|
mcp.AddTool(s, &mcp.Tool{Name: "greet"}, func(_ context.Context, req *mcp.CallToolRequest, _ struct{}) (*mcp.CallToolResult, any, error) {
|
|
if len(req.Params.InputResponses) == 0 {
|
|
// First call: ask the user for their name. The map key
|
|
// ("user_name") is a label the handler chooses; the client must
|
|
// echo it back in InputResponses. The wire-level method name
|
|
// ("elicitation/create") is derived from the value's Go type.
|
|
return &mcp.CallToolResult{
|
|
InputRequests: mcp.InputRequestMap{
|
|
"user_name": &mcp.ElicitParams{
|
|
Message: "What's your name?",
|
|
RequestedSchema: &jsonschema.Schema{
|
|
Type: "object",
|
|
Properties: map[string]*jsonschema.Schema{
|
|
"name": {Type: "string"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
// RequestState is an opaque token the client echoes back on
|
|
// the retry, letting the handler resume its work without
|
|
// per-session storage.
|
|
RequestState: "step=1",
|
|
}, nil, nil
|
|
}
|
|
// Retry: read the elicitation response and produce the final greeting.
|
|
name := req.Params.InputResponses["user_name"].(*mcp.ElicitResult).Content["name"].(string)
|
|
return &mcp.CallToolResult{
|
|
Content: []mcp.Content{&mcp.TextContent{Text: "Hello " + name}},
|
|
}, nil, nil
|
|
})
|
|
|
|
// Client: declares an elicitation handler. The SDK's MRTR middleware
|
|
// uses it to fulfil any input request the tool handler returns.
|
|
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{"name": "MCP Go"}}, nil
|
|
},
|
|
})
|
|
|
|
ct, st := mcp.NewInMemoryTransports()
|
|
if _, err := s.Connect(ctx, st, nil); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
cs, err := c.Connect(ctx, ct, nil)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
defer cs.Close()
|
|
|
|
// Single call site. The MRTR round trip is invisible to the caller.
|
|
res, err := cs.CallTool(ctx, &mcp.CallToolParams{Name: "greet"})
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
fmt.Println(res.Content[0].(*mcp.TextContent).Text)
|
|
}
|
|
```
|
|
|
|
## Cacheable list results
|
|
|
|
[SEP-2549](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2549)
|
|
adds `ttlMs` and `cacheScope` fields to the results of `tools/list`,
|
|
`prompts/list`, `resources/list`, `resources/templates/list`, and
|
|
`resources/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.** After paginating, the SDK sets `CacheScope = "public"`
|
|
and leaves `TTLMs = 0` (which the client cache treats as "immediately
|
|
stale"). A handler that wants its responses cached must set `TTLMs`
|
|
explicitly on the returned result.
|
|
|
|
```go
|
|
mcp.AddTool(server, &mcp.Tool{Name: "expensive"}, func(...) (*mcp.CallToolResult, any, error) {
|
|
// ...
|
|
})
|
|
// Override the default for tools/list:
|
|
server.AddSendingMiddleware(func(next mcp.MethodHandler) mcp.MethodHandler {
|
|
return func(ctx context.Context, method string, req mcp.Request) (mcp.Result, error) {
|
|
res, err := next(ctx, method, req)
|
|
if lr, ok := res.(*mcp.ListToolsResult); ok {
|
|
lr.TTLMs = 30_000 // 30s cache freshness hint
|
|
}
|
|
return res, err
|
|
}
|
|
})
|
|
```
|
|
|
|
## Utilities
|
|
|
|
### Completion
|
|
|
|
To support the
|
|
[completion](https://modelcontextprotocol.io/specification/2025-06-18/server/utilities/completion)
|
|
capability, the server needs a completion handler.
|
|
|
|
**Client-side**: completion is called using the
|
|
[`ClientSession.Complete`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientSession.Complete)
|
|
method.
|
|
|
|
**Server-side**: completion is enabled by setting
|
|
[`ServerOptions.CompletionHandler`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#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.
|
|
|
|
```go
|
|
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](https://modelcontextprotocol.io/seps/2577-deprecate-roots-sampling-and-logging)).
|
|
> 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 `SetLevel` (see below).
|
|
For stateful sessions, the level defaults to "info".
|
|
|
|
[`ServerSession.Log`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#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`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#NewLoggingHandler) to obtain a [`slog.Handler`](https://pkg.go.dev/log/slog#Handler).
|
|
By setting [`LoggingHandlerOptions.MinInterval`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#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`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientOptions.LoggingMessageHandler) to receive log messages.
|
|
|
|
Call [`ClientSession.SetLevel`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientSession.SetLevel) to change the log level for a session.
|
|
|
|
```go
|
|
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](#capability-inference), set
|
|
[`ServerOptions.Capabilities`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#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: false` on 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.
|
|
|
|
```go
|
|
// 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.
|
|
|
|
### Extensions
|
|
|
|
[SEP-2133](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/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](https://modelcontextprotocol.io/specification/2025-06-18/server/utilities/pagination),
|
|
using cursors. The SDK supports this by default.
|
|
|
|
**Client-side**: The `ClientSession` provides methods returning
|
|
[iterators](https://go.dev/blog/range-functions) for each feature type.
|
|
These iterators are an `iter.Seq2[Feature, error]`, where the error value
|
|
indicates whether page retrieval failed.
|
|
|
|
- [`ClientSession.Prompts`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientSession.Prompts)
|
|
iterates prompts.
|
|
- [`ClientSession.Resource`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientSession.Resource)
|
|
iterates resources.
|
|
- [`ClientSession.ResourceTemplates`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientSession.ResourceTemplates)
|
|
iterates resource templates.
|
|
- [`ClientSession.Tools`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#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 use
|
|
[`ServerOptions.PageSize`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ServerOptions.PageSize)
|
|
to customize the page size.
|